In previous chapters, we discussed the basics about models:
we created a model for each database table
used the $fillable or $guarded properties to prevent mass assignment vulnerability
used the $hasMany and $belongsTo relations between the database tables
In this chapter, you will get an introduction to:
accessors and mutators
hide attributes from the database table
add additional properties
query scopes
For these examples, we work in the controller app/Http/Controllers/Admin/RecordController.php and the related view resources/views/admin/records/index.blade.php
do this first
Before starting this chapter, make sure you have configured the debug middleware
If we don't need the created_at and updated_at attributes in our JSON representation, we can hide them by adding these fields
to the $hidden array in the Genre model
classGenreextendsModel{.../** Add attributes that should be hidden to the $hidden array */protected$hidden=['created_at','updated_at'];}
1 2 3 4 5 6 7
What if you want to hide these attributes for almost every query except for this one?
No problem, you can make hidden attributes back visible inside the controller by chaining the makeVisible()methode AFTER you get all the genres
If you want an attribute to be visible in most of your queries, except for one, leave the $hidden array in the model empty
and chain the makeHidden() method inside the controller.
E.g. $genres = Genre::orderBy('name')->get()->makeHidden(['created_at', 'updated_at']);
REMARK
$hidden and makeHidden() only hides attributes from the JSON representation but you can still use the hidden attribute in a view!
classGenreextendsModel{.../** Accessors and mutators (method name is the attribute name) */protectedfunctionname(): Attribute
{return Attribute::make(
get:fn($value)=>ucfirst($value),// accessor
set:fn($value)=>strtolower($value),// mutator);}...}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Remove the makeVisible()methode from the previous example
Line 7: loop over all the genres @foreach ($genres as $genre) ... @endforeach
Line 10: show the genre name with $genre->name
Line 11:
count the number of records belonging to this genre with $genre->records()->count()
pluralize(opens new window) the word recordStr::plural('record', $genre->records()->count())
(if the second argument is 1, the word is singular, otherwise it is plural)
Line 14: loop over all the records belonging to this genre @foreach($genre->records as $record) ... @endforeach
Line 15: show the artist of the record with $record->artist
Line 15: show the title of the record with $record->title
Line 3: get all records with the genre, ordered first by the artist and secondly by the title
(if an artist has multiple records, the records for this artist are ordered by the title)
In the view, we can select the genre name with $record->genre->name and that's fine but won't it be easier to add the genre name as an additional attribute to the record?
To add additional attributes to the JSON representation of the model
First, define an accessor for the attribute you want to add
(The name of the accessor must be different from the attribute names in the database table)
Then, add the attribute to the $appends array in the model
The accessor genreName searches in the Genre table for the record with id equal to genre_id from the record and gets the name attribute
Genre::find($attributes['genre_id'])->name
E.g. the record Atari Teenage Riot - The Future of War has the genre_id of 12, so the accessor searches in the Genre table for the record with id=12 and gets the name attribute Genre::find(12)->name = Noise
Next, append the accessor in snake_case to the $appends array
classRecordextendsModel{.../** Add additional attributes that do not have a corresponding column in your database */protectedfunctiongenreName(): Attribute
{return Attribute::make(
get:fn($value,$attributes)=> Genre::find($attributes['genre_id'])->name,);}protected$appends=['genre_name'];...}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Line 3: delete the with('genre') because we don't need it anymore
Next, append the accessor in snake_case to the $appends array
classRecordextendsModel{.../** Add additional attributes that do not have a corresponding column in your database */protectedfunctiongenreName(): Attribute {...}protectedfunctionpriceEuro(): Attribute
{return Attribute::make(
get:fn($value,$attributes)=>'€ '.number_format($attributes['price'],2),);}protected$appends=['genre_name','price_euro'];...}
As you may have noticed, there is no cover column in the records table
The image refers to the record's mb_id attribute and can be found in the covers folder.
E.g. if the mb_id value is fcba15e2-3d1e-40b3-996c-be22450bda82, the path to the cover is /storage/covers/fcba15e2-3d1e-40b3-996c-be22450bda82.jpg
else return the path to the dummy cover (no-cover.png)
Line 13: search in the public disk (= public folder) for the file with the path /storage/covers/{$attributes['mb_id']}.jpg
Line 14 - 17: if the file exists, return an associative array with:
exists: true
url: the path to the file
Line 14 - 17: if the file doesn't exist, return an associative array with:
exists: false
url : the path to the dummy cover
Next, append the accessor to the $appends array
classRecordextendsModel{.../** Add additional attributes that do not have a corresponding column in your database */protectedfunctiongenreName(): Attribute {...}protectedfunctionpriceEuro(): Attribute {...}protectedfunctioncover(): Attribute
{return Attribute::make(
get:function($value,$attributes){if(Storage::disk('public')->exists('covers/'.$attributes['mb_id'].'.jpg')){return['exists'=>true,'url'=> Storage::url('covers/'.$attributes['mb_id'].'.jpg'),];}else{return['exists'=>false,'url'=> Storage::url('covers/no-cover.png'),];}},);}protected$appends=['genre_name','price_euro','cover'];...}
Sometimes you want to filter the results of a query by a certain attribute value
This can be done with the where method inside the controller, but you can also do it in the model with local or global query scopes,
so you don't have to write the same code twice
Local scopes allow you to define common sets of query constraints that you may easily re-use throughout your application,
e.g. to filter out all users with the admin role
Let's create a filter for the records with a price less or equal to € 20
first, we filter the records inside the controller
then we refactor the controller and move the filter to the model
Line 3: set the price limit to € 20 $maxPrice = 20;
Line 4: limit the result to only records up to 20 euro where('price', '<=', $maxPrice)
Let's create a global scope to filter the records with a price less or equal to $maxPrice
first, we create the scope
then we add it to the model
Local scopes always start with the prefix scope, followed by the name of the scope
e.g. the scope scopeMaxPrice($query, $price = 100) can be used inside the controller as:
maxPrice($maxPrice)
maxPrice() (if the default value of 100 is used)
the first parameter in the function is always the query
the second parameter (optional) is the value or values to filter by
classRecordextendsModel{.../** Apply the scope to a given Eloquent query builder */publicfunctionscopeMaxPrice($query,$price=100){return$query->where('price','<=',$price);}...}
1 2 3 4 5 6 7 8 9 10 11 12
Line 4: replace where('price', '<=', $maxPrice) with maxPrice($maxPrice)
Because the list of records can be very long, it's better to show only a fraction of the records and add some pagination(opens new window) to the views
You can limit the number of records per page by replacing get() with paginate(x), where x is the number of records per view (e.g. 6)
Add a pagination navigation to the view (e.g. one before and another one after the loop) using $records->withQueryString()->links()
Line 5: set the number of records to display per page $perPage = 6;
Now that we added some extra attributes to the model, it's sometimes hard to remember all those attributes and the relations to other models
The artisan CLI has the great command php artisan model:show <Model> to generate a compact overview with a lot of information about the model
(table name, fillable attributes, appended attributes, relations to other models, ...)
Inspecting database information requires the Doctrine DBAL (doctrine/dbal) package. Would you like to install it?
The first time you execute this artisan command, it might ask you to install the Doctrine DBAL package.
Choose y and press enter to continue
Re-execute the php artisan model:show <Model> command
Run the command php artisan model:show Genre in the console
Run the command php artisan model:show Record in the console
Run the command php artisan model:show User in the console