# internachi/modular: Laravel modules as Composer path repositories

> internachi/modular is a module system for Laravel that leans on Composer path repositories for autoloading and Laravel package discovery for initialisation, then adds only the tooling the framework does not already provide. It suits teams with a large application whose directories no longer reflect its domains, and it deliberately keeps the modules as ordinary Laravel packages.

**InterNACHI/modular** — Modularize your Laravel application

- Repository: https://github.com/InterNACHI/modular
- Stars: 1,212 · Forks: 108
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/internachi-modular

## A module is a package that lives in the repository

The fundamental idea is stated early: you can create modules in a separate `app-modules/` directory, which allows you to better organize large projects.

What makes that work without inventing a new framework is the choice of mechanism. The package uses Composer path repositories for autoloading and Laravel package discovery for module initialization, and then provides minimal tooling to fill in any gaps.

So a module is a Composer package, with its own `composer.json`, that lives inside your application repository rather than in `vendor/`. The scaffolding command shows the shape:

```
app-modules/
  my-module/
    composer.json
    src/
    tests/
    routes/
    resources/
    database/
```

Those directories are the standard Laravel package layout, which is the point. The module uses the existing Laravel package system and follows existing Laravel conventions, and the project is described as being as much a set of conventions as it is a package.

The autoloading mechanism is the part that makes the arrangement cheap. A Composer path repository tells Composer that a package lives at a given directory on disk, and the module then gets installed from there into `vendor/` as a symlink, with the source of truth staying in `app-modules/`. So your module code is versioned with your application, edited in place, and still autoloaded as a dependency.

That is a materially different arrangement from a package pulled from Packagist, and it is what lets the modules be extracted into separate packages later rather than being a dead end.

## The install is a require, a config publish and a make command

Installation is short, and the first step does most of the work.

```shell script
composer require internachi/modular
```

Laravel will auto-discover the package and everything will be automatically set up for you.

