A nullable user lets a guest reach a policy method; otherwise the gate answers false for guests before your code runs. ReviewPolicy::view(?User $user, Review $review) returns $review->published || $user?->id === $review->user_id. create() has no model, so callers pass the class, can('create', Review::class), to pick the policy. And before() is a policy filter: true or false decides at once, and null falls through to the method:
public function before(User $user, string $ability): ?bool
{
return $user->hasRole('staff') && $ability !== 'create' ? true : null;
}
public function create(User $user): Response
{
return $user->orders()->exists() ? Response::allow()
: Response::deny('Only customers with an order can review books.');
}$ try $B/reviews/3 403 {"message":"This action is unauthorized."} $ try -b sam.jar $B/reviews/3 200 {"id":3,"book":"The Pragmatic Programmer","published":false} $ try -b sam.jar -d 'book=Dune&rating=5' $B/reviews 403 {"message":"Only customers with an order can review books."} |
| A guest and Sam ask for draft review 3, then Sam posts a review |
Sam's filter opened the draft but let create fall through to the order check, which Ben, with an order, passed (201). In tinker, Gate::forUser($sam)->allows('publish', $draft) was false: the gate calls before() only when the policy has a method for the ability. Guests skip before() too, since its $user is not nullable.