How to authorize then update a Laravel model - php

I'm calling this controller to update a model:
public function update(Request $request, $id)
{
$question = Question::find($id);
$this->authorize('edit', $question); // uses Laravel's built-in Policy framework
$updateArray = [
'question' => $request->question,
'type_based_id' => $request->type_based_id,
];
//$question = Question::where('id', $id);
$question = $question->update($updateArray);
// Add the id into the array for return to the client
$updateArray["id"] = $id;
return ['message' => 'Question Updated', 'data' => $updateArray];
}
The code above throws a MassAssignmentException on the call to $question->update(). If I uncomment $question = Question::where('id', $id); it works.
I did some logging, and it seems that find() returns an instance of my model (App\Question) and where() returns a builder (Illuminate\Database\Eloquent\Builder)
How can I satisfy both authorize() and update() without making two separate database requests?
Thank you!

The reason it works using the Query Builder is because it by-passes the mass assignment checks of the model. You are running your own query and not using the Model's update method.
Question::where()->update is calling update on the Query Builder, not the Model.
There is no reason to use the query builder when you already have the model instance you are updating, but this isn't actually running any additional SQL queries.
MassAssignmentException usually means one of the attributes you're passing is guarded in the model. To unguard attributes, either remove them from the $guarded property or add them to the $fillable property of the model. Do NOT use both $guarded and $fillable, you must use one or the other. Read the full documentation here:
https://laravel.com/docs/5.5/eloquent#mass-assignment

MassAssigntmentException is due to the fields you are updating not being fillable and therefor being guarded from assignment, to achieve this you need to set these on the Question class.
public class Question
{
protected $fillable = [
'question',
'type_based_id',
];
}

Related

Laravel model - uncast specific column

I have a laravel table with a column I've defined like this in the migration:
$table->json('json');
And in the model, I cast it to an array:
protected $casts = [
'json' => 'array'
];
This works perfectly the majority of the time I need it, but there's one api call I'm making where I actually want my collection of that Model to give me the raw string rather than casting it to the array.
So, assuming my model is called Model, my api call looks like this:
$systemModels = Model::whereNull('user_id')->get();
$userModels = Model::where('user_id', $user->id)->get();
return response()->json([
'user_models' => $userModels->toArray(),
'system_models' => $systemModels->toArray()
]);
This is where I'd like the 'json' column of my Model to be rendered as a string rather than cast to an array. Is there a reliable way to do that?
Inside your model you can define a custom attribute which is added when the model is serialized:
class YourModel extends Model
{
protected $appends = ['json_raw'];
public function getJsonRawAttribute()
{
return $this->attributes['json'];
// or return json_encode($this->attributes['json']);
}
}
And then when doing the toArray() you can do $userModels->makeHidden('json')->toArray(); to remove the casted field you do not want.

How to update an user using Route Model Binding in Laravel 5.6

