Skip to content

Latest commit

 

History

History
158 lines (113 loc) · 5.08 KB

File metadata and controls

158 lines (113 loc) · 5.08 KB

Integrating with Model Policies

Marque handles permission data — which roles have which permissions in which scopes. Laravel model policies handle business logic — ownership checks, time-based rules, state flags. Use them together: the policy delegates permission questions to canDo() and handles the business rules itself.

Deciding when to use a policy

Scenario Use a policy?
Pure permission check (posts.create) No — use $user->can() directly
Ownership check (.own vs .any) Yes
Time-based rules (locked after 24h) Yes
State-based (pinned, archived, draft) Yes
Compound (permission + business rule) Yes

If the authorization decision depends only on a permission string, skip the policy. If it depends on the resource being acted on, write a policy.

Writing a policy that uses canDo

class PostPolicy
{
    public function create(User $user, Group $group): bool
    {
        return $user->canDo('posts.create', scope: $group);
    }

    public function update(User $user, Post $post): bool
    {
        $group = $post->group;

        if ($user->canDo('posts.update.any', scope: $group)) {
            return true;
        }

        return $user->canDo('posts.update.own', scope: $group)
            && $post->user_id === $user->id;
    }

    public function delete(User $user, Post $post): bool
    {
        $group = $post->group;

        if ($post->is_pinned) {
            return $user->canDo('posts.delete.pinned', scope: $group);
        }

        if ($user->canDo('posts.delete.any', scope: $group)) {
            return true;
        }

        return $user->canDo('posts.delete.own', scope: $group)
            && $post->user_id === $user->id;
    }
}

The policy never hardcodes role names. It asks canDo() about permissions and applies business logic (ownership, pinned state) on top.

Policy methods receive a raw User object, so they call canDo() directly on the trait. This is the correct usage inside policies — canDo() is the engine method that evaluates permissions directly, without going through the Gate.

Using the policy in a controller

class PostController extends Controller
{
    public function update(Post $post)
    {
        $this->authorize('update', $post);

        // update post...
    }

    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);

        // delete post...
    }
}

Standard Laravel authorization — $this->authorize(), Gate::allows(), @can in Blade. The policy handles the canDo() call internally.

How the Gate hook interacts with policies

The Gate hook only intercepts dot-notated abilities (like posts.create). Non-dot abilities (like update, delete, create) pass through to your model policies as usual.

$user->can('posts.create');       // Gate hook → Marque
$user->can('update', $post);     // Standard Gate → PostPolicy::update()
$this->authorize('delete', $post); // Standard Gate → PostPolicy::delete()

When a policy method internally calls canDo(), it goes directly to the HasPermissions trait — it does not re-enter the Gate.

Passing a resource model through the Gate

Models that use the HasResourcePolicies trait are automatically detected as resources when passed to can().

use DynamikDev\Marque\Concerns\HasResourcePolicies;

class Post extends Model
{
    use HasResourcePolicies;
}
$user->can('posts.update', $post);

The Gate hook calls $post->toPolicyResource() and forwards the resulting Resource DTO to the evaluator. This makes the resource available to the ResourcePolicyResolver and any conditions that inspect resource attributes.

Passing both a scope and a resource

$user->can('posts.update', [$team, $post]);

When the Gate receives an array, the first argument is treated as the scope and the second is checked for the HasResourcePolicies trait. If the second argument has the trait, it is converted to a Resource DTO.

Scope-only behavior (unchanged)

$user->can('posts.update', $team);

When the argument does not have the HasResourcePolicies trait, it is treated as a scope — the same behavior as v1.

Registering the policy

// app/Providers/AuthServiceProvider.php
protected $policies = [
    Post::class => PostPolicy::class,
];

Or use automatic policy discovery if your policy follows Laravel's naming convention.

Using @can in Blade with policies

{{-- Pure permission check — no resource involved --}}
@can('posts.create', $group)
    <button>New Post</button>
@endcan

{{-- Resource-level check — hits PostPolicy::delete() --}}
@can('delete', $post)
    <button>Delete</button>
@endcan

@can handles both cases. Dot-notated abilities route through the Gate hook to Marque. Non-dot abilities route through model policies. Both work in the same template.

Policies should never contain role names (if ($user->hasRole('admin'))). Use permission checks with canDo() instead. Roles are a way to group permissions — policies should only care about what a user can do, not what role they hold.