> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Package policy registration and overrides

> Read Laravel 13's Gate implementation to learn how a package registers policies, avoids depending on the user model, which policy wins during resolution, and how consuming apps override and test them.

Sometimes you want to add authorization to the models a package provides while leaving the consuming application room to swap in its own rules. If you declare the model-to-policy mapping explicitly and check the user only when authorization runs, you separate registration from per-request decisions.

This page refers to the default branch `13.x` of the official Laravel documentation and to the official implementation and tests of the latest Framework release `v13.42.0`, checked on November 10, 2026. For authorization basics, see the [authorization guide](https://kawax.biz/en/authorization.md). For how providers are loaded, see [package auto-discovery](https://kawax.biz/en/advanced/package-discovery.md).

## Don't depend on the consuming app's User class

As an example, add `update` authorization to the package's `Acme\Reports\Models\Report`. The `acme_reports` table is assumed to have an `owner_id` column that stores the owner's authentication identifier. How to ship migrations is covered in [package migrations](https://kawax.biz/en/advanced/package-migrations.md).

`src/Models/Report.php` is an ordinary Eloquent model.

```php theme={null}
<?php

namespace Acme\Reports\Models;

use Illuminate\Database\Eloquent\Model;

class Report extends Model
{
    protected $table = 'acme_reports';
}
```

If the package's policy type-hints `App\Models\User`, it depends on the consuming app's namespace and model structure. If all you need is the user's authentication identifier, use Laravel's `Authenticatable` contract.

Put the following in `src/Policies/ReportPolicy.php`.

```php theme={null}
<?php

namespace Acme\Reports\Policies;

use Acme\Reports\Models\Report;
use Illuminate\Contracts\Auth\Authenticatable;

class ReportPolicy
{
    public function update(Authenticatable $user, Report $report): bool
    {
        $userId = $user->getAuthIdentifier();
        $ownerId = $report->getAttribute('owner_id');

        return $userId !== null
            && $ownerId !== null
            && (string) $userId === (string) $ownerId;
    }
}
```

The policy rules out `null` identifiers first, then casts both values to strings to absorb the type difference between an integer from the database and a string. In this example only the owner may update. In apps that handle several user types or guards, also check whether the identifier alone uniquely identifies the subject. If it doesn't, replace the policy with your own that includes user-type or tenant boundaries.

> [!NOTE]
>
> `Authenticatable` is a contract for things like the authentication identifier. It does not provide `isAdministrator()` or methods for fetching roles. If you need package-specific permission data, define a separate contract and have the consuming app inject an implementation.

## Register the mapping in a service provider

In `boot()` of `src/ReportsServiceProvider.php`, register only the models the package owns.

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Models\Report;
use Acme\Reports\Policies\ReportPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class ReportsServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::policy(Report::class, ReportPolicy::class);
    }
}
```

`Gate::policy()` stores the policy class name keyed by the model's class name. The registration itself does not build the policy, query the database or check any user. The Gate builds the policy with the container's `make()` when it is needed, for example by `Gate::getPolicyFor()`. That means you can inject dependencies into the policy's constructor.

The placement of `Report` and `ReportPolicy` also fits standard auto-discovery, but here the mapping is registered explicitly to make clear what the package provides. Registration alone does not apply authorization to HTTP endpoints. Call the following in a controller or similar before the update runs.

```php theme={null}
use Acme\Reports\Models\Report;
use Illuminate\Support\Facades\Gate;

$report = Report::query()->findOrFail($id);

Gate::authorize('update', $report);
```

When access is denied, an `AuthorizationException` is thrown, which normal HTTP handling turns into a 403 response. The policy method arguments differ between `update`, which receives a model, and abilities such as `create`, which receive the model's class name. Follow the calling conventions in the [authorization guide](https://kawax.biz/en/authorization.md).

## Read the policy resolution order

`Gate::getPolicyFor()` converts an object target to its class name and searches in the following order. This is **the order for choosing a policy**, not the order in which all authorization hooks run.

| Order | What it looks for |
| - | - |
| 1 | An explicit `Gate::policy()` registration for the target class itself |
| 2 | `#[UsePolicy]` on the target class itself |
| 3 | A guess from the standard naming convention or `guessPolicyNamesUsing()` |
| 4 | Explicit registrations for parent classes and implemented interfaces |
| 5 | `#[UsePolicy]` found by walking up the parent classes |

So while an explicit registration exists for the same `Report` class, changing `#[UsePolicy]` or naming conventions alone does not swap the policy. On the other hand, if the consuming app uses a model that extends `Report`, a policy found via the child class's own attribute or a guess comes before the explicit registration on the parent. Check with the model class you actually use, too.

> [!WARNING]
>
> `guessPolicyNamesUsing()` does not add to a list of callbacks. It replaces the single guessing callback the Gate holds. If an independent package sets it just for its own models, it also affects guessing for the consuming app and other packages. Use `policy()` registrations for the models the package owns as the default approach.

## Override explicitly in the consuming app

Calling `policy()` again for the same model replaces the mapping with the later registration. Simply putting it in another provider's `boot()` depends on load order. One approach is to register an application-booted callback from the consuming app's `AppServiceProvider::boot()`.

```php theme={null}
use Acme\Reports\Models\Report;
use App\Policies\ApplicationReportPolicy;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    $this->app->booted(static function (): void {
        Gate::policy(Report::class, ApplicationReportPolicy::class);
    });
}
```

`ApplicationReportPolicy` is a policy the consuming app implements. When the package registers in a normal `boot()`, this callback runs afterwards. It is not a mechanism that unconditionally wins over everything, including re-registrations in other booted callbacks or providers loaded later. In this example, the package's registering provider is not a deferred provider.

