Accessor for relation objects in Laravel - php

this is my Category model:
/**
* #var array
*/
protected $guarded = ['id'];
public function media()
{
return $this->belongsTo(Media::class);
}
public function getMediaAttribute()
{
return 'Foo';
return ( ! is_null($this->media))
? $this->media
: '/products/default/thumb.jpg';
}
and when i call it in route for get all object like this:
return \App\Category::with('media')->get();
it seems accessor not work and i can't get 'Foo' in category's media object

You can use withDefault():
public function media()
{
return $this->belongsTo(Media::class)
->withDefault(['url' => '/products/default/thumb.jpg']);
}
When there is no result, it returns a Media instance with the given attributes.

that's not how laravel accessors work
if you created it like
public function getMediaAttribute()
{
return 'Foo';
return ( ! is_null($this->media))
? $this->media
: '/products/default/thumb.jpg';
}
then you will access it like:
return \App\Category::first()->media;
it will work as 'additional field' which can be manipulated in various ways for your model, in this case for the model category
more info on that:
https://laravel.com/docs/5.6/eloquent-mutators#defining-an-accessor
also as mentioned in the comment under your question, accessor with the same name as the relationship will override original field with given name, I'm not 100% sure the last part about overriding, that's how it works with mutators, I think it's same with accessors

Related

Create dynamic Laravel accessor

I have a Product model and also an Attribute model. The relationship between Product and Attribute is many to many. On my Product model I am trying to create a dynamic accessor. I am familiar with Laravel's accessors and mutators feature as documented here. The problem I am having is that I do not want to create an accessor every time I create a product attribute.
For example, A product may have a color attribute which could be setup like so:
/**
* Get the product's color.
*
* #param string $value
* #return string
*/
public function getColorAttribute($value)
{
foreach ($this->productAttributes as $attribute) {
if ($attribute->code === 'color') {
return $attribute->pivot->value;
}
}
return null;
}
The product's color could then be accessed like so $product->color.
If I where to add a size attribute to the product I would need to setup another accessor on the Product model so I could access that like so $product->size.
Is there a way I can setup a single "dynamic" accessor to handle all of my attributes when accessed as a property?
Do I need to override Laravel's accessor functionality with my own?
Yes, you can add your own piece of logic into the getAttribute() function of the Eloquent Model class (override it in your model), but in my opinion, it's not a good practice.
Maybe you can have a function:
public function getProductAttr($name)
{
foreach ($this->productAttributes as $attribute) {
if ($attribute->code === $name) {
return $attribute->pivot->value;
}
}
return null;
}
And call it like this:
$model->getProductAttr('color');
Override Magic method - __get() method.
Try this.
public function __get($key)
{
foreach ($this->productAttributes as $attribute) {
if ($attribute->code === $key) {
return $attribute->pivot->value;
}
}
return parent::__get($key);
}
I think probably Олег Шовкун answer is the right one but if you did want to use the model attribute notation you could get the required argument into the model via a class variable.
class YourModel extends Model{
public $code;
public function getProductAttribute()
{
//a more eloquent way to get the required attribute
if($attribute = $this->productAttributes->filter(function($attribute){
return $attribute->code = $this->code;
})->first()){
return $attribute->pivot->value;
}
return null;
}
}
Then do
$model->code = 'color';
echo $model->product;
But its a bit long and pointless

Laravel how to add a custom function in an Eloquent model?

I have a Product model
class Product extends Model
{
...
public function prices()
{
return $this->hasMany('App\Price');
}
...
}
I want to add a function which will return the lowest price, and in controller I can get the value using:
Product::find(1)->lowest;
I added this in Product model:
public function lowest()
{
return $this->prices->min('price');
}
but I got an error saying:
Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
And if I use Product::find(1)->lowest();, it will work. Is it possible to get Product::find(1)->lowest; to work?
Any help would be appreciated.
When you try to access a function in the model as a variable, laravel assumes you're trying to retrieve a related model. They call them dynamic properties. What you need instead is a custom attribute.
Before Laravel 9
Laravel 6 docs: https://laravel.com/docs/6.x/eloquent-mutators
add following method to your model:
public function getLowestAttribute()
{
//do whatever you want to do
return 'lowest price';
}
Now you should be able to access it like this:
Product::find(1)->lowest;
EDIT: New in Laravel 9
Laravel 9 offers a new way of dealing with attributes:
Docs: https://laravel.com/docs/9.x/eloquent-mutators#accessors-and-mutators
// use Illuminate\Database\Eloquent\Casts\Attribute;
public function lowest(): Attribute
{
return new Attribute(
get: function( $originalValue ){
//do whatever you want to do
//return $modifiedValue;
});
/**
* Or alternatively:-
*
* return Attribute::get( function( $originalValue ){
* // do whatever you want to do
* // return $modifiedValue;
* });
*/
}
Use Eloquent accessors
public function getLowestAttribute()
{
return $this->prices->min('price');
}
Then
$product->lowest;
you can use above methods or use following method to add a function direct into existing model:
class Company extends Model
{
protected $table = 'companies';
// get detail by id
static function detail($id)
{
return self::find($id)->toArray();
}
// get list by condition
static function list($name = '')
{
if ( !empty($name) ) return self::where('name', 'LIKE', $name)->get()->toArray();
else return self::all()->toArray();
}
}
Or use Illuminate\Support\Facades\DB; inside your function. Hope this help others.
why you just dont do this? i know , it's not what you asked for specificallyand it migh be a bad practice sometimes. but in your case i guess it's good.
$product = Product::with(['prices' => function ($query) {
$query->min('price');
}])->find($id);
change follow code
public function lowest()
{
return $this->prices->min('price');
}
to
// add get as prefix and add posfix Attribute and make camel case function
public function getLowestAttribute()
{
return $this->prices->min('price');
}

