Laravel Nova: Dynamically bind fields to JSON attribute on model - laravel

Laravel Nova
For a particular project, I would like to use the power of the Nova Resource UI combined with a more flexable data model. Specifically, I want to be able to add fields to the resource for attributes stored inside a JSON database field, and not on the table.
Specifics:
Database model: quotations (migration included)
public function up()
{
Schema::create('quotations', function(Blueprint $table)
{
$table->bigInteger('id', true);
$table->timestamps();
$table->string('client_name', 100)->nullable();
$table->string('client_surname', 100)->nullable();
$table->string('status', 10)->nullable()->default('NEW');
$table->text('data')->nullable();
});
}
Nova Resource
So I can define a "normal" NOVA resource and define the following fields (*ignoring status) in App\Nova\Quotation:
public function fields(Request $request)
{
return [
Text::make('Client Name')->sortable(),
Text::make('Client Surname')->sortable(),
];
}
Now my "wish" is to have something to this effect, using the non-existant "bindTo" method to illustrate what I want to achieve
public function fields(Request $request)
{
return [
Text::make('Client Name')->sortable(),
Text::make('Client Surname')->sortable(),
//Fields bound into the JSON data property
Text::make('Client Id')->bindTo('data.client_id),
Date::make('Client Date Of Birth')->bindTo('data.client_date_of_birth),
//etc
];
}
So when a Quotation model is saved, the client_name and client_surname attributes will save to the database as per normal. but client_id and client_date_of_birth should save to the JSON data attribute.
I know I can set up a Mutator on the Quotation model
public function setClientIdAttribute($value)
{
set_data($this->data,'client_id',$value);
}
However that would still require a "non-dynamic" Quoation model. I want to be able to add fields to the view dynmiacally without having to change the Quotation model beyond the basics. A real world example would be where different products have different input fields to gather before generating a quote. I could then easily inject the field deffinition dynamically in the Nova Resource whilst keeping the database model simple.
I also tried the answer proposed to a simpliar question:
Laravel Model Dynamic Attribute
However - the sollution proposed does not work since Nova is still looking for the attribues on the table.
I would love to get some input on how to tackle this requirement.

you can do something like this:
// Model
protected $casts = [
'data' => 'array'
];
// Nova Resource
Text::make('Client Id', 'data->client_id')->resolveUsing(function ($value) {
return $value;
}),

Related

Laravel 9 Cannot UPDATE Data

CONTROLLER
public function update(Request $request)
{
DB::table('bahanbaku')->where('id', $request->id)->update([
'id_bb' => $request->id_bb,
'nama_bb' => $request->nama_bb,
'stok' => $request->stok
]);
dd($request);
return redirect('/bahanbaku');
}
Sorry for my bad english, but in my case,
After Submit, Data still same, not change
Can you help me? T.T
Id does not exist, this is quite basic, but i feel like there is some misunderstandings with Laravel. Just to get the basics down, in Laravel i would expect you to do the following.
Use models for database operations
Instead of passing the id on the request, pass it on URL parameters
Use model binding for getting your model.
Create your model, since it has a non standard table name define that. Add the properties you want to be filled when updating it as fillables.
use Illuminate\Database\Eloquent\Model;
class Bahanbaku extends Model {
protected $table = 'bahanbaku';
protected $fillables = [
'id_bb',
'nama_bb',
'stok',
];
}
In your route, define the model as a parameter.
Route::put('bahanbaku/{bahanbaku}', [BahanbakuController::class, 'update']);
Now the logic can be simplified to this, which will automatically handle if the model is not found and give you a 404 error.
public function update(Request $request, Bahanbaku $bahanbaku)
{
$bahanbaku->fill(
[
'id_bb' => $request->id_bb,
'nama_bb' => $request->nama_bb,
'stok' => $request->stok
],
);
$bahanbaku->save();
return redirect('/bahanbaku');
}
To improve even more, i would look into utilizing form requests.

Can eloquent ignore irrelevant data in Laravel 5.7

This is basically the same question as this here from 2013. Except the answer doesn't work for me.
I have a Model App\Post:
class Post extends Model
{
protected $fillable = ['title'];
// This Model doesn't contain an 'authorname' field
public function author()
{
return $this->belongsTo('App\Author');
}
}
and a Model App\Author:
class Author extends Model
{
protected $fillable = ['name'];
public function posts()
{
return $this->hasMany('App\Post');
}
}
And an array I want to save to that Model:
$posts = [
['title'=>'one post', 'authorname' => 'Mickey'],
['title'=>'another post', 'authorname' => 'Minny'],
];
foreach($posts as $post){
$authorModel=App\Author::firstOrCreate(['name'=>$post['authorname']]);
App\Post::create($post)->author()->associate($authorModel)->save();
}
According to this question, that should work, but I get an
SQL error 42522: Column not found: 1054 Unknown column 'authorname' in 'field list'
which suggests Laravel forwards the whole array to mySQL. Is there a way to make this work without unsetting the authorname key?
Obviously this is a simpified version of what I want to do and keeping track of what to unset seems unnecessary - as would be assigning all array keys to their respective database fields manually.
The only idea I have here is that you run this code in DatabaseSeeder (which automatically unguards models) or you somewhere manually call Eloquent::unguard() (or code similar to this). This would explain why any other fields are used when creating model no matter of $fillable property.

How to get specific columns in Laravel Eloquent with pagination?

I use this table schema:
Schema::create('forms', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 255)->default('');
$table->text('html')->nullable();
$table->text('json')->nullable();
$table->timestamps();
$table->softDeletes();
});
This is the model:
class Form extends Model
{
use SoftDeletes;
protected $fillable = [
'name',
'html',
'json'
];
protected $hidden = ['created_at', 'updated_at', 'deleted_at'];
}
And in the controller I want to show a list of all items of model but only the id and name fileds. Now I use this, but it show all not hidden fields:
public function index() {
return Form::->paginate(100);
}
This function is only for the list of forms names. But here is the second one for show a form datas for modify:
public function show(string $id) {
$item = Form::findOrFail($id);
return response()->json($item);
}
Of course this last one function needs to be show all fields (id, name, html and json too).
Is there any best practice to show only fields what I needed in the index() function using with paginate()?
If i am not wrong then hopefully you can do it something like this for getting specific columns along with pagination:
return Form::paginate(100,['id','name',.....]);
If I read your question correctly, what you want to do is create a collection of the Form object where only the id and the name fields are actually retrieved on the index overview.
You can do that pretty easily by creating a new collection instance in your controller:
public function index() {
// use the Eloquent select() function
$forms = Form::select('id', 'name')->paginate(100);
return $forms;
}
I would personally put that collection in a repository pattern to make it more easily cacheable. Here's a nice canonical reference to repository patterns in Laravel.
In your show function on your controller you needn't change a thing, considering the ID is still the same.
For future reference, remember that the paginate method paginates only the collection it is called on, and not everything related to a specific model or anything other than that collection. Thus, if you create a new collection in any way, and call the paginate method on that new collection, only whatever is inside that will be paginated. It's pretty powerful stuff! Here's the documentation reference.