Alternatively, you can leave the mapping unchanged and swap what `ReportPolicy::class` resolves to in the container. `resolvePolicy()` asks the container to resolve the policy every time, but whether you actually get a new instance depends on that policy's binding. Avoid designs that read user or request data in the constructor and keep it in a singleton policy.

## Keep hooks that affect all authorization separate

In a normal Gate check, the global `Gate::before()` runs first. If it returns anything other than `null`, the policy check does not run.

```mermaid theme={null}
flowchart TD
    A["Gate::authorize / allows"] --> B["Global Gate::before"]
    B --> C{"Return value is null?"}
    C -->|No| D["Use that result"]
    C -->|Yes| E["Find the target policy and<br>a callable method"]
    E --> F["Call the policy's before if present"]
    F --> G["If before is null,<br>decide with the policy method"]
```

The diagram shows only the path where a policy is found and its matching method can be called. It omits the global `after` hooks that run afterwards and the fallback to Gate definitions when the policy has no matching method.

Don't register a global `Gate::before()` that allows every ability just to implement admin handling inside your package. It would also allow authorization for other packages and the consuming app. A `before()` inside the policy is scoped to that policy, but it is not called when the policy has no method for the ability. When you want the following method to decide, return `null` instead of `false`.

Also, the `update()` in this example requires a non-nullable `Authenticatable`, so the policy method does not run for guests. Unless something like a global hook overrides the result, the request is denied. To allow guests, for example for public viewing, provide a nullable user argument and an explicit check separately.

## Test registration and decisions separately

This assumes the Testbench base class introduced in [package testing](https://kawax.biz/en/advanced/package-testing.md), loading `ReportsServiceProvider::class` in `getPackageProviders()`. The following is an example `tests/Feature/ReportPolicyTest.php`. It uses Laravel's generic authenticatable model for users, so you can check decisions without saving to the database.

```php theme={null}
<?php

namespace Acme\Reports\Tests\Feature;

use Acme\Reports\Models\Report;
use Acme\Reports\Policies\ReportPolicy;
use Acme\Reports\Tests\TestCase;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\User;

class ReportPolicyTest extends TestCase
{
    public function test_provider_registers_the_policy(): void
    {
        $gate = $this->app->make(GateContract::class);

        $this->assertInstanceOf(
            ReportPolicy::class,
            $gate->getPolicyFor(Report::class),
        );
    }

    public function test_only_the_owner_can_update(): void
    {
        $gate = $this->app->make(GateContract::class);
        $report = (new Report)->forceFill(['owner_id' => 7]);
        $owner = (new User)->forceFill(['id' => 7]);
        $other = (new User)->forceFill(['id' => 8]);

        $this->assertTrue($gate->forUser($owner)->allows('update', $report));
        $this->assertFalse($gate->forUser($other)->allows('update', $report));
        $this->assertFalse($gate->forUser(null)->allows('update', $report));
        $this->assertFalse($gate->forUser($owner)->allows('update', new Report));
    }

    public function test_an_override_does_not_change_an_existing_user_gate(): void
    {
        $gate = $this->app->make(GateContract::class);
        $owner = (new User)->forceFill(['id' => 7]);
        $report = (new Report)->forceFill(['owner_id' => 7]);
        $existingUserGate = $gate->forUser($owner);

        $gate->policy(Report::class, DenyReportPolicy::class);

        $this->assertInstanceOf(
            DenyReportPolicy::class,
            $gate->getPolicyFor(Report::class),
        );
        $this->assertFalse($gate->forUser($owner)->allows('update', $report));
        $this->assertTrue($existingUserGate->allows('update', $report));
    }
}

class DenyReportPolicy
{
    public function update(Authenticatable $user, Report $report): bool
    {
        return false;
    }
}
```

`forUser()` does not rewrite the user on the shared Gate. It creates a separate Gate, passing along the mappings, abilities and callbacks as they are at that moment. As a result, a per-user Gate that already exists does not see later policy registration changes on the shared Gate. Finish registration at boot, and don't store per-user Gates in long-lived services.

In the consuming app you also need checks with the real guard and user model, and HTTP tests against the update endpoint. The tests above alone do not verify that authorization is applied to the route or how the owner identifier is stored in the database.

### Contracts to keep when updating

* Have the results for the owner, non-owners, guests and records with no owner stayed the same?
* Do the published ability names match the policy method arguments?
* Does the consuming app's policy replacement still work?
* If you support child models, is the policy actually selected for the child class correct?
* Are real update requests allowed and denied as expected, including the app's global hooks?

Updates that remove existing abilities or change the default allow or deny behavior affect consuming apps. Together with [version compatibility management](https://kawax.biz/en/advanced/package-versioning.md), treat authorization changes as part of your public contract.

## Primary sources

* [Official Laravel documentation: Authorization](https://github.com/laravel/docs/blob/13.x/authorization.md)
* [Official Laravel documentation: Package development](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Framework v13.42.0: Gate registration, resolution and forUser](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Auth/Access/Gate.php)
* [Framework v13.42.0: AuthServiceProvider, which registers the Gate as a singleton](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Auth/AuthServiceProvider.php)
* [Framework v13.42.0: Application boot and booted](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Application.php)
* [Framework v13.42.0: Official tests for the authorization Gate](https://github.com/laravel/framework/blob/v13.42.0/tests/Auth/AuthAccessGateTest.php)


## Related topics

- [Authorization (gates and policies)](https://kawax.biz/en/authorization.md)
- [Advanced Topics](https://kawax.biz/en/advanced/index.md)
- [Package route registration and caching](https://kawax.biz/en/advanced/package-routes.md)
- [Package translation overrides and updates](https://kawax.biz/en/advanced/package-translations.md)
- [Package response macros and HTTP contracts](https://kawax.biz/en/advanced/package-response-macros.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.