The second step is optional but recommended. While not required, it is highly recommended that you customize your default namespace for modules. By default this is set to `Modules\`, which works just fine but makes it harder to extract your module to a separate package should you ever choose to. The recommendation is to configure an organization namespace, with the example given being `InterNACHI`. Publishing the config takes:

```shell script
php artisan vendor:publish --tag=modular-config
```

The reason the namespace matters is worth spelling out. A `Modules\` namespace works just fine, but it makes it harder to extract your module to a separate package later. Change the namespace to your organisation, and each module is unambiguously yours, which is the difference between a module you can move and one that is welded to the app.

Creating a module is then one command:

```shell script
php artisan make:module my-module
```

And the last piece is a Composer update, because the module has just been added as a dependency:

```shell script
composer update modules/my-module
```

The mechanism behind that update is described in the documentation. The scaffold adds two new entries to your app's `composer.json` file. The first registers `./app-modules/my-module/` as a path repository, and the second requires `modules/my-module:*`, like any other Composer dependency. The `:*` constraint is what makes it work, since the module is in your repository and has no published version to compare against.

## Auto-discovery does the wiring, so the package stays small

The value of building on the framework's own mechanisms shows up in the usage section, where the list of what works without configuration is the list of things a module system usually has to reinvent.

Commands are auto-registered with Artisan. Migrations will be run by the Migrator. Factories are auto-loaded for `factory()`. Policies are auto-discovered for your Models. Blade components will be auto-discovered. Event listeners will be auto-discovered.

Every one of those is a mechanism Laravel already has, and none of them needed a module specific implementation. A module that had to provide its own migration runner, its own command registrar and its own policy discovery would be a parallel framework with all the maintenance that implies. This one is a set of conventions plus a scaffolder.

The optional config synchronization command is the other piece of tooling:

```shell script
php artisan modules:sync
```

It adds a `Modules` test suite to your `phpunit.xml` file, if one exists, and updates the PhpStorm Laravel plugin configuration, if it exists, to properly find your module's views. It is safe to run this command at any time, as it will only add missing configurations, and the documentation suggests you may even want to add it to your `post-autoload-dump` scripts in your application's `composer.json` file.

The idempotence is the point. A command that can be run at any time and only adds what is missing can go in a post-autoload-dump hook, which means it runs on every install, which means a new contributor who has just cloned the repository has a working IDE and a working test suite without reading a setup document. That is a small thing that removes a whole category of first-day friction.

The package also ships a cache for discovery. `php artisan modules:cache` caches the loaded modules for slightly faster auto-discovery, and `php artisan modules:clear` clears the module cache, with `php artisan modules:list` to list all modules.

## The make commands gain a module option, and nothing else changes

The most substantial convenience is the `--module` option added to most Laravel `make:` commands.

The commands themselves are exactly the same, which means you can use your custom stubs and everything else Laravel provides. The examples in the documentation run from `make:cast` through `make:controller`, `make:command`, `make:component`, `make:channel`, `make:event`, `make:exception`, `make:factory`, `make:job`, `make:listener`, `make:mail`, `make:middleware`, `make:model`, `make:notification`, `make:observer`, `make:policy`, `make:provider`, `make:request`, `make:resource`, `make:rule`, `make:seeder` and `make:test`, each with the same shape:

```shell script
php artisan make:controller MyModuleController --module=[module name]
```

This matters more than it looks, because of what it does not do. Because the command is the same command, the stub is the same stub, and your own customised stubs still apply. A module system that reimplemented `make:controller` with its own template engine would break every team that has invested in stub customisation, and would drift from Laravel's own changes to the generated code. Adding an option to the existing command avoids that entirely.

The `db:seed` command gained the same option. If you pass the `--module` option to `db:seed`, it will look for your seeder within your module namespace.

There is one deliberate asymmetry in the command set. `make:module` is the package's own command, and the other four are `modules:cache`, `modules:clear`, `modules:sync` and `modules:list`. The namespace prefix for the package's own commands is `modules:`, which keeps them separate from Laravel's own `make:` family and makes it obvious at a glance which commands come from the package.

## What the package does not do, and the comparison you should read

The limits here are the limits of the approach, and they are honest ones.

The first is that a path repository module is not an independently versioned package. It lives in your repository, it is required with a `:*` constraint, and it has no version of its own to release. The documentation makes the intent clear by pointing out that the default namespace makes it harder to extract your module to a separate package should you ever choose to. Extraction is possible, and the namespace configuration exists to make it easier, but the path repository arrangement is built for modules that ship with the application.

The second is that the modules share the application's Composer dependency graph. Each module's `composer.json` can require its own dependencies, but they resolve together with the application's, and a version conflict between a module and the app is a conflict in one graph rather than in two isolated ones. That is the cost of a single vendor directory.

The third is that this is Laravel specific and version coupled. The documentation links to the Laravel package discovery documentation for version 11, and the package is built on the framework's conventions rather than on an abstraction above them. When Laravel changes how discovery works, this package has to follow, and it has to follow quickly, because the whole design depends on that mechanism behaving as documented.

The README has a dedicated section comparing this approach to another Laravel module package, `nwidart/laravel-modules`, and that comparison is the single most decision-relevant document in the repository for anyone choosing between them. The two packages take different routes to the same problem, and the differences in how modules are installed, versioned and discovered are what you are actually choosing between.

The alternative to any package at all is the domain directory structure that Laravel applications used before modules existed, with `app/Domain/Users/Models` and similar. That approach requires no package, no path repositories and no extra Composer configuration, and it is the right answer for a codebase that has not yet outgrown `app/`. The module system earns its complexity when the application has many domains and the `app/` tree has become the thing people complain about.

On maintenance, the last push was on 2026-05-21, the repository is not archived. The version history shows 3.0.0, 3.0.1 and 3.0.2 in February and March 2026, and the repository carries a `CHANGES-V3.md` file describing the major version's changes alongside the regular changelog. The licence is MIT.

## Conclusion

internachi/modular fits a Laravel codebase that has outgrown its directory structure, because a module is an ordinary Laravel package with its own composer.json, so the framework's own discovery mechanisms do the work and the package adds only the scaffolding and the `--module` option on the make commands. Skip it if your application is small, since the `app/` directory is a fine place for code that is still growing, and skip it if you want modules to be genuinely independent packages with their own release cycle, since the path repository arrangement is designed for in-repo modules first. Before you install, read the comparison section in the project documentation against the other Laravel module package, since the trade-off between the two approaches is the decision you are actually making. The last push was on 2026-05-21 and the current release is 3.0.2 from 2026-03-23.

## FAQ

### How do I install internachi/modular in a Laravel application?

Run `composer require internachi/modular`. Laravel will auto-discover the package and everything will be set up for you, after which you can create a module with `php artisan make:module my-module`.

### Why should I change the default Modules namespace?

The default `Modules\` namespace works but makes it harder to extract your module to a separate package later. The documentation recommends publishing the config with `php artisan vendor:publish --tag=modular-config` and setting an organization namespace.

### How do modules get autoloaded in internachi/modular?

Through Composer path repositories. The scaffold registers `./app-modules/my-module/` as a path repository in your app's composer.json and requires `modules/my-module:*`, so the module is installed as a dependency from disk.

### What does the modules:sync command do?

It adds a `Modules` test suite to your `phpunit.xml` if one exists and updates the PhpStorm Laravel plugin configuration if it exists, so your module views are found. It only adds missing configurations, so it is safe to run at any time.

### Can I use the existing Laravel make commands with modules?

Yes. A `--module=` option is added to most Laravel `make:` commands and to `db:seed`, and the commands themselves are unchanged, so your custom stubs still apply. The package also provides `make:module`, `modules:cache`, `modules:clear`, `modules:sync` and `modules:list`.

## Sources

- [InterNACHI/modular on GitHub](https://github.com/InterNACHI/modular)
- [Issues](https://github.com/InterNACHI/modular/issues)
- [License: MIT](https://github.com/InterNACHI/modular/blob/main/LICENSE)
- [README](https://github.com/InterNACHI/modular/blob/main/README.md)
- [Releases](https://github.com/InterNACHI/modular/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/internachi-modular