Laravel nova resource extending/overriding the create method

I am developing a web admin panel using Laravel Nova.
I am having an issue since Nova is quite a new technology.
What I would like to do now is I would like to add a hidden field or extend or override the create method.
This is my scenario. Let's say I have a vacancy nova resource with the following field.
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title')->sortable(),
Text::make('Salary')->sortable()
// I will have another field, called created_by
];
}
Very simple. What I like to do is I want to add a new field called created_by into the database. Then that field will be auto filled with the current logged user id ($request->user()->id).
How can I override or extend the create function of Nova? How can I achieve it?
I can use resource event, but how can I retrieve the logged in user in
the event?
What you're looking for is Resource Events.
From the docs:
All Nova operations use the typical save, delete, forceDelete, restore Eloquent methods you are familiar with. Therefore, it is easy to listen for model events triggered by Nova and react to them.
The easiest approach is to simply attach a model observer to a model:
If you don't feel like creating a new observable you could also create a boot method in your eloquent model as so:
public static function boot()
{
parent::boot();
static::creating(function ($vacancy) {
$vacancy->created_by = auth()->user()->id;
});
}
But please do note that these are a bit harder to track than observables, and you or a next developer in the future might be scratching their head, wondering how's the "created_at" property set.
In my opinion you should go for Observers. Observers will make you code more readable and trackable.
Here is how you can achieve the same with Laravel Observers.
AppServiceProvider.php
public function boot()
{
Nova::serving(function () {
Post::observe(PostObserver::class);
});
}
PostObserver.php
public function creating(Post $post)
{
$post->created_by = Auth::user()->id;
}
OR
You can simply hack a Nova field using withMeta.
Text::make('created_by')->withMeta([
'type' => 'hidden',
'value' => Auth::user()->id
])
You could also do that directly within your Nova resource. Every Nova resource has newModel() method which is called when resource loads fresh instance of your model from db. You can override it and put there your logic for setting any default values (you should always check if values already exist, and only set if they are null, which will only be the case when the model is being created for the first time, which is what you actually need):
public static function newModel()
{
$model = static::$model;
$instance = new $model;
if ($instance->created_by == null) {
$instance->created_by = auth()->user()->id;
}
return $instance;
}
a) Create an Observer class with following command:
php artisan make:observer -m "Post" PostObserver
b) Add following code in the PostObserver:
$post->created_by = Auth::user()->id;
c) Register PostObserver in AppServiceProvider.php
For detailed explanation: https://medium.com/vineeth-vijayan/how-to-add-a-new-field-in-laravel-nova-resource-87f79427d38c
Since Nova v3.0, there is a native Hidden field.
Usage:
Hidden::make('Created By', 'created_by')
->default(
function ($request) {
return $request->user()->id;
}),
Docs: https://nova.laravel.com/docs/3.0/resources/fields.html#hidden-field

