Eloquent's default $attributes don't get mutated - laravel

It seems that default values defined in the $attributes property are not being mutated before saving to the database?
I have this setup:
protected $attributes = array(
'data' => array();
);
public function getDataAttribute($value)
{
return unserialize($value);
}
public function setDataAttribute($value)
{
$this->attributes['data'] = serialize($value);
}
All I get is database field filled with literally 'Array' and a preg_replace error. Upon specifying the default attribute in an overridden save() method it does get mutated.
Can anyone confirm this and/or has suggestions?
Thanks.

You are assigning a default variable on the object itself, which is outside the function of a mutator.
The best option is probably to include the default in the constructor of the model, so they are called and mutated when the object is created. I think something like this should work:
function __construct($attributes = array()) {
parent::__construct($attributes);
$this->data = array();
}

Related

Assign value to custom property in model

How to assign value to my custom property ? I am comming from Yii2 background and there is pretty straight forward. Here I made this:
protected $appends = ['sites'];
public function getSitesAttribute()
{
return $this->sites;
}
public function setSitesAttribute($value)
{
$this->attributes['sites'] = $value;
}
Trying to assign like mymodel->sites = ['1', '2', '3'] but without success. After trying to get the mymodel->sites value error is thrown: undefined property 'sites'.
Your accessor is not quite correct, note that you're not returning $this->attributes['sites']; but instead $this->sites.
class MyModel extends Model
{
public function getSitesAttribute()
{
return $this->attributes['sites'];
}
public function setSitesAttribute($value)
{
$this->attributes['sites'] = $value;
}
}
Then;
$my_model->sites = [1,2,3];
$my_model->sites; // [1,2,3]
You might want to set an initial default value for $this->attributes['sites'] in the model constructor.
The problem lies with the $this->sites. It is correct that it will throw an exception that you class instance does not have a property $sites.
Take a look at the __get method of Model. You can see here that a getAttribute($key) call is being made. Diving even deeper you would find out that at some point the following code is being executed in the transformModelValue method:
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
This piece of code calls your getSitesAttribute method.
Going back a little bit and you will see that it tries to retrieve the value for the $key (sites) from the attributes array by calling the getAttributeFromArray($key). This will return the value stored in the attributes array which is where your setSitesAttribute mutator stores the value.
The following options I could come up with:
Remove the accessor to let the model retrieve the value from the $attributes array.
protected $appends = ['sites'];
public function setSitesAttribute($value)
{
$this->attributes['sites'] = $value;
}
Store value in and retrieve from $sites property. Not sure if $appends still works.
protected $appends = ['sites'];
private $sites = [];
public function getSitesAttribute()
{
return $this->sites;
}
public function setSitesAttribute($value)
{
$this->sites = $value;
}

Laravel Eloquent: how to set custom attribute when model is construted?

I am trying to add a custom attribute "role" to my User model. My current implementation is like this:
User extends Authenticatable
{
protected $appends = array('role');
public $getRoleAttribute()
{
$role = DB::table('acl_user_has_roles')->where('user_id', $this->id)
->value('role');
return $role;
}
}
This implementation largely works. The concern is, this role attribute is referenced many times in the life time of $user instance. Whenever it is reference, the getRoleAttribute() function will be called, then database queries will be executed. It seems a bit unnecessary to me, so I am trying find a way to only run these queries once, preferably when model instance is contructed:
I tried to override model constructor as described in answer to another similar question:
public $role;
public function __construct(array $attributes = array())
{
parent::__construct($attributes);
$this->role= $this->role();
}
protected function role()
{
$role = DB::table('acl_user_has_roles')->where('user_id', $this->id)
->value('role');
return $role;
}
When I tried to reference the role attribute like this:
$user = User::find(1);
echo $user->role;
I get nothing.
if I simply set the role attribute to some dummy text:
$this->role = "Dummy Role";
instead of:
$this->role();
Then I can get this "Dummy Role" text.
What am I missing here?

Eloquent ORM: Define allowed model attributes

