Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/internachi/modular/llms.txt

Use this file to discover all available pages before exploring further.

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.