I have a profile view where i would like to update users profile.
my Controller:
public function update(Request $request, User $user)
{
$validatedData = $request->validate([
'name' => 'required',
'email' =>'required|email',
'key' => 'required'
]);
// dd($user);
$user->update($validatedData);
// User::whereId($user->id)->update($validatedData);
return back()->with('flash', 'Successfully updated profile.');
}
I'm injecting a model's instance into my route. When i dd($user) i get the current user instance.
Now i would like to update the user with the validatedData. But unfortunately this $user->update($validatedData); is not working. I don't understand why...
This User::whereId($user->id)->update($validatedData); is working but it feels very strange to call on user the user->id.
It's important to understand the difference between the two similar calls.
whereId() returns an instance of the query builder so you're no longer calling methods on the model. So update() is a query builder call, not a model call.
find() (or route model binding) would returning an instance of the Model. Therefore, you'd be calling update() from the Model itself.
Eloquent models have mass assignment protection built in. My assumption is one or more of your columns are not in the $fillable array in your model. Either explicitly add every column into the $fillable array or remove the $fillable array and explicitly add every column that should not be mass assigned to the $guarded array.
https://laravel.com/docs/5.6/eloquent#mass-assignment
If you try update remenber declare fields into $fillable property on model.
protected $fillable = ['name','email','key'];
After that you can update the model using Route Model Binding
I ran into this yesterday. I was using model route binding for a delete action. So, I passed the ONLY the user ID to the DELETE route and hinted {user} in the route like this:
Route::middleware(['auth:sanctum', 'verified'])
->delete('/users/delete/{user}', [UserController::class, 'delete'])
->name('users.delete')
->where('id', '[0-9]+');
And the controller worked so simply like this:
public function delete(User $user) {
$user->delete();
return Redirect::route('users')
->with('success', "User $user->id $user->name was deleted successfully");
}
I was glad to have the user object available for the flash message.
Using the same process, I wanted to add a user update action. So, I did the same thing, I sent only the user ID to the PUT route and hinted user again:
Route::middleware(['auth:sanctum', 'verified'])
->put('/users/edit/{user}', [UserController::class, 'edit'])
->name('users.edit')
->where('id', '[0-9]+');
I got to the controller and could dump the user object. So, I tried a simple update something like this:
public function edit(User $user)
{
$user->update(
[
'name' => user->name,
'email' => user->email,
]
);
}
I received no errors and the update returned true.
But my data did not reflect the intended changes...
Then, in the car a while later, I realized, with model route binding, the route is querying the existing user in the database. So, if I assign the values from that user object in the controller, there will be no change!
::facepalm::
So, it makes sense that the delete action would work fine, but for an update action, you must assign the changed values from the request during the update. Because the values in the request would likely be different than the existing user objects values:
public function edit(Request $request, User $user)
{
$user->update(
$request->validate([
'name' => 'required|string',
'email' => 'required|email',
])
);
return Redirect::route('users')
->with('success', "User $user->id $user->name was updated successfully");
}

Eloquent $appends attributes returning null