Laravel - Set Default Value for Relation Models

I have a table accounts:
act_id,
act_name,
act_address
And I have a table addresses:
add_id,
add_street1,
<other fields you'd expect in an address table>
accounts.act_address is a foreign key to addresses.add_id. In Laravel, I have my Account model:
use LaravelBook\Ardent\Ardent;
use Illuminate\Database\Eloquent\SoftDeletingTrait;
class Account extends Ardent
{
use SoftDeletingTrait;
protected $table = 'accounts';
protected $primaryKey = 'act_id';
public static $rules = array
(
'act_name' => 'required|unique:accounts'
);
protected $fillable = array
(
'act_name'
);
public function address()
{
return $this->hasOne('Address', 'add_id', 'act_address');
}
}
As you can see, I have my one-to-one relationship setup here. (Of course, the Address model has a 'belongsTo' as well). This all works.
The thing is, the address foreign key is nullable, as accounts don't require addresses. So, if I try to access the Account->address when it doesn't have one, I'll get a 'trying to access property of non-object' error.
What I'd like to do is set Account->address to a new Address object (all fields empty), if the account record doesn't have one set.
What I've been able to do is either create a second method in the model:
public function getAddress()
{
return empty($this->address) ? new Address() : $this->address;
}
Or, add it on the fly:
if (empty($account->address))
$account->address = new Address();
The first solution is really close, but I'd really like to keep the functionality of accessing address as a property instead of a method.
So, my question is:
How can I have Account->address return new Address() if Account->address is empty/null?
Oh, and I tried overriding the $attributes like so:
protected $attributes = array
(
'address' => new Address()
);
But that throws an error.
Use accessor:
Edit: Since it is belongsTo not hasOne relation, it is a bit tricky - you can't associate a model to non-existing one, for the latter has no id:
public function getAddressAttribute()
{
if ( ! array_key_exists('address', $this->relations)) $this->load('address');
$address = ($this->getRelation('address')) ?: $this->getNewAddress();
return $address;
}
protected function getNewAddress()
{
$address = $this->address()->getRelated();
$this->setRelation('address', $address);
return $address;
}
However, now you need this:
$account->address->save();
$account->address()->associate($account->address);
which is not very convenient. You can alternatively save newly instantiated address in getNewAddress method, or override Account save method, to do the association automatically. Anyway for this relation I'm not sure if it makes sense to do it.. For hasOne it would play nice.
Below is the way how it should look like for hasOne relation:
public function getAddressAttribute()
{
if ( ! array_key_exists('address', $this->relations)) $this->load('address');
$address = ($this->getRelation('address')) ?: $this->getNewAddress();
return $address;
}
protected function getNewAddress()
{
$address = $this->address()->getRelated();
$this->associateNewAddress($address);
return $address;
}
protected function associateNewAddress($address)
{
$foreignKey = $this->address()->getPlainForeignKey();
$address->{$foreignKey} = $this->getKey();
$this->setRelation('address', $address);
}
You could do all this in single accessor, but this is the way it 'should' look like.

Laravel Eloquent setting a default value for a model relation?

I have two models:
class Product extends Eloquent {
...
public function defaultPhoto()
{
return $this->belongsTo('Photo');
}
public function photos()
{
return $this->hasMany('Photo');
}
}
class Photo extends Eloquent {
...
public function getThumbAttribute() {
return 'products/' . $this->uri . '/thumb.jpg';
}
public function getFullAttribute() {
return 'products/' . $this->uri . '/full.jpg';
}
...
}
This works fine, I can call $product->defaultPhoto->thumb and $product->defaultPhoto->full and get the path to the related image, and get all photos using $product->photos and looping through the values.
The problem arises when the product does not have a photo, I can't seem to figure out a way to set a default value for such a scenario.
I have tried doing things such as
public function photos()
{
$photos = $this->hasMany('Photo');
if ($photos->count() === 0) {
$p = new Photo;
$p->url = 'default';
$photos->add($p);
}
return $photos;
}
I have also creating a completely new Collection to store the new Photo model in, but they both return the same error:
Call to undefined method Illuminate\Database\Eloquent\Collection::getResults()
Has anyone done anything similar to this?
Thanks in advance!
You could create an accessor on the Product model that did the check for you. Works the same if you just wanted to define it as a method, also (good for if you want to abstract some of the Eloquent calls, use an interface for your Product in case you change it later, etc.)
/**
* Create a custom thumbnail "column" accessor to retrieve this product's
* photo, or a default if it does not have one.
*
* #return string
*/
public function getThumbnailAttribute()
{
$default = $this->defaultPhoto;
return ( ! is_null($default))
? $default->thumb
: '/products/default/thumb.jpg';
}
You might also want to look into Presenters. A bit overkill for some situations, but incredibly handy to have (and abstract things like this away from your models).

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