Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Modularize Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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):

  • A Laravel version compatible with the console component. Release 1.1.1 added ^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\Modularize
  • InterNACHI\Modularize\Support\ModularizeGeneratorCommand

There 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.

Implementation Patterns

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.

Gotchas and Tips

  • Don't ship it to end apps. This is a dependency of packages that provide commands, not of applications. Apps should depend on 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.
  • Null is the default and the most common crash. --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.
  • The generator trait does not set the namespace. 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.
  • Path overrides need explicit handling too. Migrations, seeders, jobs and factories don't match generator defaults. Override getPath() alongside getDefaultNamespace().
  • Read the README's word "automatic" loosely. Placement is automatic only once you've wired the namespace/path hooks. Budget per-command edits when adopting this across a package, not a one-line flip.
  • CLI-only. Nothing happens at request time. It won't help with dynamic module loading — use internachi/modular's resolution APIs for that.
  • Debugging: dump the config. When a generated file lands somewhere unexpected, 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.
  • Pin the version and keep usage thin. Two stars, zero dependents, single-organisation ownership. The code is small and the API narrow, which limits blast radius — but pin ^1.1 and confine the trait to base classes so a future ModuleConfig change is a one-file shim.
  • Laravel/Symfony version awareness. 1.1.1 added ^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.
  • The extension point is the trait, not the config object. Override 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.
  • Non-generator commands have no filesystem awareness. 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style