Modules
Modules are an optional, additive way to group a feature into a self-contained folder instead of spreading it across app/Controllers, app/Repository, app/Providers, etc. They're for in-house plugins that grow large enough that a flat app/ structure stops telling you which files belong to which feature (e.g. CRM + Reporting + Booking in one plugin).
If you don't create an app/Modules/ folder, none of this applies — the framework boots exactly as it did before modules existed.
Directory shape
A module is a sibling of app/Controllers, app/Repository, etc. — not a replacement for them:
app/
├── Controllers/ # unaffected, keeps working as-is
├── Repository/ # unaffected, keeps working as-is
├── Providers/ # unaffected, keeps working as-is
└── Modules/
└── Crm/
├── Controllers/
├── Repository/
├── Listeners/
├── Providers/
│ └── CrmServiceProvider.php
└── routes/
└── routes.phpCreating a module
php nikogin make:module Crm// app/Modules/Crm/Providers/CrmServiceProvider.php
namespace Nikogin\Modules\Crm\Providers;
use Nikogin\Framework\Abstracts\ServiceProvider;
use Nikogin\Framework\Attributes\AsModule;
#[AsModule(name: 'crm')]
class CrmServiceProvider extends ServiceProvider
{
public function priority(): int
{
return 10;
}
protected array $services = [
ContactRepository::class,
];
public function boot(): void
{
}
}#[AsModule] is what tells ModuleManager this ServiceProvider belongs to a module and gives it a name (used for the enable/disable lookup below):
#[Attribute(Attribute::TARGET_CLASS)]
class AsModule
{
public function __construct(
public string $name,
public bool $enabled = true,
) {}
}A Providers/*ServiceProvider.php file with no #[AsModule] attribute is silently skipped — it's never registered, booted, or treated as an error.
Auto-discovery
ModuleManager scans app/Modules/*/Providers/*ServiceProvider.php — same convention ProviderManager uses for app/Providers/*.php. Nothing needs to be registered by hand. It runs from app/Bootstraps/Loader.php, immediately after the regular ProviderManager:
(new ProviderManager())->register();
(new ModuleManager())->register();
(new ListenerManager())->register();If app/Modules/ doesn't exist, ModuleManager no-ops — no errors, no warnings.
register() and boot()
Both are called on an enabled module's ServiceProvider, in order:
register()— inherited fromServiceProvider, binds$servicesinto theContainer(same as any other provider)boot()— new lifecycle hook, empty by default, meant for setup that needs services already bound
boot() is defined on the base ServiceProvider class as a no-op, so it's opt-in and doesn't affect existing non-module providers.
Routes
A module's routes/routes.php is loaded the same way the root routes/ folder is — via View::loadDir() on rest_api_init — from app/Bootstraps/Router.php:
public static function run(): void
{
View::loadDir(NIKOGIN_DIR . 'routes');
foreach ((new ModuleManager())->enabledRouteDirs() as $routesDir) {
View::loadDir($routesDir);
}
}Only enabled modules' route files are loaded. Write routes exactly like you would in routes/api.php — see Routing.
Enabling / disabling a module
Add an entry to config/modules.php, keyed by the module's #[AsModule] name (not the folder name, though make:module sets them to match by default):
// config/modules.php
return [
'crm' => false, // uncomment to disable a module without deleting it
];Resolution order:
- An explicit key in
config/modules.phpwins, whatever its value - Otherwise, the
enabledvalue on#[AsModule]is used (defaults totrue)
A disabled module's ServiceProvider is never registered or booted, and its routes/routes.php is never loaded.
What modules don't do
- They don't change how
app/Controllers,app/Repository,app/Managers,app/Providerswork — those are untouched. - There's no wp-admin UI for toggling modules —
config/modules.phponly. make:modulenever overwrites an existing module folder — it fails with a clear error instead.