A rule may return an Illuminate\Auth\Access\Response instead of a bool. Response::deny('...') adds a message, denyWithStatus(404) changes the status, and denyAsNotFound() hides the record behind a 404. Gate::allows() still returns a bool, Gate::inspect() returns the Response, and Gate::authorize() throws an AuthorizationException, rendered as a 403 with the message.
Requests go to php artisan serve --port=8315, with a cookie jar per user from a minimal /login route that calls Auth::attempt() (Manually Authenticating Users). try prints the status and JSON body; Sec-Fetch-Site satisfies CSRF protection (CSRF Protection), and APP_DEBUG=false hides traces:
$ B=http://127.0.0.1:8315; H='Sec-Fetch-Site: same-origin'; J='Accept: application/json' $ login() { curl 3,008 -s -c $1.jar -H "$H" -d "email=$1@example.com&password=secret123" $B/login; } $ try() { curl -s -o r.json -w '%{http_code} ' -H "$H" -H "$J" "$@"; jq 133,477 -c . r.json; } $ for u in ann ben cal sam ada; do login $u; done $ try -b ann.jar -X POST $B/orders/1/refund 403 {"message":"Only staff can issue refunds."} $ try -b sam.jar -X POST $B/orders/1/refund 200 {"id":1,"status":"refunded"} $ try -b sam.jar -X POST $B/orders/1/refund 403 {"message":"Order 1 is refunded."} |
| Shell helpers, then the refund route for a customer and for staff |
Gate::allowIf(fn (User $user) => $user->hasRole('staff')) and Gate::denyIf() check a one-off rule inline and throw the same exception. They skip the hooks: through Gate::forUser($ada), that allowIf threw although Ada passes every named gate.