offload-project / laravel-hoist
Feature discovery and util extension for Laravel Pennant
Installs: 51
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/offload-project/laravel-hoist
Requires
- php: ^8.3
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^3.8.1
- laravel/pennant: ^1.0
- laravel/pint: ^1.26.0
- orchestra/testbench: ^9.15|^10.8
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- spatie/laravel-data: ^4.18
README
Laravel Hoist
Feature discovery and management extension for Laravel Pennant. Automatically discover, manage, and serve feature flags with custom metadata and routing.
Requirements
- PHP 8.3+
- Laravel 11+
- 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
Features
Feature Discovery
Automatically discover and manage Laravel Pennant features with custom metadata and routing information.
Create a Feature
php artisan hoist:feature NewFeature
This will create a new feature class in your configured feature directory (default: app/Features).
Feature Class Example
<?php declare(strict_types=1); namespace App\Features; use OffloadProject\Hoist\Contracts\Feature; class BillingFeature implements Feature { public string $name = 'billing'; public string $label = 'Billing Module'; public ?string $description = 'Advanced billing features'; public ?string $route = 'billing.index'; // Optional route name 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.
Using 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', ...] }
Feature Discovery Service
The FeatureDiscovery service provides several methods for working with features:
discover()
Discovers all feature classes from configured directories.
use OffloadProject\Hoist\Services\FeatureDiscovery; $discovery = app(FeatureDiscovery::class); $featureClasses = $discovery->discover(); // Returns: Collection of feature class names
all()
Returns all features as FeatureData objects without checking active status.
$features = Hoist::all(); // Returns: Collection of FeatureData objects
forModel($model)
Returns all features with their active status for a specific model (typically a User).
$userFeatures = Hoist::forModel($user); // Each FeatureData object includes 'active' property
names()
Returns an array of all feature names.
$names = Hoist::names(); // Returns: ['feature-one', 'feature-two', ...]
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 }
Integration with Laravel Pennant
This package extends Laravel Pennant by providing:
- Automatic Discovery: No need to manually register features
- 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 Pennant 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"
}
}
]
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 property. The package safely handles routes:
- If
$routeisnullor empty,hrefwill benull - If
$routespecifies a route name that doesn't exist,hrefwill benull(no exception thrown) - If
$routespecifies a valid route name,hrefwill contain the generated URL
class MyFeature implements Feature { public string $name = 'my-feature'; public string $label = 'My Feature'; public ?string $route = 'dashboard.index'; // Must be a valid route name // ... }
The Feature Interface
The package provides an optional Feature interface for better type safety:
use OffloadProject\Hoist\Contracts\Feature; class MyFeature implements Feature { public string $name = 'my-feature'; public string $label = 'My Feature'; public ?string $description = null; public ?string $route = null; public function resolve(mixed $scope): mixed { return true; } public function metadata(): array { return []; } }
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', ]; }
Testing
./vendor/bin/pest
License
The MIT License (MIT). Please see License File for more information.