Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Porter Laravel Package

hdaklue/porter

Lightweight, fast access control for Laravel with roles modeled as domain logic. Assign roles between any Assignable (users/teams) and Roleable (projects/docs) via a Roster. Supports cross-database role assignments for complex, distributed architectures.

View on GitHub
Deep Wiki
Context7

Laravel Integration

Porter integrates seamlessly with Laravel's existing authorization system, working alongside Gates, Policies, and Blade directives.

The Game Changer: Porter's isAtLeastOn() method eliminates verbose hasRole('admin') || hasRole('manager') patterns with a single hierarchy-aware call. Instead of listing every acceptable role, you simply express business logic: "needs at least manager level." This one-liner approach combines assignment checking + hierarchy comparison, delivering both cleaner code and better maintainability.

Table of Contents

Policy Classes

Porter works perfectly with Laravel's Policy classes for clean, testable authorization logic:

class ProjectPolicy
{
    public function view(User $user, Project $project)
    {
        return $user->hasAnyRoleOn($project);
    }

    public function update(User $user, Project $project)
    {
        return Porter::isAtLeastOn($user, RoleFactory::manager(), $project);
    }

    public function delete(User $user, Project $project)
    {
        return $user->hasRoleOn($project, 'admin');
    }

    public function invite(User $user, Project $project)
    {
        return Porter::isAtLeastOn($user, RoleFactory::manager(), $project);
    }
}

Using Policies in Controllers

class ProjectController extends Controller
{
    public function show(Project $project)
    {
        $this->authorize('view', $project);
        
        return view('projects.show', compact('project'));
    }

    public function update(UpdateProjectRequest $request, Project $project)
    {
        $this->authorize('update', $project);
        
        $project->update($request->validated());
        
        return redirect()->route('projects.show', $project);
    }
}

Blade Directives

Use Laravel's [@can](https://github.com/can) directive with Porter-powered policies:

