gadreel's avatar

Presenters orAccessors

I am a bit confused about presenters and accessors.

Searching the internet about presenters you end up with the same example like below which makes sense if you want to display something that is related to the way you present your data to the view.

public function fullName()
{
	return $this->first_name  . ' ' . $this->last_name;
}

But what if in a blade view you want to have an "if" and "else" statement that depends on certain set of conditions related to your model. For example let's say you want to show or hide an edit button. But to be able to show the button you have the following conditions:

  1. the the post is not older than X days
  2. I am the editor of the post
  3. the post belongs to the category X or Y
  4. another condition
  5. then another condition
  6. etc...
public function canEdit()
{
	if(condition1) {
		return true;
	}
	if(condition2) {
		return true;
	}
	if(condition3) {
		return true;
	}
	if(condition4) {
		return true;
	}
	return false;
}

Do you write the above logic in a Presenter, as an Accessor in the Model or something else?

0 likes
15 replies
LaryAI's avatar
Level 58

In this case, it would be best to use an Accessor in the Model to determine whether or not the edit button should be displayed. The Accessor can take in any necessary parameters (such as the current user) and return a boolean value based on the conditions outlined in the question.

Here's an example of what the Accessor could look like:

public function getCanEditAttribute()
{
    $currentUser = auth()->user();
    $postAge = Carbon::parse($this->created_at)->diffInDays();

    if ($postAge <= config('app.edit_post_age_limit') && $this->editor_id === $currentUser->id && ($this->category === 'X' || $this->category === 'Y') && $this->another_condition && $this->yet_another_condition) {
        return true;
    }

    return false;
}

This Accessor checks if the post is not older than a certain number of days (as defined in the app config), if the current user is the editor, if the post belongs to category X or Y, and if there are additional conditions that are met. If all of these conditions are true, the Accessor returns true, indicating that the edit button should be displayed. Otherwise, it returns false.

In the Blade view, you can then use the Accessor like this:

@if ($post->canEdit)
    <button>Edit</button>
@endif

This will only display the edit button if the Accessor returns true.

1 like
gadreel's avatar

@LaryAI I like your answer, this is what I am thinking too but that does not make the Model code a mess with all those accessors? Can you split that logic to other classes like the presenter and be able to still access it through your model?

cwhite's avatar

This can be greatly simplfied as:

public function canEdit(): bool
{
    return condition1
        || condition2
        || condition3
        || condition4;
}
click's avatar

@cwhite fyi, this sample is an and condition (all must be true to return true). The sample OP's is an or condition, only one of them must be true to let canEdit() return true

public function canEdit(): bool
{
    return condition1
        || condition2
        || condition3
        || condition4;
}
gadreel's avatar

@cwhite Yes you are right but that is not the point. Imagine that those conditions are not that simple. Imagine that the conditions are very complex and a not 5 lines of code but 50.

The point here is where do you place that code? As a model accessor or somewhere else?

cwhite's avatar

@gadreel, I would definitely not place 50 lines of authorization checks into a model accessor.

gadreel's avatar

@cwhite Understood, where to place it and still be able to call it inside a blade view?

gadreel's avatar

@cwhite This is a real example. The accessor below will either hide or display a checkbox in a blade view. Can I still use a Model policy for it?

Policies in my head are more like permissions whether a user can do something. Maybe I am wrong and policies can be used on any model that you want to add conditions that do not depend on the authenticated user.

/**
     * Determine if the given transaction can be sent to the banking system for force payment.
     *
     * @return bool
     */
    public function getCanForcePayAttribute(): bool
    {
        //Is the transaction completed?
        if ($this->isCompleted) {
            return false;
        }
        //If the transaction locked or closed?
        if (in_array($this->transaction->status, [Transaction::STATUS_LOCKED, Transaction::STATUS_CLOSED])) {
            return false;
        }

        //This transaction belongs to the given bank?
        if ($this->bank_code !== $this->getCurrentBankCode()) {
            return false;
        }
        //This transaction has any pending authorisations?
        if ($this->pendingAuthorisations->count() > 0) {
            return false;
        }
        //This transaction has any denied authorisation?
        if ($this->deniedForcePaymentAuthorisations->count() > 0) {
            return false;
        }
        return true;
    }
cwhite's avatar
cwhite
Best Answer
Level 19

@gadreel,

I see what you mean now, although to some extent you still want to validate that the user is authorized to make such a request AND that all of these conditions are true before you allow anyone to force pay. You shouldn't rely on UI elements not being on the DOM as validation.

If this is only used to control the visibility of a checkbox in a single blade view, then there's a couple of things you could do:

  1. consider adding it as a private method the a controller, however some folks like to keep their controllers slim (which is fine) but it's also just a boolean that's only ever going to be used on that view.
  2. it would probably be fine to be on the model as it doesn't rely on the authenticated user, but I would use Laravel 9 style attributes (if you're on >9.x), and again it can be cleaned up:
    /** @return Attribute<bool, never> */
    protected function isForcePayable(): Attribute
    {
        return Attribute::get(fn (): bool => ! $this->is_completed
            && ! in_array($this->transaction->status, [
                    Transaction::STATUS_LOCKED,
                    Transaction::STATUS_CLOSED,
                ])
            && $this->bank_code === $this->getCurrentBankCode()
            && ! $this->pendingAuthorisations()->exists()
            && ! $this->deniedForcePaymentAuthorisations()->exists(),
        );
    }
    
    You could also clean this up even more by creating other accessors which are then referenced in this one:
    /** @return Attribute<bool, never> */
    protected function isForcePayable(): Attribute
    {
        return Attribute::get(fn (): bool => ! $this->is_completed
            && ! $this->is_closed_or_locked // or a better word for this
            && $this->has_current_bank_code
            && ! $this->has_pending_authorizations
            && ! $this->has_denied_force_authorizations,
        );
    }
    
  3. you could place this logic in a 'service' class (like PaymentService::isForcePayable($payment)) to keep the model slim
1 like
gadreel's avatar

@cwhite

Thanks, that is the answer that makes more sense to me. :)

You are right about policies also. To some extend I need to validate whether the user (based on other conditions) is authorised to check that checkbox. As you can see at the Policy example below I am referencing the "User" in order to use the "@can" blade directive but the "$user" variable is not used anywhere...

I will go with the Policy, I want to keep the Model slim and move all the "can" related methods to policies.

Thanks again for your answer and patience.

class TransactionPolicy
{
    use BankCacheTrait;

    /**
     * Determine if the given transaction can be sent to the banking system for force payment.
     *
     * @param  User  $user
     * @param  Transaction  $item
     * @return bool
     */
    public function forcePay(User $user, Transaction$item): bool
    {
        if ($item->isCompleted) {
            return false;
        }

        if (in_array($item->status, [Transaction::STATUS_LOCKED, Transaction::STATUS_CLOSED])) {
            return false;
        }

        if ($item->bank_code !== $this->getCurrentBankCode()) {
            return false;
        }

        if ($item->pendingAuthorisations->count() > 0) {
            return false;
        }

        if ($item->deniedForcePaymentAuthorisations->count() > 0) {
            return false;
        }
        return true;
    }
}

Please or to participate in this conversation.