<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function before(User $user, string $ability)
{
if ($user->is_admin) {
return true;
}
return null;
}
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Post $post): bool
{
return $post->published || $user->id === $post->user_id;
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail();
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
<?php
namespace App\Providers;
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Post::class => PostPolicy::class,
];
public function boot(): void
{
$this->registerPolicies();
Gate::define('access-editor', function ($user) {
return $user->hasVerifiedEmail() && !$user->is_banned;
});
}
}
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->authorizeResource(Post::class, 'post');
}
public function store(Request $request)
{
$this->authorize('create', Post::class);
$data = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
]);
$post = $request->user()->posts()->create($data);
return redirect()->route('posts.edit', $post)
->with('status', 'Post created.');
}
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$data = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
]);
$post->update($data);
return redirect()->route('posts.edit', $post)
->with('status', 'Post updated.');
}
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index')
->with('status', 'Post deleted.');
}
}
This snippet shows how Laravel separates who can do what from the controller logic that responds to a request, using a Policy class and the framework's authorization gate. The pattern keeps permission rules in one testable place instead of scattering if ($user->id === $post->user_id) checks across every action.
In PostPolicy, each ability maps to a method that receives the authenticated User and (optionally) the model instance. update and delete return true only when the acting user owns the post, while create allows any verified user and viewAny is open. The important subtlety is the before hook: it runs ahead of every other check and short-circuits the whole class when it returns a non-null value. Returning true for an administrator grants blanket access; returning null (not false) lets the specific ability methods decide, which is why before must not return false for normal users — doing so would deny everything.
AuthServiceProvider wires the model to its policy in the $policies map. Laravel's convention-based discovery often finds this automatically, but registering it explicitly documents the relationship and survives non-standard namespaces. Once mapped, calling authorize('update', $post) anywhere knows to consult PostPolicy@update.
PostController demonstrates two idioms. The constructor calls authorizeResource, which automatically maps each resource action to the matching policy ability, so edit/update require update and destroy requires delete without a line inside those methods. For actions that need explicit control, store still calls $this->authorize('create', Post::class) — passing the class name because no instance exists yet. When a check fails, Laravel throws an AuthorizationException that the framework renders as a 403, so the controller code never branches on permissions itself.
The trade-off is a small amount of indirection: a reader must open the policy to see the rule. In return, the rules become unit-testable in isolation, reusable from Blade via @can, and consistent across HTTP, console, and queued contexts. Reaching for a policy makes sense whenever a permission depends on the relationship between a user and a specific record; simpler, model-agnostic checks are better expressed as standalone Gate::define closures.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import crypto from 'node:crypto';
import jwt from 'jsonwebtoken';
export function signAccessToken(payload: object) {
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m' });
}
JWT access + refresh token rotation (conceptual)
Share this code
Here's the card — post it anywhere.