In laravel's eloquent ORM, is there a way to define a model's allowed attributes?
By default I can put any attributes into the model's constructor - but then I only get notified about the erroneous attribute names when I actually try to save the model to database.
Example code:
// this works although there is a typo in "lastname"
$user = new \App\User(['firstname' => 'foo', 'lastnam' => 'bar']);
// this errors out with an SQL error
$user->save();
So, is there a way to let Laravel automatically check if there are invalid keys in the request's input data?
If you would like to prevent not only filling not allowed attributes using fill() method but also directly setting them, like $model->foo = 'bar', then you got to override Model::setAttribute() method.
Best to do it in a custom base Model that extends Eloquent. So in app/Model.php:
namespace App;
use Exception;
use Illuminate\Database\Eloquent\Model as Eloquent;
class Model extends Eloquent
{
// this should be actually defined in each sub-model
protected $allowed = ['firstname', 'lastname'];
public function setAttribute($key, $value)
{
// this way we can allow some attributes by default
$allowed = array_merge($this->allowed, ['id']);
if (! in_array($key, $allowed)) {
throw new Exception("Not allowed attribute '$key'.");
}
return parent::setAttribute($key, $value);
}
}
Then in the models that should not allow invalid attributes you can extend this base model:
use App\Model;
class User extends Model
I don't believe this can be done natively. I think Laravel is intentionally permissive in that sense, and I personally don't mind having a SQL error instead of an Eloquent one if I make a mistake setting attributes somewhere.
That being said, it's not hard to customize your Models to fail when non-existing attributes are set:
// User.php
protected $fillable = [
'firstname',
'lastname',
];
public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
if (!in_array($key, $this->getFillable())) {
throw new \Exception("Attribute [{$key}] is not fillable.");
}
}
return parent::fill($attributes);
}
When you're adding attributes like this, Laravel uses the fill() method which is part of mass assignment feature:
if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
} elseif ($totallyGuarded) {
throw new MassAssignmentException($key);
}
So, to make it work add all allowed values you want to be saved to $fillable array :
$fillable = ['firstname', 'lastname'];
You could override the model constructor and validate there:
use Illuminate\Support\Facades\Schema;
//...
public function __construct(array $attributes = [])
{
$columns = Schema::getColumnListing($this->table);
foreach ($attributes as $attribute => $value) {
if (! in_array($attribute, $columns)) {
// not allowed
}
}
parent::__construct($attributes);
}
You can use laravel exists:column validation rule for each input.
Please check the documentation https://laravel.com/docs/5.3/validation#rule-exists
OR
You can make helper for this purpose
$table is table name
function validateInputColumns($table, array $inputs)
{
$unknownCols = null;
$i = 0;
foreach ($inputs as $key => $val) {
if (! Schema::hasColumn($table, $key)) {
$unknownCols[$i] = $key;
$i++;
}
}
return is_null($unknownCols) ? true : $unknownCols;
}
It will return the unknown column list in array.
If I understand you correctly, Eloquent Events might be of help to you.
You could then compare the input array to the fillable array.

What is different between save(), create() function in laravel 5

I need to know what is the difference of save() and create() function in laravel 5.
Where we can use save() and create() ?
Model::create is a simple wrapper around $model = new MyModel(); $model->save()
See the implementation
/**
* Save a new model and return the instance.
*
* #param array $attributes
* #return static
*/
public static function create(array $attributes = [])
{
$model = new static($attributes);
$model->save();
return $model;
}
save()
save() method is used both for saving new model, and updating
existing one. here you are creating new model or find existing one,
setting its properties one by one and finally saves in database.
save() accepts a full Eloquent model instance
$comment = new App\Comment(['message' => 'A new comment.']);
$post = App\Post::find(1);
$post->comments()->save($comment);
create()
while in creating method you are passing an array, setting properties in
model and persists in the database in one shot.
create() accepts a plain
PHP array
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
EDIT
As #PawelMysior pointed out, before using the create method, be sure to mark columns whose values are safe to set via mass-assignment (such as name, birth_date, and so on.), we need to update our Eloquent models by providing a new property called $fillable. This is simply an array containing the names of the attributes that are safe to set via mass assignment:
example:-
class Country extends Model {
protected $fillable = [
'name',
'area',
'language',
];
}

Laravel Model create function returns column with null value

In Laravel, When I run the following query, it returns a row with null values.
//Cards.php
public function __construct(array $attributes = []) {
$this->gateway = StripeGateway;
}
protected $fillable = ['user_id', 'card_id', 'customer_id', 'exp_year', 'exp_month', 'funding', 'brand', 'last4'];
public function createNewCardFromCustomer($user_id, $customer)
{
$result = $this->create([
'user_id' => $user_id,
'customer_id' => $customer->id,
'card_id' => $customer['sources']['data'][0]->id,
'exp_year' => $customer['sources']['data'][0]->exp_year,
'exp_month' => $customer['sources']['data'][0]->exp_month,
'funding' => $customer['sources']['data'][0]->funding,
'brand' => $customer['sources']['data'][0]->brand,
'last4' => $customer['sources']['data'][0]->last4
]);
return $result;
}
Even the Model static create method receives the right parameters. And I've taken care of the mass assignment also.
I posted this on Laracasts too :)
Anyway, you have to change your constructor to this:
public function __construct(array $attributes = []) {
$this->gateway = StripeGateway;
parent::__construct($attributes);
}
You are overriding the Model's base constructor, which changes its default behavior. Laravel uses the constructor for a lot of things (create method, relationships, etc.).
The base model's constructor function does several things, but one very important part of it is that it accepts an array to fill out its attributes as can be seen here:
public function __construct(array $attributes = [])
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
So, after you set your gateway property, you should call the parent's constructor function and pass the attributes.

Resources