Some extra context on "it can get more complex" and why I'm afraid this is a code smell: A Policy is quite a low-level and self-contained context, and sometimes it can be hard to have all the information available - especially if you need data from related models.
Example from the same app: The rules around deleting User models are more complex. A User can have one or more roles: admin, tutor (each class can have one tutor), and student (students can be enrolled in classes).
The User model then has two Eloquent relationships for querying information about the tutor and student roles:
/**
* One-to-many relationship between a (tutor) user and classes they tutor.
*/
public function classesTutored(): HasMany { /* ... */ }
/**
* Many-to-many relationship between (student) users and the classes in which they are enrolled.
*/
public function classesTaken(): BelongsToMany { /* ... */ }
The (simplified) rules for deleting a target User model are:
admin.'classesTutored' relationship must have a count of zero.'classesTaken' relationship must have a count of zero.A naive implementation of this in the UserPolicy would simply query the relationships:
public function delete(User $user, User $target): bool
{
return $user->isAdmin()
&& $target->classesTutored()->count() === 0
&& $target->classesTaken()->count() === 0;
}
However, this does two separate DB requests. A Policy is (IMO) the wrong place for DB requests - I would usually want to make it efficient by eager-loading all the user data at the same time. So by the time it gets to checking the policy, the relevant count data may well already be available. But that may also happen in different ways. Taking the 'classesTutored' relation, for example:
->withCount('classesTutored'), in which case it's available in a classes_tutored_count property on the target User model.->with('classesTutored'), in which case the count is available (without hitting the DB) through $target->classesTutored->count().I've created a trait called SmartCount to automatically cater for these possibilities. The trait is applied to any Model where it's needed (in this case, User). Then the UserPolicy can handle deletes efficiently like this:
public function delete(User $user, User $target): bool
{
return $user->isAdmin()
&& $target->smartCount('classesTutored') === 0
&& $target->smartCount('classesTaken') === 0;
}
The trait itself is:
trait SmartCount
{
/**
* Implements a smart count algorithm for the given relation,
* only hitting the DB if necessary.
*/
public function smartCount(string $relation): int
{
$prop = Str::snake($relation).'_count';
return match (true) {
/* Scenario 1: The count has been loaded using withCount('relation'). */
isset($this->$prop) => $this->$prop,
/* Scenario 2: The collection has been eager-loaded using with('relation'). */
$this->relationLoaded($relation) => $this->$relation->count(),
/* Scenario 3: The count is not available, so a DB request is required. */
default => $this->$relation()->count(),
};
}
}
Again, I'd love any expert opinions on this approach - and if it's too clever for it's own good, what a "better" approach may be.
Sign in to participate in this thread!
The Laravel portal for problem solving, knowledge sharing and community building.