Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel - Lot of Accessors (Mutators) for rendering Views with Blade

I have a Laravel 7 project with the need of a lot of data transformation from the model to the views before being displayed.

I thought about using Laravel Accessors and use them directly in my blade.php files.
But when I finished dealing with a simple html table, I looked at my code and I think there is too many accessors and even some of them have a difficult name to read.

Blade view

@foreach($races as $race)
<tr>
    <td>{{ $race->display_dates }}</td>
    <td>{{ $race->display_name }}</td>
    <td>{{ $race->type->name }}</td>
    <td>{{ $race->display_price }}</td>
    <td>{{ $race->display_places }}</td>
    <td>{{ $race->online_registration_is_open ? 'Yes' : 'No' }}</td>
</tr>
@endforeach

Controller

public function show(Group $group)
{
    $races = $group->races;
    $races->loadMissing('type'); // Eager loading
    return view('races', compact('races'));
}

Model

// Accessors
public function getOnlineRegistrationIsOpenAttribute()
{
    if (!$this->online_registration_ends_at && !$this->online_registration_starts_at) return false;
    if ($this->online_registration_ends_at < now()) return false;
    if ($this->online_registration_starts_at > now()) return false;
    return true;
}

public function getNumberOfParticipantsAttribute()
{
    return $this->in_team === true
        ? $this->teams()->count()
        : $this->participants()->count();
}

// Accessors mainly used for displaying purpose
public function getDisplayPlacesAttribute()
{
    if ($this->online_registration_ends_at < now()) {
        return "Closed registration";
    }
    if ($this->online_registration_starts_at > now()) {
        return "Opening date: " . $this->online_registration_starts_at;
    }
    return "$this->number_of_participants / $this->max_participants";
}

public function getDisplayPriceAttribute()
{
    $text = $this->online_registration_price / 100;
    $text .= " €";
    return $text;
}

public function getDisplayDatesAttribute()
{
    $text = $this->starts_at->toDateString();
    if ($this->ends_at) { $text .= " - " . $this->ends_at->toDateString(); }
    return $text;
}

public function getDisplayNameAttribute()
{
    $text = $this->name;
    if ($this->length) { $text .= " $this->length m"; }
    if ($this->elevation) { $text .= " ($this->elevation m)"; }
    return $text;
}

This code is working but I think it has a lot of cons : readability, mistake are possible, for example if the associated DB table has a name column while I am creating a getDisplayNameAttribute accessor here.
And that's only a start, I think I will need 30-40 more accessors for the other views... Also, I will need to use some of them many time, for example getDisplayNameAttribute could be used in a regular page and an admin page (and maybe more).

I also looked at JsonResource and ViewComposer but JsonResource seems to be for APIs while ViewComposer seems to be especially for Views.

I also thought about prefixing the accessors with something like acc_ to reduce mistakes with existing db column :

public function getAccDisplayNameAttribute() { ... };

But I really don't think that's a solution, I'm not even sure if what I am doing is right or wrong. I also searched for best practices over the internet, without success.

like image 849
Alice Avatar asked May 07 '20 16:05

Alice


People also ask

What are accessors and mutators in eloquent and why should you use them?

Accessors and mutators allow you to format Eloquent attributes when retrieving them from a model or setting their value. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model.

What is the use of accessors and mutators in Laravel?

Laravel Accessors and Mutators are custom, user defined methods. Accessors are used to format the attributes when you retrieve them from database. Whereas, Mutators are used to format the attributes before saving them into the database.

What is mutators in Laravel?

Defining A Mutator 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);

What are accessors and mutators explain with code snippets in Laravel?

Laravel accessors and mutators are custom, user defined methods that allow you to format Eloquent attributes. Accessors are used to format attributes when you retrieve them from the database, while mutators format the attributes before saving them to the database.

What is accessors and Mutators in Laravel eloquent?

Laravel provides Accessors and Mutators in Eloquent. First of all, I will give you a simple definition of what is Accessors and Mutators in Laravel Eloquent, Then I will give you a very simple example where you will understand what it works. "Accessors create a custom attribute on the object which you can access as if it were a database column."

How do I transform eloquent attributes in Laravel?

Accessors, mutators, and attribute casting allow you to transform Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model.

What is Laravel?

Blade Templates - Laravel - The PHP Framework For Web Artisans Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things. Skip to content Prologue Release Notes Upgrade Guide

How to display data in blade views in Laravel Livewire?

