offload-project / laravel-hoist
Feature discovery and util extension for Laravel Pennant
Requires
- php: ^8.3
- illuminate/support: ^11.0|^12.0|^13.2
Requires (Dev)
- captainhook/captainhook-phar: ^5.29
- larastan/larastan: ^3.8.1
- laravel/pennant: ^1.0
- laravel/pint: ^1.26.0
- orchestra/testbench: ^9.15|^10.8|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- ramsey/conventional-commits: ^1.7
- spatie/laravel-data: ^4.18
This package is auto-updated.
Last update: 2026-06-14 20:07:14 UTC
README
Feature discovery and management extension for Laravel Pennant. Automatically discover, manage, and serve feature flags with custom metadata, tags, and routing.
Features
- Automatic discovery — Drop a class into your
Featuresdirectory; it's picked up without manual registration - PHP attributes — Declarative metadata via
#[Label],#[Description],#[Route],#[Tags],#[FeatureSet] - Rich
FeatureDatapayload — Structured DTO with label, description, href, active status, tags, and metadata - Per-user resolution —
Hoist::forModel($user)returns every feature with its active status for that scope - Tag-based filtering — Filter features by single tag, ALL tags (AND), or ANY tag (OR)
- Feature sets — Group related features under a named set
- Route integration — Generate an
hreffrom a named route, safely handling missing routes - Pennant compatible — Works alongside Pennant's native
Feature::active(),@featureBlade directive, and middleware - Customizable stubs — Publish and customize the
hoist:featuregenerator template
Table of Contents
- Requirements
- Installation
- Configuration
- Quick Start
- Attributes
- Feature Discovery Service
- Feature Data Structure
- Integration with Laravel Pennant
- Use Cases
- Advanced Usage
- AI Coding Assistant Skill
- Testing
- Contributing
- Security
- License
Requirements
- PHP 8.3+
- Laravel 11/12/13
- Laravel Pennant 1+
Installation
composer require offload-project/laravel-hoist
Configuration
Publish the configuration file:
php artisan vendor:publish --tag=hoist-config
Edit config/hoist.php:
return [ 'feature_directories' => [ app_path('Features') => 'App\\Features', ], ];
The configuration uses an associative array where keys are directory paths and values are their corresponding namespaces.
Optionally, publish the stub files for customization:
php artisan vendor:publish --tag=hoist-stubs
Quick Start
1. Create a Feature
php artisan hoist:feature BillingFeature
This creates a new feature class in your configured feature directory (default: app/Features).
Features can define metadata using PHP attributes (recommended) or class properties. Attributes take precedence over properties when both are present.
<?php declare(strict_types=1); namespace App\Features; use OffloadProject\Hoist\Attributes\Description; use OffloadProject\Hoist\Attributes\FeatureSet; use OffloadProject\Hoist\Attributes\Label; use OffloadProject\Hoist\Attributes\Route; use OffloadProject\Hoist\Attributes\Tags; use OffloadProject\Hoist\Contracts\Feature; #[Label('Billing Module')] #[Description('Advanced billing features')] #[Route('billing.index')] #[Tags('subscription', 'pro')] #[FeatureSet('premium')] class BillingFeature implements Feature { public string $name = 'billing'; public function resolve(mixed $scope): mixed { return $scope->subscription?->isActive() ?? false; } public function metadata(): array { return [ 'category' => 'premium', 'icon' => 'credit-card', 'version' => '2.0', ]; } }
Note: The
Featureinterface is optional but recommended. Features are discovered based on having aresolve()method, but implementing the interface provides better IDE support and type safety.
2. Use Features
use OffloadProject\Hoist\Facades\Hoist; // Get all features $features = Hoist::all(); // Get features for a specific user with active status $userFeatures = Hoist::forModel($user); // Get array of all feature names $featureNames = Hoist::names(); // Returns: ['billing', 'dashboard', 'reporting', ...] // Access feature data foreach ($userFeatures as $feature) { echo $feature->name; // 'billing' echo $feature->label; // 'Billing Module' echo $feature->description; // 'Advanced billing features' echo $feature->href; // route('billing.index') echo $feature->active; // true/false print_r($feature->metadata); // ['category' => 'premium', ...] print_r($feature->tags); // ['subscription', 'pro'] }
3. Filter by Tags
// Get features with a specific tag $flags = Hoist::tagged('flag'); $subscriptionFeatures = Hoist::tagged('subscription'); // Get features with ALL specified tags (AND logic) $proSubscriptions = Hoist::withTags(['subscription', 'pro']); // Get features with ANY of the specified tags (OR logic) $paidFeatures = Hoist::withAnyTags(['pro', 'enterprise']); // Filter with model scope (includes active status) $features = Hoist::taggedFor('subscription', $user); $proFeatures = Hoist::withTagsFor(['subscription', 'pro'], $user); $paidFeatures = Hoist::withAnyTagsFor(['pro', 'enterprise'], $user);
Attributes
PHP attributes provide a clean, declarative way to define feature metadata directly on the class. All attributes are optional and target the class level.
| Attribute | Parameter | Description |
|---|---|---|
#[Label('...')] |
string $value |
Human-readable display name |
#[Description('...')] |
string $value |
Feature description |
#[Route('...')] |
string $value |
Named route for generating the feature's href |
#[Tags('...')] |
string ...$tags |
One or more tags for categorization |
#[FeatureSet('...')] |
string $name, ?string $label |
Group features into a named set |
When an attribute is present, it takes precedence over the equivalent class property. You can mix both approaches — for example, use attributes for static metadata and properties for values that need to be computed.
// Properties-only approach (still supported) class MyFeature implements Feature { public string $name = 'my-feature'; public string $label = 'My Feature'; public ?string $description = 'A description'; public ?string $route = 'my-feature.index'; public array $tags = ['flag']; public string $featureSet = 'core'; public function resolve(mixed $scope): mixed { return true; } }
Feature Discovery Service
The FeatureDiscovery service (accessed via the Hoist facade) provides several methods for working with features:
| Method | Returns |
|---|---|
Hoist::discover() |
Collection of discovered feature class names |
Hoist::all() |
Collection of FeatureData without active status |
Hoist::forModel($model) |
Collection of FeatureData with active status |
Hoist::names() |
Array of all feature names |
Hoist::tagged($tag) |
Features with the given tag |
Hoist::withTags([...]) |
Features with ALL given tags (AND) |
Hoist::withAnyTags([...]) |
Features with ANY of the given tags (OR) |
Hoist::taggedFor($tag, $m) |
Tagged features with active status for $m |
Hoist::withTagsFor([...], $m) |
All-tags features with active status for $m |
Hoist::withAnyTagsFor([...], $m) |
Any-tags features with active status for $m |
Feature Data Structure
The FeatureData class provides a structured way to access feature information:
class FeatureData { public string $name; // Feature identifier public string $label; // Human-readable name public ?string $description; // Feature description public ?string $href; // Generated route URL public ?bool $active; // Active status (when using forModel) public array $metadata; // Custom metadata public array $tags; // Feature tags for categorization public ?string $featureSet; // Feature set grouping }
Integration with Laravel Pennant
This package extends Laravel Pennant by providing:
- Automatic Discovery — No need to manually register features
- PHP Attributes — Declarative metadata using native PHP attributes
- Rich Metadata — Add custom metadata to features
- Route Integration — Link features to routes automatically
- Structured Data — Get features as structured data objects
- Bulk Operations — Get all features and their status in one call
Using with Pennant's Native Features
You can still use all of Laravel Pennant's native features:
use Laravel\Pennant\Feature; // Standard Pennant usage if (Feature::active('billing')) { // Feature is active } // In Blade @feature('billing') <!-- Feature content --> @endfeature // Combined with Hoist $features = Hoist::forModel($user); foreach ($features as $feature) { if ($feature->active) { // Do something with active feature } }
Use Cases
Building a Feature Dashboard
public function featureDashboard(Request $request) { $features = Hoist::forModel($request->user()); return view('features.dashboard', [ 'features' => $features, ]); }
<!-- resources/views/features/dashboard.blade.php --> <div class="features-grid"> @foreach($features as $feature) <div class="feature-card {{ $feature->active ? 'active' : 'inactive' }}"> <h3>{{ $feature->label }}</h3> <p>{{ $feature->description }}</p> @if($feature->active && $feature->href) <a href="{{ $feature->href }}" class="btn"> Go to {{ $feature->label }} </a> @endif @if(!empty($feature->metadata['icon'])) <i class="icon-{{ $feature->metadata['icon'] }}"></i> @endif </div> @endforeach </div>
API Endpoint for Frontend
Route::get('/api/features', function (Request $request) { return Hoist::forModel($request->user()); });
Returns:
[
{
"name": "billing",
"label": "Billing Module",
"description": "Advanced billing features",
"href": "https://app.example.com/billing",
"active": true,
"metadata": {
"category": "premium",
"icon": "credit-card"
},
"tags": [
"subscription",
"pro"
],
"featureSet": "premium"
}
]
Dynamic Navigation
public function navigation(Request $request) { $features = Hoist::forModel($request->user()) ->filter(fn($f) => $f->active && $f->href) ->filter(fn($f) => $f->metadata['show_in_nav'] ?? true); return view('layouts.navigation', [ 'features' => $features, ]); }
Advanced Usage
Custom Feature Directories
You can configure multiple feature directories, each mapped to its namespace:
// config/hoist.php return [ 'feature_directories' => [ app_path('Authorization/Features') => 'App\\Authorization\\Features', app_path('Billing/Features') => 'App\\Billing\\Features', app_path('Admin/Features') => 'App\\Admin\\Features', ], ];
Each directory is mapped to its corresponding namespace, allowing you to organize features across different modules or domains while maintaining proper class resolution.
Feature Organization
Organize features by category:
app/Features/
├── Admin/
│ ├── UserManagementFeature.php
│ └── SystemSettingsFeature.php
├── Premium/
│ ├── BillingFeature.php
│ └── AnalyticsFeature.php
└── Core/
├── DashboardFeature.php
└── ProfileFeature.php
Route Handling
The href property in FeatureData is generated from the feature's route value (via the #[Route] attribute or
$route property). The package safely handles routes:
- If no route is defined,
hrefwill benull - If the route name doesn't exist,
hrefwill benull(no exception thrown) - If the route name is valid,
hrefwill contain the generated URL
The Feature Interface
The package provides an optional Feature interface for better type safety. Features are discovered if they either:
- Implement the
Featureinterface, OR - Have a
resolve()method (for backward compatibility with plain Pennant features)
Metadata Best Practices
Use metadata for:
- Categorization — Group features by category
- UI Elements — Icons, colors, badges
- Permissions — Access levels, roles
- Versioning — Track feature versions
- Analytics — Track feature usage
public function metadata(): array { return [ 'category' => 'premium', 'icon' => 'credit-card', 'color' => 'blue', 'version' => '2.0', 'requires_subscription' => true, 'min_plan' => 'pro', ]; }
AI Coding Assistant Skill
This package ships a Laravel Boost skill so coding assistants (Claude Code, Cursor, etc.) follow the package's conventions when generating code. Install it in your app with:
php artisan boost:add-skill offload-project/laravel-hoist
The skill source lives at skills/SKILL.md.
Testing
composer test
Contributing
Contributions are welcome! Please see the documents below before getting started.
- Contributing Guide — setup, workflow, commit conventions, and PR process
- Code of Conduct — expectations for participation in this project
Security
- Security Policy — how to report a vulnerability privately
License
The MIT License (MIT). Please see License File for more information.