Laravel Eloquent - Override Model's constructor to change attributes - php

So I have a Page Model, which extends the Eloquent Model class. I am trying to override the constructor, where I need some additional logic. This is what I currently have:
class Page extends Model
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->main_image = null;
}
}
But this does not seem to save the main_image into the $this->attributes property when I call Page::find(1);.
I believe this is because Page::find eventually calls Model::newFromBuilder, which looks like this:
public function newFromBuilder($attributes = [], $connection = null)
{
$model = $this->newInstance([], true);
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
return $model;
}
So as you can see it creates the instance first and then sets the attributes, which means that anything set in the constructor gets ignored.
Is there any workaround for this to be able to override constructor (or similar method) to change the attributes for every retrieved/created model instance? Obviously I could override the newFromBuilder, newInstance, __construct and similar methods, but this seems very hacky and unmaintainable.
Thanks!

If all you need is to be able automatically modify a model's attribute when retrieved or set, then use Laravel Eloquent's Accesors and Mutators:
Defining An Accessor
To define an accessor, create a getFooAttribute method on your model where Foo is the "studly" cased name of the column you wish to access. In this example, we'll define an accessor for the first_name attribute. The accessor will automatically be called by Eloquent when attempting to retrieve the value of the first_name attribute:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get the user's first name.
*
* #param string $value
* #return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
As you can see, the original value of the column is passed to the accessor, allowing you to manipulate and return the value. To access the value of the accessor, you may simply access the first_name attribute on a model instance:
$user = App\User::find(1);
$firstName = $user->first_name;
Defining A Mutator
To define a mutator, define a setFooAttribute method on your model where Foo is the "studly" cased name of the column you wish to access. So, again, let's define a mutator for the first_name attribute. This mutator will be automatically called when we attempt to set the value of the first_name attribute on the model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Set the user's first name.
*
* #param string $value
* #return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
The mutator will receive the value that is being set on the attribute, allowing you to manipulate the value and set the manipulated value on the Eloquent model's internal $attributes property. So, for example, if we attempt to set the first_name attribute to Sally:
$user = App\User::find(1);
$user->first_name = 'Sally';
In this example, the setFirstNameAttribute function will be called with the value Sally. The mutator will then apply the strtolower function to the name and set its resulting value in the internal $attributes array.

Related

Is there any way to access laravel model dynamically through variable when model name is stored in variable?

Basically what I need to do is:
$x = 'Admin';
$model = new \ReflectionClass($x);
$model->getFieldList();
Where I have Admin model inside app folder.
Obviously, this doesn't work. Does anyone have any idea? Can it be done?
Firstly you need to add the namespace to your model. By default this is App\. So your string would have to be "\App\Admin". Now you can simply create a class instance using this string.
$x = '\\App\\Admin';
$model = new $x();
you can do this, but you need to use the fully qualified class name.for example My models are in the Model directory :
$model = 'App\Model\User';
$user=$model::where('id', $id)->first();
i use something like that in the controller constructor. here what i use:
1)first declare a variable in parent controller lets say:
$route_model_name and $model_location.
then add this to each child controller:
function __construct() {
parent::__construct();
$this->setRouteModelName( 'model_route_name' );
$this->setModelLocation( 'App\Models\ModelName' );
}
then you add a method in the parent controller to initialize the model class in order to be ready for making queries:
/**
* #return mixed
*/
protected function getModelClass() {
return app( $this->model_class );
}
then you can change the model location and route in each of the child controllers and still use the same method in the parent controller:
public function edit( $id ) {
return $this->getModelClass()::where( 'id', $id )->first();
}
$model = 'App?Models?'.$model;
$model = str_replace('?','\\',$model);
return $model::products()->paginate();

Return class variable of a Laravel Model in JSON