Check out Laravel Livewire. Displaying Data You may display data that is passed to your Blade views by wrapping the variable in curly braces. For example, given the following route: Route::get('/', function(){ returnview('welcome',['name'=>'Samantha']); You may display the contents of the namevariable like so: Hello, {{$name}}.


2 Answers

I have 2 solutions for this case which ive tested on my local machine. Though am not sure if they comply with the best practices or design principles, that has to be seen, but i am sure that they will organize the code in a managable way.

First is you can create an accessor that is an array and club all your logic inside it which will compute and return the array. This can work for 4-5 attributes maybe, and you will have to make changes to your views to access the array instead of the properties. Code example 1 is given below

Second way is to create a separate class which will house all the different computing logic as methods. Lets say you make a ModelAccessor class, you can then create accessors in your Model class just the way you are doing now and return ModelAccessor->someMethod from inside each of them. This will add some neatness to your Model class and you can manage your computation logic from the class methods easily. Example code 2 below may make it more clear

  1. Example 1 for arrays as accessors Lets call the returned attribute $stats

public function getStatsAttribute(){

   $stats = [];


   if (!$this->online_registration_ends_at && !$this->online_registration_starts_at) $o=false;
    if ($this->online_registration_ends_at < now()) $o = false;
    if ($this->online_registration_starts_at > now()) $o = false;
    $o= true;

    $stats['online_registration_is_open'] = $o;

    $stats['number_of_participants'] = $this->in_team === true
        ? $this->teams()->count()
        : $this->participants()->count();

    return $stats;
}

You will have to change your view files to use the stats array

<td>{{ $race->stats['number_of_participants'] }} </td>
<td>{{ $race->stats["online_registration_is_open"] ? 'Yes' : 'No' }}</td>

This might become messy for large num of attributes. To avoid that, you can have multiple arrays grouping similar things($stats, $payment_details, $race_registration, etc) or you can use a separate class to manage all of them, as in the next example

  1. Example 2, Separate class with methods to set different attributes
class ModelAccessors
{
protected $race;

function __construct(\App\Race $race){

     $this->race = $race;
}

public function displayPrice()
{
    $text = $race->online_registration_price / 100;
    $text .= " €";
    return $text;
}

Then inside the model

public function getDisplayPriceAttribute()
{
    $m = new ModelAccessor($this);

    return $m->displayPrice();
}

Using this you wont have to update your blade files

3. In case you have 30-40 accessors then i think maintaining a separate class with all the methods will be much simpler. In addition to that you can create the array of attributes from the class itself and call it like this,

class ModelAccessors
{
protected $race;
protected $attributes;

function __construct(\App\Race $race){

     $this->race = $race;
     $this->attributes = [];

}

public function displayPrice()
{
    $text = $race->online_registration_price / 100;
    $text .= " €";
    $this->attributes['display_price'] = $text;
}

public function allStats(){

    $this->displayPrice();
    $this->someOtherMethod();
    $this->yetAnotherMethod();
    // ..
    // you can further abstract calling of all the methods either from 
    // other method or any other way

    return $this->attributes;
}
// From the model class
public function getStatsAccessor()
{
    $m = new ModelAccessor($this);

    // Compute the different values, add them to an array and return it as
    // $stats[
    //         "display_price"= "234 €",
    //         "number_of_participants" = 300;
    //  ]
    return $m->allStats() 
}
like image 119
Kushal Billaiya Avatar answered Sep 19 '22 16:09

Kushal Billaiya


You can create a class that specifically handle your transformations. Then you can load an instance of this class in the Model constructor. You can also make your accessors classes use PHP magic getters if you wish.

Accessors Class

class RaceAccessors {
    // The related model instance
    private $model;

    // Already generated results to avoid some recalculations
    private $attributes = [];

    function __construct($model)
    {
        $this->model = $model;
    }

    // Magic getters
    public function __get($key)
    {
        // If already called once, just return the result
        if (array_key_exists($key, $this->attributes)) {
            return $this->attributes[$key];
        }

        // Otherwise, if a method with this attribute name exists
        // Execute it and store the result in the attributes array
        if (method_exists(self::class, $key)) {
            $this->attributes[$key] = $this->$key($value);
        }

        // Then return the attribute value
        return $this->attributes[$key];
    }

    // An example of accessor 
    public function price()
    {
        $text = $this->model->online_registration_price  / 100;
        $text .= " €";
        return $text;
    }
}

Model Class

class Race {
    // The Accessors class instance
    public $accessors;

    function __construct()
    {
        $this->accessors = new \App\Accessors\RaceAccessors($this);
    }
}

View

@foreach($races as $race)
<tr>
    <td>{{ $race->accessors->price }}</td>
    [...]
</tr>
@endforeach

I did not set a type for the $model variable here because you could use the same accessors class for other models that need to transform some fields in the same way than this accessors class does.

For example, the price accessor can work for a Race (in your case) but it could also work for other models, that way you can imagine creating groups of accessors used by many models without changing too much code to handle that.

like image 45
David Avatar answered Sep 20 '22 16:09

David