What are Plugins?
Plugins are the core mechanism that Laravel Modular uses to discover and register module resources. Each plugin:
Discovers specific types of files (routes, commands, views, etc.)
Registers those files with Laravel’s services
Runs at the appropriate time in Laravel’s lifecycle
Built-in Plugins
Laravel Modular ships with 9 plugins that handle different Laravel features:
ModulesPlugin Discovers modules via composer.json
RoutesPlugin Loads route files
ViewPlugin Registers view namespaces
ArtisanPlugin Discovers Artisan commands
BladePlugin Registers Blade components
EventsPlugin Auto-discovers event listeners
GatePlugin Maps policies to models
MigratorPlugin Registers migration paths
TranslatorPlugin Registers translation namespaces
Plugin Base Class
All plugins extend the abstract Plugin class:
abstract class Plugin
{
// Boot the plugin (called during app boot)
public static function boot ( Closure $handler , Application $app ) : void
{
static :: firstBootableAttribute () ?-> newInstance () -> boot ( static :: class , $handler , $app );
}
// Discover files/data in modules
abstract public function discover ( FinderFactory $finders ) : iterable ;
// Process discovered data
abstract public function handle ( Collection $data );
}
Required Methods
The discover() method scans modules and returns data to be cached: public function discover ( FinderFactory $finders ) : iterable
{
return $finders
-> routeFileFinder ()
-> values ()
-> map ( fn ( SplFileInfo $file ) => $file -> getRealPath ());
}
Returns an iterable (array, Collection, generator) of data.
The handle() method processes the discovered data: public function handle ( Collection $data ) : void
{
$data -> each ( fn ( string $filename ) => require $filename );
}
Receives a Collection of the cached discovery data.
Plugin Lifecycle Attributes
Plugins use PHP 8 attributes to control when they execute:
OnBoot Attribute
Runs immediately during application boot:
use InterNACHI\Modular\Plugins\Attributes\ OnBoot ;
#[ OnBoot ]
class RoutesPlugin extends Plugin
{
public static function boot ( Closure $handler , Application $app ) : void
{
if ( ! $app -> routesAreCached ()) {
$handler ( static :: class );
}
}
public function discover ( FinderFactory $finders ) : iterable
{
return $finders
-> routeFileFinder ()
-> values ()
-> map ( fn ( SplFileInfo $file ) => $file -> getRealPath ());
}
public function handle ( Collection $data ) : void
{
$data -> each ( fn ( string $filename ) => require $filename );
}
}
The default behavior (no attribute) is OnBoot. Most plugins use this pattern.
AfterResolving Attribute
Runs after a specific service is resolved from the container:
use InterNACHI\Modular\Plugins\Attributes\ AfterResolving ;
use Illuminate\View\ Factory as ViewFactory ;
#[ AfterResolving ( ViewFactory :: class , parameter : 'factory' )]
class ViewPlugin extends Plugin
{
public function __construct (
protected ViewFactory $factory ,
) {}
public function discover ( FinderFactory $finders ) : iterable
{
return $finders
-> viewDirectoryFinder ()
-> withModuleInfo ()
-> values ()
-> map ( fn ( ModuleFileInfo $dir ) => [
'namespace' => $dir -> module () -> name ,
'path' => $dir -> getRealPath (),
]);
}
public function handle ( Collection $data )
{
$data -> each ( fn ( array $d ) => $this -> factory -> addNamespace ( $d [ 'namespace' ], $d [ 'path' ]));
}
}
The parameter argument specifies how the resolved service is injected into the plugin’s constructor.
Plugin Registry
The PluginRegistry manages all registered plugins:
class PluginRegistry
{
protected array $plugins = [];
// Register plugins
public function add ( string ... $class ) : static
{
foreach ( $class as $plugin ) {
$this -> plugins [ $plugin ] ??= true ;
}
return $this ;
}
// Get a plugin instance
public function get ( string $plugin , array $parameters = []) : Plugin
{
if ( ! array_key_exists ( $plugin , $this -> plugins )) {
throw new InvalidArgumentException ( "The plugin '{ $plugin }' has not been registered." );
}
$plugin = $this -> container -> make ( $plugin , $parameters );
$this -> container -> instance ( $plugin :: class , $plugin );
return $plugin ;
}
// Get all registered plugin class names
public function all () : array
{
return array_keys ( $this -> plugins );
}
}
Registering Plugins
Plugins are registered in the service provider:
protected function registerDefaultPlugins () : void
{
$registry = $this -> app -> make ( PluginRegistry :: class );
$registry -> add (
ArtisanPlugin :: class ,
BladePlugin :: class ,
EventsPlugin :: class ,
GatePlugin :: class ,
MigratorPlugin :: class ,
ModulesPlugin :: class ,
RoutesPlugin :: class ,
TranslatorPlugin :: class ,
ViewPlugin :: class ,
);
}
Plugin Examples
Routes Plugin
Loads route files from all modules:
class RoutesPlugin extends Plugin
{
public static function boot ( Closure $handler , Application $app ) : void
{
if ( ! $app -> routesAreCached ()) {
$handler ( static :: class );
}
}
public function discover ( FinderFactory $finders ) : iterable
{
return $finders
-> routeFileFinder () // Finds *.php in */routes
-> values ()
-> map ( fn ( SplFileInfo $file ) => $file -> getRealPath ());
}
public function handle ( Collection $data ) : void
{
$data -> each ( fn ( string $filename ) => require $filename );
}
}
Artisan Plugin
Registers console commands from modules:
class ArtisanPlugin extends Plugin
{
public static function boot ( Closure $handler , Application $app ) : void
{
Artisan :: starting ( fn ( $artisan ) => $handler ( static :: class , [ 'artisan' => $artisan ]));
}
public function __construct (
protected Artisan $artisan ,
protected ModuleRegistry $registry ,
) {}
public function discover ( FinderFactory $finders ) : iterable
{
return $finders
-> commandFileFinder () // Finds *.php in */src/Console/Commands
-> withModuleInfo ()
-> values ()
-> map ( fn ( ModuleFileInfo $file ) => $file -> fullyQualifiedClassName ())
-> filter ( $this -> isInstantiableCommand ( ... ));
}
public function handle ( Collection $data ) : void
{
$data -> each ( fn ( string $fqcn ) => $this -> artisan -> resolve ( $fqcn ));
$this -> registerNamespacesInTinker ();
}
protected function isInstantiableCommand ( $command ) : bool
{
return is_subclass_of ( $command , Command :: class )
&& ! ( new ReflectionClass ( $command )) -> isAbstract ();
}
}
The Artisan plugin filters out abstract commands to prevent instantiation errors.
Gate Plugin
Automatically maps policies to models:
#[ AfterResolving ( Gate :: class , parameter : 'gate' )]
class GatePlugin extends Plugin
{
public function __construct (
protected Gate $gate
) {}
public function discover ( FinderFactory $finders ) : iterable
{
return $finders
-> modelFileFinder ()
-> withModuleInfo ()
-> values ()
-> map ( function ( ModuleFileInfo $file ) {
$fqcn = $file -> fullyQualifiedClassName ();
$namespace = rtrim ( $file -> module () -> namespaces -> first (), ' \\ ' );
$candidates = [
$namespace . ' \\ Policies \\ ' . Str :: after ( $fqcn , 'Models \\ ' ) . 'Policy' ,
$namespace . ' \\ Policies \\ ' . Str :: afterLast ( $fqcn , ' \\ ' ) . 'Policy' ,
];
foreach ( $candidates as $candidate ) {
if ( class_exists ( $candidate )) {
return [
'fqcn' => $fqcn ,
'policy' => $candidate ,
];
}
}
return null ;
})
-> filter ();
}
public function handle ( Collection $data ) : void
{
$data -> each ( fn ( array $row ) => $this -> gate -> policy ( $row [ 'fqcn' ], $row [ 'policy' ]));
}
}
Events Plugin
Auto-discovers event listeners:
#[ AfterResolving ( Dispatcher :: class , parameter : 'events' )]
class EventsPlugin extends Plugin
{
public function __construct (
protected Application $app ,
protected Dispatcher $events ,
protected Repository $config ,
) {}
public function discover ( FinderFactory $finders ) : array
{
if ( ! $this -> shouldDiscoverEvents ()) {
return [];
}
return $finders
-> listenerDirectoryFinder ()
-> withModuleInfo ()
-> reduce ( fn ( array $discovered , ModuleFileInfo $file ) => array_merge_recursive (
$discovered ,
DiscoverEvents :: within ( $file -> getPathname (), $file -> module () -> path ( 'src' ))
), []);
}
public function handle ( Collection $data ) : void
{
$data -> each ( function ( array $listeners , string $event ) {
foreach ( array_unique ( $listeners , SORT_REGULAR ) as $listener ) {
$this -> events -> listen ( $event , $listener );
}
});
}
}
Plugin Data Repository
The PluginDataRepository manages plugin discovery data and caching:
class PluginDataRepository
{
public function __construct (
protected array $data , // Cached data
protected PluginRegistry $registry , // Plugin registry
protected FinderFactory $finders , // File finders
) {}
// Get data for a specific plugin
public function get ( string $name ) : Collection
{
$this -> data [ $name ] ??= $this -> registry -> get ( $name ) -> discover ( $this -> finders );
return collect ( $this -> data [ $name ]);
}
// Get all plugin data (triggers discovery for all)
public function all () : array
{
foreach ( $this -> registry -> all () as $plugin ) {
$this -> get ( $plugin );
}
return $this -> data ;
}
}
Plugin data is lazily loaded - discovery only runs when the data is first accessed.
Creating Custom Plugins
You can create custom plugins for your own needs:
namespace App\Plugins ;
use Illuminate\Support\ Collection ;
use InterNACHI\Modular\Plugins\ Plugin ;
use InterNACHI\Modular\Support\ FinderFactory ;
class ConfigPlugin extends Plugin
{
public function discover ( FinderFactory $finders ) : iterable
{
return FinderCollection :: forFiles ()
-> name ( '*.php' )
-> inOrEmpty ( $finders -> base_path . '/*/config' )
-> values ()
-> map ( fn ( $file ) => [
'key' => pathinfo ( $file -> getFilename (), PATHINFO_FILENAME ),
'path' => $file -> getRealPath (),
]);
}
public function handle ( Collection $data ) : void
{
$data -> each ( function ( array $config ) {
config ([ $config [ 'key' ] => require $config [ 'path' ]]);
});
}
}
Registering Custom Plugins
Register in your AppServiceProvider:
use InterNACHI\Modular\ PluginRegistry ;
use App\Plugins\ ConfigPlugin ;
public function register ()
{
PluginRegistry :: register ( ConfigPlugin :: class );
}
Custom plugins follow the same lifecycle and caching behavior as built-in plugins.
Plugin Handler
The PluginHandler orchestrates plugin execution:
class PluginHandler
{
public function __construct (
protected PluginRegistry $registry ,
protected PluginDataRepository $data ,
) {}
// Boot all plugins
public function boot ( Application $app ) : void
{
foreach ( $this -> registry -> all () as $class ) {
$class :: boot ( $this -> handle ( ... ), $app );
}
}
// Execute a specific plugin
public function handle ( string $name , array $parameters = []) : mixed
{
return $this -> registry -> get ( $name , $parameters ) -> handle ( $this -> data -> get ( $name ));
}
}
Boot Attributes Interface
All boot attributes implement the HandlesBoot interface:
interface HandlesBoot
{
public function boot ( string $plugin , Closure $handler , Application $app );
}
OnBoot Implementation
#[ Attribute ( Attribute :: TARGET_CLASS )]
class OnBoot implements HandlesBoot
{
public function boot ( string $plugin , Closure $handler , Application $app )
{
$handler ( $plugin );
}
}
AfterResolving Implementation
#[ Attribute ( Attribute :: TARGET_CLASS )]
class AfterResolving implements HandlesBoot
{
public function __construct (
public string $abstract ,
public string $parameter ,
) {}
public function boot ( string $plugin , Closure $handler , Application $app )
{
$app -> afterResolving ( $this -> abstract , fn ( $resolved ) => $handler ( $plugin , [ $this -> parameter => $resolved ]));
if ( $app -> resolved ( $this -> abstract )) {
$handler ( $plugin );
}
}
}
You can create custom boot attributes by implementing the HandlesBoot interface.