Hidden fields from model appear when model is a relation

Situation
So this just happend to me. I have 3 models that have a relation with eachother. My schema is like this:
-- RollingStocks
id
name
[...]
-- Users
id
name
[..]
-- Tasks
id
description
projectleader_id // Corresponds with App\User or App\Group
projectleader_type // Corresponds with App\User or App\Group
rolling_stock_id
[...]
My models have a relationship with eachother:
RollingStock.php
public function task()
{
return $this->hasMany(Task::class);
}
User.php
public function task()
{
return $this->morphMany(Task::class, 'projectleader');
}
Task.php
public function RollingStock()
{
return $this->belongsTo(RollingStock::class);
}
public function projectleader()
{
return $this->morphTo();
}
In my User model I have set the 'password' and 'remember_token' as $hidden which looks like this:
User.php
protected $hidden = [
'password', 'remember_token',
];
Problem
With this little introduction, I will now bring you to my problem. When I fetch in my RollingStocksController all tasks WITH the projectleaders with the following query, the results include the 'hidden' fields in the User model (as projectleader) as well.
$rollingStock = RollingStock::with('task.projectleader')->find($id); // ID matches the ID of the RollingStock I'm trying to fetch)
Result
If I die and dump (dd()) the object has it's relations, BUT then the fields 'password' and 'remember_token' from the User model are vissible and printable to the screen if I loop through the object.
Is there a way to hide the fields, even if the model is (eager) loaded as a relation?
$hidden only hides fields when the result is returned as JSON. It is easy to forget, but the section of the docs is titled "Hiding Attributes From JSON".
In your controller, try:
return $rollingStock;
Laravel will convert that to JSON, and your hidden fields will not show up. Change it to:
dd($rollingStock);
and they will appear, as the result is not JSON, just a dumped variable.

Resources