[@can](https://github.com/can)('view', $project)
    <a href="{{ route('projects.show', $project) }}">View Project</a>
[@endcan](https://github.com/endcan)

[@can](https://github.com/can)('update', $project)
    <button class="btn btn-primary">Edit Project</button>
[@endcan](https://github.com/endcan)

[@can](https://github.com/can)('delete', $project)
    <form method="POST" action="{{ route('projects.destroy', $project) }}">
        [@csrf](https://github.com/csrf)
        [@method](https://github.com/method)('DELETE')
        <button class="btn btn-danger">Delete Project</button>
    </form>
[@endcan](https://github.com/endcan)

Built-in Blade Directives

Porter includes three powerful Blade directives out of the box for seamless template integration:

@hasRoleOn - Exact Role Matching

Check if a user has a specific role on a target entity:

[@hasRoleOn](https://github.com/hasRoleOn)($user, $project, 'admin')
    <div class="admin-controls">
        <button class="btn-danger">Delete Project</button>
        <button class="btn-warning">Archive Project</button>
    </div>
[@endhasRoleOn](https://github.com/endhasRoleOn)

[@hasRoleOn](https://github.com/hasRoleOn)($user, $project, 'editor')
    <button class="btn-primary">Edit Content</button>
[@endhasRoleOn](https://github.com/endhasRoleOn)

@hasAnyRoleOn - Any Role Detection

Check if a user has any role (useful for participation checks):

[@hasAnyRoleOn](https://github.com/hasAnyRoleOn)($user, $project)
    <div class="project-member-tools">
        <a href="{{ route('projects.dashboard', $project) }}">View Dashboard</a>
        <button onclick="leaveProject()">Leave Project</button>
    </div>
[@else](https://github.com/else)
    <button onclick="requestAccess()">Request Access</button>
[@endhasAnyRoleOn](https://github.com/endhasAnyRoleOn)

@isAtLeastOn - Hierarchy-Based Authorization

NEW: Check if a user has at least the minimum required role level using Porter's role hierarchy:

🚨 CRITICAL SECURITY BEHAVIOR
[@isAtLeastOn](https://github.com/isAtLeastOn) returns false if the user has NO role at all on the target entity, not just insufficient hierarchy. This is "assignment-first, hierarchy-second" behavior.

  • ✅ User with 'editor' role + checking for 'editor' = true
  • ✅ User with 'admin' role + checking for 'manager' = true (hierarchy)
  • ❌ User with 'viewer' role + checking for 'editor' = false (insufficient level)
  • ❌ User with no role + checking for any level = false (no assignment)
[@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::manager(), $project)
    <div class="management-controls">
        <button class="btn-success">Approve Budget</button>
        <button class="btn-info">Assign Tasks</button>
        <button class="btn-secondary">View Reports</button>
    </div>
[@endisAtLeastOn](https://github.com/endisAtLeastOn)

[@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::editor(), $project)
    <div class="content-controls">
        <button class="btn-primary">Edit Content</button>
        <button class="btn-outline">Save Draft</button>
    </div>
[@endisAtLeastOn](https://github.com/endisAtLeastOn)

Why @isAtLeastOn is Game-Changing

Traditional role checking requires you to list every acceptable role:

{{-- The old way: verbose and error-prone --}}
[@if](https://github.com/if)($user->hasRoleOn($project, 'admin') || 
    $user->hasRoleOn($project, 'manager') || 
    $user->hasRoleOn($project, 'team_lead'))
    <button>Manage Team</button>
[@endif](https://github.com/endif)

With [@isAtLeastOn](https://github.com/isAtLeastOn), you define the minimum requirement once:

{{-- The Porter way: clean and maintainable --}}
[@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::manager(), $project)
    <button>Manage Team</button>
[@endisAtLeastOn](https://github.com/endisAtLeastOn)

Benefits:

  • Single Call Performance: One method call vs multiple OR conditions reduces query overhead
  • Hierarchy-Aware: Automatically includes higher-level roles (Admin ≥ Manager ≥ Editor)
  • Future-Proof: Add new high-level roles without updating templates
  • Business Logic: Expresses intent clearly - "needs at least manager-level access"
  • Type Safety: Uses RoleFactory for compile-time role validation
  • Maintainable: Single source of truth for role requirements

Real-World @isAtLeastOn Examples

Progressive Feature Access

{{-- Basic content access for all members --}}
[@hasAnyRoleOn](https://github.com/hasAnyRoleOn)($user, $project)
    <div class="project-overview">
        <h2>{{ $project->name }}</h2>
        <p>{{ $project->description }}</p>
    </div>
[@endhasAnyRoleOn](https://github.com/endhasAnyRoleOn)

{{-- Content editing for editors and above --}}
[@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::editor(), $project)
    <div class="content-actions">
        <button class="edit-btn">Edit Content</button>
        <button class="preview-btn">Preview Changes</button>
    </div>
[@endisAtLeastOn](https://github.com/endisAtLeastOn)

{{-- Management features for managers and above --}}
[@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::manager(), $project)
    <div class="management-panel">
        <h3>Team Management</h3>
        <button class="invite-btn">Invite Members</button>
        <button class="role-btn">Manage Roles</button>
    </div>
[@endisAtLeastOn](https://github.com/endisAtLeastOn)

{{-- Administrative controls for admins only --}}
[@hasRoleOn](https://github.com/hasRoleOn)($user, $project, 'admin')
    <div class="admin-panel">
        <h3>Administration</h3>
        <button class="danger-btn">Delete Project</button>
        <button class="archive-btn">Archive Project</button>
    </div>
[@endhasRoleOn](https://github.com/endhasRoleOn)

Dynamic Navigation Menus

<nav class="project-nav">
    <a href="{{ route('projects.show', $project) }}">Overview</a>
    
    [@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::editor(), $project)
        <a href="{{ route('projects.edit', $project) }}">Edit</a>
        <a href="{{ route('projects.content', $project) }}">Manage Content</a>
    [@endisAtLeastOn](https://github.com/endisAtLeastOn)
    
    [@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::manager(), $project)
        <a href="{{ route('projects.team', $project) }}">Team</a>
        <a href="{{ route('projects.reports', $project) }}">Reports</a>
    [@endisAtLeastOn](https://github.com/endisAtLeastOn)
    
    [@hasRoleOn](https://github.com/hasRoleOn)($user, $project, 'admin')
        <a href="{{ route('projects.settings', $project) }}">Settings</a>
    [@endhasRoleOn](https://github.com/endhasRoleOn)
</nav>

Budget Approval Workflows

<div class="budget-request">
    <h3>Budget Request: ${{ number_format($request->amount) }}</h3>
    
    [@if](https://github.com/if)($request->amount <= 1000)
        [@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::editor(), $project)
            <button class="approve-btn">Approve Small Budget</button>
        [@endisAtLeastOn](https://github.com/endisAtLeastOn)
    [@elseif](https://github.com/elseif)($request->amount <= 10000)
        [@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::manager(), $project)
            <button class="approve-btn">Approve Medium Budget</button>
        [@endisAtLeastOn](https://github.com/endisAtLeastOn)
    [@else](https://github.com/else)
        [@hasRoleOn](https://github.com/hasRoleOn)($user, $project, 'admin')
            <button class="approve-btn">Approve Large Budget</button>
        [@endhasRoleOn](https://github.com/endhasRoleOn)
    [@endif](https://github.com/endif)
</div>

Advanced Usage Patterns

Using with Custom Role Classes

[@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::teamLead(), $project)
    <div class="team-lead-tools">
        <!-- Custom role with specific business logic -->
    </div>
[@endisAtLeastOn](https://github.com/endisAtLeastOn)

Combining with Laravel Authorization

[@can](https://github.com/can)('update', $project)
    [@isAtLeastOn](https://github.com/isAtLeastOn)($user, RoleFactory::editor(), $project)
        <button class="save-btn">Save Changes</button>
    [@endisAtLeastOn](https://github.com/endisAtLeastOn)
[@endcan](https://github.com/endcan)

Custom Blade Directives

You can also create additional custom directives for Porter-specific checks:

// In AppServiceProvider.php
use Illuminate\Support\Facades\Blade;
use Hdaklue\Porter\Facades\Porter;

public function boot()
{
    // Custom directive for current user checks
    Blade::if('canManageProject', function ($project) {
        return auth()->check() && 
               Porter::isAtLeastOn(auth()->user(), RoleFactory::manager(), $project);
    });
}

Usage in Blade:

[@canManageProject](https://github.com/canManageProject)($project)
    <div class="project-management">
        <!-- Management interface -->
    </div>
[@endcanManageProject](https://github.com/endcanManageProject)

Middleware

Create custom middleware for role-based route protection:

class RequireRoleOnEntity
{
    public function handle(Request $request, Closure $next, string $role)
    {
        $entity = $request->route('project'); // or any entity parameter
        
        if (!$request->user()->hasRoleOn($entity, $role)) {
            abort(403, 'Insufficient role permissions');
        }
        
        return $next($request);
    }
}

Register Middleware

// In app/Http/Kernel.php
protected $middlewareAliases = [
    // ... other middleware
    'role.on.entity' => \App\Http\Middleware\RequireRoleOnEntity::class,
];

Use in Routes

// Routes that require specific roles on entities
Route::put('/projects/{project}', [ProjectController::class, 'update'])
    ->middleware('role.on.entity:admin');

Route::delete('/projects/{project}', [ProjectController::class, 'destroy'])
    ->middleware('role.on.entity:admin');

Route::post('/projects/{project}/invite', [ProjectController::class, 'invite'])
    ->middleware('role.on.entity:manager');

Gates

You can also use Laravel Gates with Porter for more complex authorization logic:

// In AuthServiceProvider.php
use Illuminate\Support\Facades\Gate;
use Hdaklue\Porter\Facades\Porter;

public function boot()
{
    Gate::define('manage-project-budget', function (User $user, Project $project, int $amount) {
        if (!Porter::hasRoleOn($user, $project, 'admin')) {
            return false;
        }
        
        $role = Porter::getRoleOn($user, $project);
        return $role && method_exists($role, 'getMaxBudgetApproval') 
            && $amount <= $role->getMaxBudgetApproval();
    });
}

Form Requests

Integrate Porter checks into Form Request validation:

class UpdateProjectRequest extends FormRequest
{
    public function authorize()
    {
        $project = $this->route('project');
        
        return Porter::isAtLeastOn($this->user(), RoleFactory::manager(), $project);
    }

    public function rules()
    {
        $project = $this->route('project');
        $rules = [
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
        ];

        // Only admins can change certain fields
        if ($this->user()->hasRoleOn($project, 'admin')) {
            $rules['budget'] = 'nullable|numeric|min:0';
            $rules['status'] = 'nullable|string|in:active,inactive,archived';
        }

        return $rules;
    }
}

Validation Rules

Porter provides two specialized validation rules for ensuring proper role assignments in your forms and API requests:

AssignedTo Rule

Validates that an assignable entity (like a user) is assigned to a roleable entity (like a project). Perfect for operations that require existing role assignments:

use Hdaklue\Porter\Rules\AssignedTo;

class RemoveUserFromProjectRequest extends FormRequest
{
    public function rules()
    {
        $project = $this->route('project');
        $userToRemove = User::find($this->input('user_id'));

        return [
            'user_id' => [
                'required',
                'exists:users,id',
                new AssignedTo($userToRemove, $project)
            ],
        ];
    }
}

NotAssignedTo Rule

Validates that an assignable entity is not assigned to a roleable entity. Ideal for preventing duplicate assignments:

use Hdaklue\Porter\Rules\NotAssignedTo;

class InviteUserToProjectRequest extends FormRequest
{
    public function rules()
    {
        $project = $this->route('project');

$userToInvite = User::find($this->input('user_id'));

        return [
            'user_id' => [
                'required',
                'exists:users,id',
                new NotAssignedTo($userToInvite, $project)
            ],
            'role' => 'required|string|in:admin,manager,editor,viewer',
        ];
    }
}

Real-World Validation Examples

Project Team Management

class ProjectTeamController extends Controller
{
    public function addMember(AddMemberRequest $request, Project $project)
    {
        $user = User::find($request->user_id);
        $role = $request->role;

        // Validation ensures user is not already assigned
        Porter::assign($user, $project, $role);

        return redirect()->back()->with('success', 'Member added successfully');
    }

    public function removeMember(RemoveMemberRequest $request, Project $project)
    {
        $user = User::find($request->user_id);

        // Validation ensures user is currently assigned
        Porter::remove($user, $project);

        return redirect()->back()->with('success', 'Member removed successfully');
    }
}

Dynamic Form Validation

class ProjectInviteRequest extends FormRequest
{
    public function rules()
    {
        $project = $this->route('project');
        $rules = [
            'email' => 'required|email',
            'role' => 'required|string|in:admin,manager,editor,viewer',
        ];

        // If user already exists, ensure they're not already assigned
        if ($this->has('user_id')) {
            $user = User::find($this->input('user_id'));
            $rules['user_id'] = [
                'required',
                'exists:users,id',
                new NotAssignedTo($user, $project)
            ];
        }

        return $rules;
    }

    public function messages()
    {
        return [
            'user_id.assigned_to' => 'This user is already a member of the project.',
        ];
    }
}

Bulk Operations Validation

class BulkAssignRolesRequest extends FormRequest
{
    public function rules()
    {
        $project = $this->route('project');

        return [
            'assignments' => 'required|array|min:1',
            'assignments.*.user_id' => [
                'required',
                'exists:users,id',
                function ($attribute, $value, $fail) use ($project) {
                    $user = User::find($value);
                    $notAssignedRule = new NotAssignedTo($user, $project);

                    $notAssignedRule->validate($attribute, $value, $fail);
                }
            ],
            'assignments.*.role' => 'required|string|in:admin,manager,editor,viewer',
        ];
    }
}

API Validation

class ApiProjectMemberController extends Controller
{
    public function store(Request $request, Project $project)
    {
        $user = User::find($request->user_id);

        $validator = Validator::make($request->all(), [
            'user_id' => [
                'required',
                'exists:users,id',
                new NotAssignedTo($user, $project)
            ],
            'role' => 'required|string|in:admin,manager,editor,viewer',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors()
            ], 422);
        }

        Porter::assign($user, $project, $request->role);

        return response()->json([
            'message' => 'User assigned successfully',
            'data' => new ProjectMemberResource($user)
        ], 201);
    }
}

Error Messages

Both validation rules provide clear error messages:

  • AssignedTo: "The :attribute is not assigned to this entity."
  • NotAssignedTo: "The :attribute is already assigned to this entity."

You can customize these messages in your form request's messages() method:

public function messages()
{
    return [
        'user_id.not_assigned_to' => 'The selected user is already a member of this project.',
        'user_id.assigned_to' => 'The selected user is not currently a member of this project.',
    ];
}

Benefits

  • Prevents Duplicate Assignments: NotAssignedTo stops users from being assigned twice
  • Validates Existing Assignments: AssignedTo ensures operations target actual members
  • Clean Error Handling: Provides clear feedback for invalid assignment states
  • Integration Ready: Works seamlessly with Laravel's validation system
  • Type Safe: Uses Porter's entity contracts for reliable validation
  • Performance Optimized: Leverages Porter's efficient role checking methods

API Resources

Include role information in API responses:

class ProjectResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'user_role' => $this->when(
                auth()->check(),
                Porter::getRoleOn(auth()->user(), $this->resource)
            ),
            'permissions' => $this->when(auth()->check(), [
                'can_edit' => Porter::isAtLeastOn(auth()->user(), RoleFactory::manager(), $this->resource),
                'can_delete' => auth()->user()->hasRoleOn($this->resource, 'admin'),
                'can_invite' => auth()->user()->hasAnyRoleOn($this->resource),
            ]),
        ];
    }
}

Event Listeners

Listen to Porter's role assignment events:

// In EventServiceProvider.php
protected $listen = [
    \Hdaklue\Porter\Events\RoleAssigned::class => [
        \App\Listeners\SendRoleAssignedNotification::class,
        \App\Listeners\LogRoleAssignment::class,
    ],
    \Hdaklue\Porter\Events\RoleChanged::class => [
        \App\Listeners\SendRoleChangedNotification::class,
    ],
    \Hdaklue\Porter\Events\RoleRemoved::class => [
        \App\Listeners\SendRoleRemovedNotification::class,
    ],
];

Example event listener:

class SendRoleAssignedNotification
{
    public function handle(RoleAssigned $event)
    {
        $user = $event->user;
        $target = $event->target;
        $role = $event->role;

        // Send notification
        $user->notify(new RoleAssignedNotification($target, $role));
    }
}

Testing

Porter integrates well with Laravel's testing helpers:

class ProjectControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_admin_can_delete_project()
    {
        $user = User::factory()->create();
        $project = Project::factory()->create();
        
        Porter::assign($user, $project, 'admin');
        
        $this->actingAs($user)
            ->delete(route('projects.destroy', $project))
            ->assertRedirect();
            
        $this->assertDatabaseMissing('projects', ['id' => $project->id]);
    }

    public function test_manager_cannot_delete_project()
    {
        $user = User::factory()->create();
        $project = Project::factory()->create();
        
        Porter::assign($user, $project, 'manager');
        
        $this->actingAs($user)
            ->delete(route('projects.destroy', $project))
            ->assertForbidden();
    }
}

Next Steps

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport
twbs/bootstrap4
php-http/client-implementation
phpcr/phpcr-implementation
cucumber/gherkin-monorepo
haydenpierce/class-finder
psr/simple-cache-implementation
uri-template/tests