I have the following model class
class MyModel extends Model {
public $some_variable; // I don't want to store this in the database
protected $fillable = ['column1', 'column2'];
In the controller:
$model = MyModel::find(2);
$model->some_variable = "some value"; // Dynamically calculated each time
return response()->json($model);
The response contains all the columns from MyModel but does not contain $some_variable. Why could this be happening? Are class variables transient by default?
Model's data is internally kept in $attributes array, so you may want to put it there prior converting your data to JSON:
...
$model->some_variable = ...;
return response()->json($model);
Because you have defined $some_variable on the Model, it will not show up in the array/json output. The array/json output only includes the table data (stored in the $attributes property) and the loaded relationship data.
If you want this field to show up, you can override the toArray() method on the model, or you could create an accessor method and add that to the $appends property.
Override toArray():
class MyModel extends Model
{
public $some_variable;
public function toArray()
{
$data = parent::toArray();
$data['some_variable'] = $this->some_variable;
return $data;
}
}
Use an accessor and $appends:
class MyModel extends Model
{
public $some_variable;
protected $appends = ['some_variable'];
public function getSomeVariableAttribute()
{
return $this->some_variable;
}
}
You can read about accessors here. You can read about appending data to json here.

How to retrieve all fields whose table belongs to a model and access them with object reference of that model?

I want to develop proprietary MVC framework in PHP and understand OOP concepts in crystal clear manner. I stuck right here. First look at code snippet....
// main model class
class Model{
protected static $table;
protected static $primary_key;
protected static $conn;
public function __construct()
{
// variable calling from configuration file
global $defalult_database_engine,$connections;
self::dbConnection();
// query to fetch all columns name belo
$query="SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA =? AND TABLE_NAME =?";
$stmt=self::$conn->prepare($query);
$stmt->execute(array($connections['mysql']['database'],'users'));
$fields=$stmt->fetchAll(PDO::FETCH_OBJ);
foreach($fields as $field)
{
$fileldname=$field->COLUMN_NAME;
// creating variable name to matching to the tables fields name
// how to set value of this variable via object
$$fileldname;
}
}
And child model is like this
class Users extends Model
{
protected static $table='users';
}
now turn for controller
class UserController extends Controller{
public function __construct(){
}
public function createUser(){
// user model
$user=new Users();
// calling attributes of the table and set their value
$user->name='full name';
$user->user_name='user name';
$user->password='password';
// finally save the value of fields
$user->save();
}
}
I want to work in above fashion. I convert tables' field names to the variable but unable to reference it via its object.... exactly same way in UserController given above. Is there any idea to make it possible? actually I am currently working in Laravel 4.2 and influenced;
No need to retrieve field details from table; instead of retrieving we must take benefit of dynamic instance variable;which can be generated in php like following code,
$user->user_name; // $user_name variable has been created dynamically and bolongs to the $user table
$user->user_name='your user_name'; // value assigned to dynamic instance variable
$user->save();// this function is defined to the model class
and the code for save() of main model should like as follows:
public function save()
{
$datainfo = (array)$this // this assing the array to the $datainfo variable with all dynamic instance variable
// do your manipulation with data received in $datainfo
}
I see you're trying to recreate Laravel's syntax -- sounds like a fun project.
Laravel utilizes PHP's __get() magic method to create an array of model attributes.
Here's the source code for Laravel's __get and getAttribute methods:
public function __get($key)
{
return $this->getAttribute($key);
}
public function getAttribute($key)
{
if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
return $this->getAttributeValue($key);
}
return $this->getRelationValue($key);
}

Updating timestamps on attaching/detaching Eloquent relations

I'm using Laravel 4, and have 2 models:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product');
}
}
class Product extends \Eloquent {
public function assets() {
return $this->belongsToMany('Asset');
}
}
Product has the standard timestamps on it (created_at, updated_at) and I'd like to update the updated_at field of the Product when I attach/detach an Asset.
I tried this on the Asset model:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product')->withTimestamps();
}
}
...but that did nothing at all (apparently). Edit: apparently this is for updating timestamps on the pivot table, not for updating them on the relation's own table (ie. updates assets_products.updated_at, not products.updated_at).
I then tried this on the Asset model:
class Asset extends \Eloquent {
protected $touches = [ 'products' ];
public function products() {
return $this->belongsToMany('Product');
}
}
...which works, but then breaks my seed which calls Asset::create([ ... ]); because apparently Laravel tries to call ->touchOwners() on the relation without checking if it's null:
PHP Fatal error: Call to undefined method Illuminate\Database\Eloquent\Collection::touchOwners() in /projectdir/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 1583
The code I'm using to add/remove Assets is this:
Product::find( $validId )->assets()->attach( $anotherValidId );
Product::find( $validId )->assets()->detach( $anotherValidId );
Where am I going wrong?
You can do it manually using touch method:
$product = Product::find($validId);
$product->assets()->attach($anotherValidId);
$product->touch();
But if you don't want to do it manually each time you can simplify this creating method in your Product model this way:
public function attachAsset($id)
{
$this->assets()->attach($id);
$this->touch();
}
And now you can use it this way:
Product::find($validId)->attachAsset($anotherValidId);
The same you can of course do for detach action.
And I noticed you have one relation belongsToMany and the other hasMany - it should be rather belongsToMany in both because it's many to many relationship
EDIT
If you would like to use it in many models, you could create trait or create another base class that extends Eloquent with the following method:
public function attach($id, $relationship = null)
{
$relationship = $relationship ?: $this->relationship;
$this->{$relationship}()->attach($id);
$this->touch();
}
Now, if you need this functionality you just need to extend from another base class (or use trait), and now you can add to your Product class one extra property:
private $relationship = 'assets';
Now you could use:
Product::find($validId)->attach($anotherValidId);
or
Product::find($validId)->attach($anotherValidId, 'assets');
if you need to attach data with updating updated_at field. The same of course you need to repeat for detaching.
From the code source, you need to set $touch to false when creating a new instance of the related model:
Asset::create(array(),array(),false);
or use:
$asset = new Asset;
// ...
$asset->setTouchedRelations([]);
$asset->save();
Solution:
Create a BaseModel that extends Eloquent, making a simple adjustment to the create method:
BaseModel.php:
class BaseModel extends Eloquent {
/**
* Save a new model and return the instance, passing along the
* $options array to specify the behavior of 'timestamps' and 'touch'
*
* #param array $attributes
* #param array $options
* #return static
*/
public static function create(array $attributes, array $options = array())
{
$model = new static($attributes);
$model->save($options);
return $model;
}
}
Have your Asset and Product models (and others, if desired) extend BaseModel rather than Eloquent, and set the $touches attribute:
Asset.php (and other models):
class Asset extends BaseModel {
protected $touches = [ 'products' ];
...
In your seeders, set the 2nd parameter of create to an array which specifies 'touch' as false:
Asset::create([...],['touch' => false])
Explanation:
Eloquent's save() method accepts an (optional) array of options, in which you can specify two flags: 'timestamps' and 'touch'. If touch is set to false, then Eloquent will do no touching of related models, regardless of any $touches attributes you've specified on your models. This is all built-in behavior for Eloquent's save() method.
The problem is that Eloquent's create() method doesn't accept any options to pass along to save(). By extending Eloquent (with a BaseModel) to accept the $options array as the 2nd attribute, and pass it along to save(), you can now use those two options when you call create() on all your models which extend BaseModel.
Note that the $options array is optional, so doing this won't break any other calls to create() you might have in your code.

Add a custom attribute to a Laravel / Eloquent model on load?

I'd like to be able to add a custom attribute/property to an Laravel/Eloquent model when it is loaded, similar to how that might be achieved with RedBean's $model->open() method.
For instance, at the moment, in my controller I have:
public function index()
{
$sessions = EventSession::all();
foreach ($sessions as $i => $session) {
$sessions[$i]->available = $session->getAvailability();
}
return $sessions;
}
It would be nice to be able to omit the loop and have the 'available' attribute already set and populated.
I've tried using some of the model events described in the documentation to attach this property when the object loads, but without success so far.
Notes:
'available' is not a field in the underlying table.
$sessions is being returned as a JSON object as part of an API, and therefore calling something like $session->available() in a template isn't an option
The problem is caused by the fact that the Model's toArray() method ignores any accessors which do not directly relate to a column in the underlying table.
As Taylor Otwell mentioned here, "This is intentional and for performance reasons." However there is an easy way to achieve this:
class EventSession extends Eloquent {
protected $table = 'sessions';
protected $appends = array('availability');
public function getAvailabilityAttribute()
{
return $this->calculateAvailability();
}
}
Any attributes listed in the $appends property will automatically be included in the array or JSON form of the model, provided that you've added the appropriate accessor.
Old answer (for Laravel versions < 4.08):
The best solution that I've found is to override the toArray() method and either explicity set the attribute:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
$array['upper'] = $this->upper;
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
or, if you have lots of custom accessors, loop through them all and apply them:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
foreach ($this->getMutatedAttributes() as $key)
{
if ( ! array_key_exists($key, $array)) {
$array[$key] = $this->{$key};
}
}
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
The last thing on the Laravel Eloquent doc page is:
protected $appends = array('is_admin');
That can be used automatically to add new accessors to the model without any additional work like modifying methods like ::toArray().
Just create getFooBarAttribute(...) accessor and add the foo_bar to $appends array.
If you rename your getAvailability() method to getAvailableAttribute() your method becomes an accessor and you'll be able to read it using ->available straight on your model.
Docs: https://laravel.com/docs/5.4/eloquent-mutators#accessors-and-mutators
EDIT: Since your attribute is "virtual", it is not included by default in the JSON representation of your object.
But I found this: Custom model accessors not processed when ->toJson() called?
In order to force your attribute to be returned in the array, add it as a key to the $attributes array.
class User extends Eloquent {
protected $attributes = array(
'ZipCode' => '',
);
public function getZipCodeAttribute()
{
return ....
}
}
I didn't test it, but should be pretty trivial for you to try in your current setup.
I had something simular:
I have an attribute picture in my model, this contains the location of the file in the Storage folder.
The image must be returned base64 encoded
//Add extra attribute
protected $attributes = ['picture_data'];
//Make it available in the json response
protected $appends = ['picture_data'];
//implement the attribute
public function getPictureDataAttribute()
{
$file = Storage::get($this->picture);
$type = Storage::mimeType($this->picture);
return "data:" . $type . ";base64," . base64_encode($file);
}
Step 1: Define attributes in $appends
Step 2: Define accessor for that attributes.
Example:
<?php
...
class Movie extends Model{
protected $appends = ['cover'];
//define accessor
public function getCoverAttribute()
{
return json_decode($this->InJson)->cover;
}
you can use setAttribute function in Model to add a custom attribute
Let say you have 2 columns named first_name and last_name in your users table and you want to retrieve full name. you can achieve with the following code :
class User extends Eloquent {
public function getFullNameAttribute()
{
return $this->first_name.' '.$this->last_name;
}
}
now you can get full name as:
$user = User::find(1);
$user->full_name;
In my subscription model, I need to know the subscription is paused or not.
here is how I did it
public function getIsPausedAttribute() {
$isPaused = false;
if (!$this->is_active) {
$isPaused = true;
}
}
then in the view template,I can use
$subscription->is_paused to get the result.
The getIsPausedAttribute is the format to set a custom attribute,
and uses is_paused to get or use the attribute in your view.
in my case, creating an empty column and setting its accessor worked fine.
my accessor filling user's age from dob column. toArray() function worked too.
public function getAgeAttribute()
{
return Carbon::createFromFormat('Y-m-d', $this->attributes['dateofbirth'])->age;
}

Categories