daniel-cintra/modular
A Laravel package for building modular applications: organize features into self-contained modules with their own routes, controllers, models, migrations, views, config, and service providers while keeping a clean, maintainable project structure.
daniel-cintra/modular is an opinionated scaffolding layer on top of the Laravel + Inertia + Vue + Tailwind stack. It does not replace Laravel; it gives you a generator and a directory convention for slicing an app into self-contained modules (routes, controllers, models, migrations, policies, Vue pages, tests), so most of your day-to-day work is "generate a module, fill in the slice."
Prerequisites: a Laravel app on a supported version (check composer.json constraints in the repo), PHP 8.1+, Node 18+, and a working Inertia setup if you are adding it to an existing app rather than starting fresh.
Minimal steps:
composer require daniel-cintra/modular
php artisan vendor:publish --provider="DanielCintra\\Modular\\ModularServiceProvider" # config + stubs
php artisan list | grep -i modular # discover exact command signatures
Then generate one module and open every file it produced before writing any of your own code.
Where to look first, in order:
config/modular.php (or whatever the published config is named) — module root path, base namespace, stub paths, and route/registration toggles. This file answers 80% of "why did it put that there?"stubs/ directory — this is the actual specification of what a "module" means to this package. Read the controller stub, the route stub, and the Vue page stub; they encode the conventions everything else assumes.src/Commands — the generator signatures and options. Treat php artisan help <command> as the real documentation.routes/ file, and the Vue page location.First use case: scaffold a single CRUD module end to end — route → controller returning Inertia::render(...) → Vue page under the module's page directory → migration → model — then run npm run dev, hit the route, and confirm the Inertia page resolves. If that works, the rest of the package is repetition.
Vertical slices, not layers. The productive habit with this package is one module = one feature or bounded context. Resist creating a shared "Core" module on day one; do it when the third module duplicates the same service. Keep module-internal classes internal by convention and expose only what other modules need.
Generation-first workflow. The loop that pays off:
Because the generator is stub-driven, the highest-leverage investment on day one is publishing and editing the stubs so every new module already has your team's traits: declare(strict_types=1), a FormRequest instead of inline $request->validate(), an Action class instead of fat controllers, and Pest instead of PHPUnit if that is your preference. Every module you generate afterwards inherits the convention for free.
Controller → Inertia contract. Keep controllers thin: authorize, delegate to an action/service, hand a shaped array or API Resource to Inertia::render(). Do not pass raw Eloquent collections as props — you leak column names into the frontend, blow up payload size, and couple Vue to the schema. Use lazy props (fn () => ...) for anything expensive so partial reloads stay fast.
Vue side. Use one persistent layout per module rather than re-declaring it on each page. useForm from Inertia handles the submit/error cycle; keep validation errors in the standard errors bag rather than inventing a custom envelope. If you use Ziggy, confirm the module routes are in the generated route manifest before debugging "route is not defined" in the browser.
Migrations and models. Module migrations must be registered with the migrator (usually via the module's service provider or config path list) or migrate silently skips them. Verify with php artisan migrate --pretend before assuming a bug. Keep table names globally unique across modules — namespace-ish prefixes (blog_posts, billing_invoices) save pain later.
Integration tips:
bootstrap/providers.php (Laravel 11+) or config/app.php, and composer dump-autoload after any new PSR-4 mapping — the generator may not do this for you.resolvePageComponent, so imports stay readable.php artisan test --filter or a path argument) so a broken module does not block unrelated work.JsonResource — the package does not force the Vue half.Autoload is the first-hour trap. New modules frequently fail with "class not found" because the namespace is not in composer.json's PSR-4 map or the autoloader cache is stale. Run composer dump-autoload and, if it persists, check the config's base namespace against the folder casing.
Case sensitivity. Module name Blog vs directory blog works on macOS and breaks on Linux CI. Pick PascalCase folders and be consistent; it also matters for the Inertia page path resolution.
"Page not found" in the browser. Almost always a mismatch between the string passed to Inertia::render('Blog/Index') and the actual file path relative to the glob root in app.js. The error names the component; grep the glob pattern, not the page file.
Vite HMR and new directories. Glob-based page resolution is evaluated at build/dev-server start. Adding a new module's pages directory often requires restarting npm run dev before the page resolves.
Route caching. php artisan route:cache fails on closure-based routes. Module route files are a common place to get casual with closures; convert them to controller actions before you enable caching in production.
Config caching. Call env() only inside config files, publish the package config before config:cache, and remember that a cached config makes edits to config/modular.php appear to do nothing. php artisan config:clear is the reflex fix.
Stub drift on upgrade. Published stubs win over the package's. After composer update, diff your stubs/ against the vendor copy — otherwise you silently miss new conventions or bug fixes. Never edit files inside vendor/; publish and own them.
Debugging toolkit. php artisan route:list --path=<module> to confirm registration, php artisan about to see which providers loaded, Inertia devtools + Vue devtools for prop inspection, and Laravel Telescope for the actual prop payload of a slow page.
Extension points worth knowing. The published stubs (customize generation), the config's path/namespace settings (relocate modules, e.g. app/Modules vs top-level Modules/), and the module service providers (register bindings, policies, event listeners per slice).
Maturity caveat. This is a young, single-maintainer package with a modest star count. Pin the version in composer.json, read the release notes before bumping, and treat the generator as a convenience rather than a framework — keep your modules conventional enough that deleting the package leaves a working Laravel app. If the scaffolding becomes load-bearing, fork the stubs into your repo so an upstream change cannot reshape your codebase overnight.
Testing Inertia responses is where most teams under-invest: assert with assertInertia(fn (AssertableInertia $page) => $page->component('Blog/Index')->has('posts', 10)). That single assertion catches the two most common regressions — a renamed component and a prop shape change — without a browser.
How can I help you explore Laravel packages today?