简体   繁体   中英

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.

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() 
}

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.

Accessors and mutators are regular getter and setter functions in Laravel. It's just a more "elegant" way to saying it.

I could say, that getter and setter functions are to give you better control over your variables in your class, but it wouldn't help much understanding it a bit.

So what is it for? Why to use it?

We are already familiar with the DRY principle(Don't repeat yourself), which states that duplication is a logic, which should be eliminated. Or in kitchen language; don't write the same code twice, because it takes your time for no reason.

So how is this helps?

For example you have a spellchecker class, which is using an another app on the server - for example hunspell. For small strings you can use it directly and when the request comes, then you process the request, by calling a command with the shell_exec, then you get the result and return with it - everyone is happy.

However when the text is too long, then the php will exceed the 30 seconds(default) limit, and you wont get any results. In that case you can create a script, which is executed by a daemon process, so the 30 seconds limit wont be a problem. When the request arrives, you are passing the text to the daemon process using a unix socket for example, then the background process is doing the spellchecking for you and sends back the result.

In both cases you have to set certain variables for the spellchecker class, which might needs validation or formatting. And you don't want to develop it twice, because it takes time, makes mess, and increases the possible places where you can mess up something.

There are cases, when there are much more then just 2 places where you need to use preparations to set or get the data, and even only on 2 places it can help a lot to avoid code duplication. This can also help preventing the increase of your code complexity.

This is not all!

Sometimes you write classes, and god knows who will use it. Maybe it's for a public package. Then when the developer gets the package, he don't wanna care about how the email will be validated. He don't wanna know why the \n should be removed from the end of the string for specific variables when before he gets the variable from the file. He don't want to care about the implementation part of the integer validation. He just want to use the package, to get the job done as soon as possible.

What you are doing with setters and getters is not other then protecting your classes variables by controlling them through these methods.

Sometimes you want to hide your variables in your class, so other classes wont depend on them. It gives you the flexibility to change the implementation and the variable type.

Should you always use them?

No. Function calls are slow in php. Just check how slow the Laravel comparing to a native PHP code. Sometimes writing the getters and setters are totally meaningless, and just waste of time.

When should I use it then?

Use it when you need to restrict access to your variables. Making getters and setters for each field is overkill. It really depends on the situation, so use it only where it needs to be used.

More on this topic

Getter and Setter?

Why use getters and setters/accessors?

https://dev.to/scottshipp/avoid-getters-and-setters-whenever-possible-c8m

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM