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.
| 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.
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
Userobject, so they callcanDo()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.
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.
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.
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.
$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.
$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.
// app/Providers/AuthServiceProvider.php
protected $policies = [
Post::class => PostPolicy::class,
];Or use automatic policy discovery if your policy follows Laravel's naming convention.
{{-- 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 withcanDo()instead. Roles are a way to group permissions — policies should only care about what a user can do, not what role they hold.