I am loading some data into an Eloquent model via an appended attribute and the returned model's attribute is always null. I have protected $appends = array('survey_status); defined in my model (named Survey), and have the accessor defined as such:
public function getSurveyStatusAttribute($value){
return $value;
}
I have tried setting the attribute both as a property and in bracket notation($this->survey_status = ... & $this->attributes['survey_status'] = ..) and also tried using the setAppends() method prior to returning the model, all to no avail.
This was reported on the Laravel forums back at the end of Sept. 2013 and reported as fixed that Oct. (see: https://github.com/laravel/framework/issues/2336 and http://laravel.io/forum/02-26-2014-model-attribute-accessor-not-working-with-object-get-helper-function)
I am on the most current version of Laravel 4 (v4.2.17) which was released in Feb of this year. And from what I read in the docs and elsewhere it seems as though I'm doing everything correctly. Can any see something I'm not doing or confirm this is still an issue?
UPDATE
So I think I figured out 75% of my issue. I didn't realize you could pass an array to $model->load() to make complex queries using where/orWhere/etc. So this basic example works:
$survey = Survey::find(168);
$survey->load(array('surveyStatus' => function ($q){
$q->where('organization_id', '=', 7485);
}));
return Response::json($survey);
In the response my SurveyStatus model is supplied. My issue now is I am trying to iterate of a collection of Survey models to add a SurveyStatus relation just like the working one above but the attribute isn't there on the response. This is what I'm using to iterate the collection:
$org->surveySets->each(function ($ss) use ($id){
$fye = $ss->fiscal_year_end;
$ss->surveys->each(function ($survey) use ($id, $fye){
$sid = $survey->id;
$survey->load(array('surveyStatus' => function ($q) use($id, $fye){
$q->where('organization_id', '=', $id)->where('fiscal_year_end', '=', $fye);
}));
$survey->content_groups->each(function ($cg) use ($id, $sid, $fye){
$cg->content_rows->each(function ($cr) use ($id, $sid, $fye){
$cr->questions->each(function ($q) use ($id, $sid, $fye){
// do the same thing as surveyStatus but load surveyData relation into question
});
});
});
});
});
Is there some reason the loading doesn't 'stick' when iterating over a collection?
Correct me if I'm wrong, but appends doesn't get passed a $value because it's not mapped to a table column. I always thought of it as a computed property of sorts.
Given we have fillable columns 'first' and 'last' we might create an appends called 'fullname'.
protected $appends = [
'fullname'
];
public function getFullnameAttribute()
{
return $this->attributes['fullname'] = $this->first . ' ' . $this->last;
}
Essentially what I think your confusing is that appends is extra data that your appending to your model attributes. You are expected to return a value from the accessor. Returning $value will be null because $value doesn't exist, which is why your manually appending it. Try returning 'foo' in your accessor then you'll see what I mean.
Hello if you want to append some extra data related to another model you could do something like this.
protected $appends = [
'catName'
];
// relation
public function category()
{
return $this->hasOne(PostCat::class, 'id', 'id_cat');
}
//getters
public function getCatNameAttribute()
{
return $this->category()->getEager()->first()->name;
}
If your related model hold many db row considere this
protected $with = [
'category'
];
protected $appends = [
'catName'
];
// relation
public function category()
{
return $this->hasOne(PostCat::class, 'id', 'id_cat');
}
//getters
public function getCatNameAttribute()
{
return $this->category->name;
}
best regards

Laravel Eloquent Save to DB Using Create - Unhelpful Error

I get a very unhelpful error when I try and insert a new record in to my db. Does anyone have any idea where to start to solve this error?
user_id
That's all it says. (This is the name of one of my required fields in the table I'm saving to but it's not very descriptive.)
For reference here's my code:
$data = array(
'user_id'=>1,
'post_type'=>'0',
'post_title'=>'blah',
'description'=>'blah');
//it fails on this line
$id = Post::create($data);
Here's what my model looks like:
class Post extends Eloquent{
public static $rules = array(
'user_id'=>'required',
'post_type'=>'required',
'post_title' => 'required|max:25',
'description'=>'required'
);
public static function validate($data){
return Validator::make($data, static::$rules);
}
public function user(){
return $this->hasOne('User', 'id', 'user_id');
}
}
Tried getting rid of all relationships and validation in the model. That didn't work.
This is called mass-assignment in Laravel, what you need to do to get it working on your model is to set a protected array called $guarded to be empty. Add this to your model.
protected $guarded = array();
What this array does, it can prevent attributes to be mass-assigned with an array, if you don't want an attribute to be filled with the Model::create() method, then you need to specify that attribute in the $guarded array.
If instead you want to specify only the fillable attributes, Laravel's Eloquent also provides an array called $fillable where you specify only the attributes that you want to fill via the mass-assigment way.
Further reading:
Mass Assignment:
http://laravel.com/docs/eloquent#mass-assignment
Create Method:
http://laravel.com/docs/eloquent#insert-update-delete

Maintaining an MVC pattern, making database calls from model using PHPs Laravel 4

I'm trying to maintain an MVC pattern using the Laravel 4 framework, in my last project I made all the database queries from the controller, I have now learned that is a terrible practice and I'm trying to figure out how to do the same from the model. Here is what I would normally do.
Controller
public function serve($company)
{
$review = Review::select('head', 'body', 'logo', 'name')->where('company', '=', $company)->firstOrFail();
return View::make('layouts.singlereview', compact('review'));
}
Model
class Review extends Eloquent {
protected $table = 'reviews';
protected $guarded = [
'id', 'company', 'head', 'body'
];
}
When I move the $review variable and database query into the model then I get the error on the view with undefined variable. How do I pass the $review variable from the model to the controller?
Thanks!
Actually you will still need to do some stuff with your Models in your controller. Use Repository Pattern do to so, which is pretty similar of querying your models in your controller, but being less verbose:
public function serve($company)
{
return View::make('layouts.singlereview')->withReview(with(new Review)->getCompanyData($company));
}
And put the whole logic in your repository:
class Review extends Eloquent {
public function getCompanyData($company)
{
return static::select('head', 'body', 'logo', 'name')
->where('company', '=', $company)
->firstOrFail();
}
}
Also, while you are on the subject of creating good code, you might as well check out Eloquent relationships. They may require database restructuring for certain occurrences, but most of the time you should be good.
With the code you have provided, I can assume that review and company are in a one-to-one relationship, so once this is defined, you can simply retrieve a company object, then do,
$review = $company->review;

Categories