internachi/modularize
Traits for Laravel package authors to add internachi/modular support to their artisan commands. The Modularize trait resolves a --module option on any command, while ModularizeGeneratorCommand routes generated files into the module directory with correct namespacing.
Understand the scope before installing. internachi/modularize is a package-author utility, not an application feature. It ships two traits that add a --module=<Name> option to Laravel console commands so a command can detect it was invoked against a module and adapt. If you are building ordinary app code inside a project that already uses internachi/modular, you do not need this package. You need it when you maintain a package whose Artisan commands should become module-aware for your consumers.
Prerequisites (both required):
^13 support, so check your constraint if you are on an older or newer major.internachi/modular installed and configured in the host app. Without it, the traits resolve nothing and $this->module() stays null.Install inside your package:
composer require internachi/modularize
Where to look first. The entire API surface is two classes:
InterNACHI\Modularize\Support\ModularizeInterNACHI\Modularize\Support\ModularizeGeneratorCommandThere is no service provider, no config file, no publish step, no container bindings. Read the short README, then read those two trait files — that is the whole package.
First use case — a normal command that should optionally target a module:
use Illuminate\Console\Command;
use InterNACHI\Modularize\Support\Modularize;
class SyncFeeds extends Command
{
use Modularize;
protected $signature = 'my-package:sync-feeds';
public function handle()
{
$module = $this->module(); // ModuleConfig|null
$path = $module
? $module->basePath . '/resources/feeds'
: config('my-package.feeds_path');
// ... work against $path
}
}
Running php artisan my-package:sync-feeds --module=Blog resolves Blog into a ModuleConfig for you. Running it without the flag must behave exactly as it did before you added the trait — that compatibility contract is the one thing to protect when retrofitting a shipped command.
First use case — a file generator:
use Illuminate\Console\GeneratorCommand;
use InterNACHI\Modularize\Support\ModularizeGeneratorCommand;
class MakeWidget extends GeneratorCommand
{
use ModularizeGeneratorCommand;
protected $name = 'make:widget';
protected $type = 'Widget';
}
php artisan make:widget Foo --module=Blog then writes into the module directory. Note that the trait adds the option and placement plumbing, but it does not set the namespace for you — see the next section.
1. Nullable-first branching. Treat $this->module() as ModuleConfig|null and keep the non-module path byte-identical to today's behavior. Every command you retrofit should read as "same command, plus an optional scope." This is the core payoff: consumers don't have to choose between "package command" and "module command."
2. Generator override points are yours to wire. ModularizeGeneratorCommand gives you the flag and file placement, but Laravel's GeneratorCommand still asks you for the namespace and path. The standard pattern:
protected function getDefaultNamespace($rootNamespace)
{
return $this->module()
? $this->module()->getNamespace('Http/Controllers')
: parent::getDefaultNamespace($rootNamespace);
}
Do the same for getPath() when the artifact doesn't live in the generator's default folder — migrations, seeders, jobs, factories, and anything with a dated or nested path. Overriding only the namespace yields files that autoload correctly but land in the wrong place (or compile with App\... imports inside a module — technically valid, conceptually broken).
3. Base command + base generator classes. If you own multiple packages (or many commands in one), standardize once:
abstract class BaseCommand extends \Illuminate\Console\Command
{
use Modularize;
}
abstract class BaseGeneratorCommand extends \Illuminate\Console\GeneratorCommand
{
use ModularizeGeneratorCommand;
protected function getDefaultNamespace($rootNamespace)
{
return $this->module()
? $this->module()->getNamespace($this->namespaceSuffix())
: parent::getDefaultNamespace($rootNamespace);
}
abstract protected function namespaceSuffix(): string;
}
Every concrete command then inherits --module with no per-class edits. This is what makes the package worth installing: one decision, then consistency.
4. A coherent scaffolding workflow. The intended day-to-day loop:
php artisan make:controller WidgetController --module=Blog
php artisan make:job RebuildWidgetIndex --module=Blog
php artisan my-package:sync-feeds --module=Blog
Generators land in modules/Blog/... with Modules\Blog\... namespacing, and your own commands receive the same ModuleConfig so they can target the same module's data. Cross-package consistency ("any module-aware command accepts --module=<Name>") is the user-facing payoff — protect it.
5. Treat ModuleConfig as a read-only value object. It exposes name, basePath, namespaces, and helper methods such as getNamespace(...). Read from it; don't mutate it. When you need different behavior, override the trait method on your command rather than wrapping or subclassing ModuleConfig.
6. Testing without real modules. Don't register modules in unit tests. Partial-mock and return a fixture config:
$cmd = Mockery::mock(SyncFeeds::class)->makePartial();
$cmd->shouldReceive('module')->andReturn($fakeModuleConfig);
Keep a small ModuleConfig fixture (name, base path, namespace helper) so you can assert exact output paths and namespaces for generators without touching the filesystem or booting internachi/modular.
7. Integration with existing package tests. Add a matrix row that runs your command with --module and without it. The without-case is the regression guard that matters most when you bolt the trait onto a shipped command.
internachi/modular; modularize belongs in your package's require, not a typical app composer.json.internachi/modular is the engine. Installing modularize alone does nothing — no module registration, no discovery, no config. If $this->module() returns null when you did pass --module, the problem is almost always upstream module registration, not this package.--module is optional. Guard $this->module() at the top of handle() rather than sprinkling null checks; retrofitting an existing command and forgetting this is the most frequent failure mode.ModularizeGeneratorCommand handles the option and file placement; getDefaultNamespace() keeps returning the app namespace unless you override it. Generated classes then compile but carry App\... imports inside a module — wrong, and painful to clean up later.getPath() alongside getDefaultNamespace().internachi/modular's resolution APIs for that.dd($this->module()) inside the command. name, basePath and the namespace helpers usually explain the surprise immediately; the root cause is often module registration config, not the trait.^1.1 and confine the trait to base classes so a future ModuleConfig change is a one-file shim.^13 support (Symfony 7 console changes). Since this package touches only the console component, that's exactly where upgrade breakage will show up first — run your command test suite immediately after a Laravel bump.getDefaultNamespace, path resolution, or your own helper methods on the command. Wrapping module() in another layer of indirection just adds seams a future maintainer has to trace.Modularize only hands you a ModuleConfig; writing files, resolving migration paths, and running module-scoped work is on you. The moment your task becomes "produce a file inside a module," switch to ModularizeGeneratorCommand instead of hand-rolling paths.How can I help you explore Laravel packages today?