<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saad</title>
    <description>The latest articles on DEV Community by Saad (@protibimbok).</description>
    <link>https://dev.to/protibimbok</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3481729%2F0b9b3f11-4b56-4c2e-bcc6-4b830e758fb0.jpg</url>
      <title>DEV Community: Saad</title>
      <link>https://dev.to/protibimbok</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/protibimbok"/>
    <language>en</language>
    <item>
      <title>Laramod: Laravel modules without the magic that still feels magical</title>
      <dc:creator>Saad</dc:creator>
      <pubDate>Sat, 19 Sep 2026 02:20:34 +0000</pubDate>
      <link>https://dev.to/protibimbok/laramod-laravel-modules-without-the-magic-that-still-feels-magical-10h</link>
      <guid>https://dev.to/protibimbok/laramod-laravel-modules-without-the-magic-that-still-feels-magical-10h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj7vex8l0us5svc3ijmuq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj7vex8l0us5svc3ijmuq.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Every Laravel application starts out tidy. Then it grows, and one day &lt;code&gt;app/Http/Controllers&lt;/code&gt; has eighty files in it, billing sits beside the blog, the blog sits beside the support desk, and the only thing the files of a feature share is a prefix in their names.&lt;/p&gt;

&lt;p&gt;Laravel sorts code by what it &lt;em&gt;is&lt;/em&gt;: controllers here, models there, migrations somewhere else. That is perfect for a small application. For a large one you want to sort by what the code is &lt;em&gt;for&lt;/em&gt;. Everything the blog needs, routes, controllers, models, migrations, views, tests, in one directory that you can read on its own, hand to a colleague, delete, or move into a package.&lt;/p&gt;

&lt;p&gt;That is all a module is. And Laravel has every part you need to build one: a service provider can load routes, views, translations and migrations from any directory. A module system is mostly an agreement about where those calls are made. So the real question about a module package is how much machinery it puts around that agreement.&lt;/p&gt;

&lt;p&gt;Here is what I wanted from one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open one file and see which modules the application has, and in which order.&lt;/li&gt;
&lt;li&gt;Open one class and see everything a module contributes.&lt;/li&gt;
&lt;li&gt;Nothing to rebuild or clear when something does not show up.&lt;/li&gt;
&lt;li&gt;A module that is a directory of ordinary Laravel code, not a small package with a &lt;code&gt;composer.json&lt;/code&gt; of its own.&lt;/li&gt;
&lt;li&gt;A module installed with Composer that works exactly like one in my own repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These expectations come from years of building products. Django, with its apps, is a great example of a modular architecture. Every time I started on a large product in anything, I ended up building something similar by hand. None of the Laravel packages I tried gave me all five points, so I wrote Laramod.&lt;/p&gt;

&lt;h2&gt;
  
  
  The existing ones
&lt;/h2&gt;

&lt;p&gt;Two packages cover most of what people use today. Both are good work that the community has leaned on for years, and Laramod takes inspiration from them. They made choices that suit many teams, but I wanted something simpler: modules that plug in with one line, and code that is as easy to use from another module as from the application itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  nwidart/laravel-modules
&lt;/h3&gt;

&lt;p&gt;It has followed the framework from Laravel 5.4 to 13. Its documentation puts the idea in one sentence: "Each module is a mini Laravel application with a predictable structure."&lt;/p&gt;

&lt;p&gt;And that is what you get. A generated module has a &lt;code&gt;module.json&lt;/code&gt;, a &lt;code&gt;composer.json&lt;/code&gt;, a &lt;code&gt;vite.config.js&lt;/code&gt; and its own service providers, a &lt;code&gt;BlogServiceProvider&lt;/code&gt; and a &lt;code&gt;RouteServiceProvider&lt;/code&gt; among them. Modules are switched on and off with &lt;code&gt;module:enable&lt;/code&gt; and &lt;code&gt;module:disable&lt;/code&gt;, the state lives in &lt;code&gt;modules_statuses.json&lt;/code&gt;. Since v11 the modules are autoloaded through the Wikimedia Composer merge plugin, which merges the &lt;code&gt;composer.json&lt;/code&gt; of every module into the application's. Files are generated with a command family of its own, &lt;code&gt;module:make-model&lt;/code&gt;, &lt;code&gt;module:make-controller&lt;/code&gt; and so on.&lt;/p&gt;

&lt;p&gt;It is mature, it is documented, and nearly every question about it has been answered somewhere already. The price is weight. A module is a lot of files before you have written a line, and the truth about a module is spread over several of them: &lt;code&gt;module.json&lt;/code&gt;, the status file, &lt;code&gt;composer.json&lt;/code&gt;, the providers. When a route is missing, there are many places to look.&lt;/p&gt;

&lt;h3&gt;
  
  
  InterNACHI/modular
&lt;/h3&gt;

&lt;p&gt;Modular is the lighter take, and I like it a lot. It does not invent a module system, it uses the two that are already there: a module is a real Composer package in &lt;code&gt;app-modules/&lt;/code&gt;, pulled in through a path repository, and it is booted by Laravel's package discovery.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;make:module&lt;/code&gt; writes the module, adds a path repository and a &lt;code&gt;require&lt;/code&gt; to your &lt;code&gt;composer.json&lt;/code&gt;, and reminds you to run &lt;code&gt;composer update&lt;/code&gt;. From then on, conventions do the work: commands, migrations, factories, policies, Blade components and event listeners are discovered on their own, and &lt;code&gt;modules:cache&lt;/code&gt; makes that discovery faster. Its best idea is that the stock generators take a &lt;code&gt;--module=&lt;/code&gt; option, &lt;code&gt;php artisan make:model Post --module=blog&lt;/code&gt;, instead of a second family of commands. I took that idea gladly.&lt;/p&gt;

&lt;p&gt;The price here is different. Every module still has its own &lt;code&gt;composer.json&lt;/code&gt;, a &lt;code&gt;src/&lt;/code&gt; directory and a service provider. A new module is a Composer operation. And what a module contributes is decided by convention, so the answer to "what does this module provide?" is its directory tree, plus a cache that may or may not be fresh.&lt;/p&gt;

&lt;p&gt;That last point is what the two have in common. Both answer the question by looking at the file system. Laramod answers it by looking at a class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laramod
&lt;/h2&gt;

&lt;p&gt;This is a whole module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogModule&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProvidesRoutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProvidesMigrations&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'blog'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Router&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/routes/web.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Database/Migrations'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is how the application knows about it, in &lt;code&gt;bootstrap/modules.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Modules\Blog\BlogModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;BlogModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole mechanism. There is no auto-discovery, no manifest, no status file, no cache and no &lt;code&gt;composer.json&lt;/code&gt; per module. The order of the list is the order the modules are wired in. To switch a module off, you remove its line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contracts instead of conventions
&lt;/h3&gt;

&lt;p&gt;A module gains a capability by implementing a contract: &lt;code&gt;ProvidesRoutes&lt;/code&gt;, &lt;code&gt;ProvidesApiRoutes&lt;/code&gt;, &lt;code&gt;ProvidesGlobalMiddlewares&lt;/code&gt;, &lt;code&gt;ProvidesMigrations&lt;/code&gt;, &lt;code&gt;ProvidesSeeders&lt;/code&gt;, &lt;code&gt;ProvidesCommands&lt;/code&gt;, &lt;code&gt;ProvidesViews&lt;/code&gt;, &lt;code&gt;ProvidesTranslations&lt;/code&gt;, &lt;code&gt;ProvidesConfig&lt;/code&gt;, &lt;code&gt;ProvidesViteEntries&lt;/code&gt;, and &lt;code&gt;Ordered&lt;/code&gt; for a module that has to come first or last.&lt;/p&gt;

&lt;p&gt;Nothing is looked up on disk. A module without &lt;code&gt;ProvidesViews&lt;/code&gt; has no views, whatever its directories contain. It sounds strict, and it is the point: the class line of a module is its table of contents, your editor can jump to every part of it, and "why is this not loaded?" has exactly one place to look.&lt;/p&gt;

&lt;p&gt;Views, translations and Blade components use the module's name as their namespace, the way packages always have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'blog::posts.index'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'blog::messages.title'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  It stays out of the way
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;composer require protibimbok/laramod&lt;/code&gt; and &lt;code&gt;php artisan laramod:init&lt;/code&gt; prepare the application: the &lt;code&gt;Modules/&lt;/code&gt; directory, one PSR-4 line in &lt;code&gt;composer.json&lt;/code&gt;, the module test suites in &lt;code&gt;phpunit.xml&lt;/code&gt;. &lt;code&gt;php artisan make:module Blog&lt;/code&gt; writes a module and lists it.&lt;/p&gt;

&lt;p&gt;After that you use the framework's own generators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Post &lt;span class="nt"&gt;-mfs&lt;/span&gt; &lt;span class="nt"&gt;--module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Blog
php artisan make:controller PostController &lt;span class="nt"&gt;--module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Blog
php artisan make:test PostTest &lt;span class="nt"&gt;--module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything a command creates along the way stays in the module, so &lt;code&gt;make:model -mfs&lt;/code&gt; keeps the migration, the factory and the seeder there too. Without the option, the commands behave exactly as they do in a stock application.&lt;/p&gt;

&lt;p&gt;The same restraint goes for the rest. Module seeders only run where you ask for them, with one &lt;code&gt;Seeder::runSeeders()&lt;/code&gt; in your own &lt;code&gt;DatabaseSeeder&lt;/code&gt;. Module routes point to controller actions, so &lt;code&gt;route:cache&lt;/code&gt; keeps working, and once the routes are cached the route methods of the modules are not called at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installed modules are the same thing
&lt;/h3&gt;

&lt;p&gt;A module from a Composer package is a class too, and it goes into the same list. The one difference is that it is read-only: &lt;code&gt;make:* --module&lt;/code&gt; refuses to write into &lt;code&gt;vendor&lt;/code&gt;, and what such a module provides is copied into the application with the stock &lt;code&gt;vendor:publish&lt;/code&gt;, under tags like &lt;code&gt;blog-views&lt;/code&gt;, &lt;code&gt;blog-config&lt;/code&gt; and &lt;code&gt;blog-migrations&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Front-end tooling
&lt;/h3&gt;

&lt;p&gt;The front end is where module systems usually stop being pleasant. The PHP side of a module is easy to move into a directory. Its JavaScript and CSS are not, because the bundler lives at the root of the project and knows nothing about modules. The usual way out is to make every module a small front-end project: its own &lt;code&gt;package.json&lt;/code&gt;, its own &lt;code&gt;vite.config.js&lt;/code&gt;, its own &lt;code&gt;npm install&lt;/code&gt;, its own build directory. It works, and it means several &lt;code&gt;node_modules&lt;/code&gt;, several builds to run in the right order, and two modules that both use Alpine or React shipping it twice.&lt;/p&gt;

&lt;p&gt;I wanted the opposite: one &lt;code&gt;package.json&lt;/code&gt;, one &lt;code&gt;vite.config&lt;/code&gt;, one &lt;code&gt;npm run dev&lt;/code&gt;, one build, and modules that still own their assets. That is a package of its own, &lt;code&gt;laramod-vite-plugin&lt;/code&gt;, and installing it is one changed line, because it wraps &lt;code&gt;laravel-vite-plugin&lt;/code&gt; and takes the same options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-import laravel from 'laravel-vite-plugin';
&lt;/span&gt;&lt;span class="gi"&gt;+import laramod from 'laramod-vite-plugin';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; export default defineConfig({
     plugins: [
&lt;span class="gd"&gt;-        laravel({
&lt;/span&gt;&lt;span class="gi"&gt;+        laramod({
&lt;/span&gt;             input: ['resources/css/app.css', 'resources/js/app.js'],
             refresh: true,
         }),
     ],
 });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Entries are declared where everything else is.&lt;/strong&gt; A module says which files it wants built, relative to itself, and loads them the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;viteEntries&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'resources/js/app.js'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ Modules::vite('blog', 'resources/js/app.js') }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Modules::vite()&lt;/code&gt; returns what &lt;code&gt;@vite&lt;/code&gt; returns, so there is the dev server with hot reloading in development and the hashed files of the manifest in production. The view does not know whether the module lives in &lt;code&gt;Modules/Blog&lt;/code&gt; or in &lt;code&gt;vendor/acme/blog&lt;/code&gt;, so it keeps working when the module moves into a package. Loading a file the module has not declared is an error and not a silent success: the dev server would happily serve it, the production build would not contain it, and that is a bug you want to meet on your own machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plugin asks, it does not guess.&lt;/strong&gt; Vite runs in Node and the modules are decided in PHP, so the plugin runs &lt;code&gt;php artisan laramod:vite&lt;/code&gt; and gets the modules, their paths and their entries as JSON. No directory is scanned, and the list in &lt;code&gt;bootstrap/modules.php&lt;/code&gt; stays the only truth, for the bundler too. For a build stage without PHP, a Node-only Docker image for example, you write that JSON to a file once and hand it to the plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Development feels like one application.&lt;/strong&gt; With &lt;code&gt;refresh: true&lt;/code&gt; the page also reloads when a module's views, translations, routes, Blade components or Livewire components change. When you add a module or change a module class, the dev server restarts by itself and asks Artisan again. And when Artisan fails, because the file is half edited, Vite logs the error and keeps the running server instead of dying on you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every module gets an alias.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;greeting&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@blog/js/greeting.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Modules/Blog/resources/js/greeting.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the front-end half of using code across modules: a component or a helper of one module is one import away from another, with no relative paths climbing out of one module and into the next. The aliases are mirrored into the &lt;code&gt;paths&lt;/code&gt; of your &lt;code&gt;tsconfig.json&lt;/code&gt; or &lt;code&gt;jsconfig.json&lt;/code&gt;, so the editor and the type checker follow them too, and only the plugin's own lines in that file are ever touched.&lt;/p&gt;

&lt;p&gt;One build has a consequence that is easy to overlook. Every module shares the same copy of every dependency. There is one React, one Alpine, one instance of whatever store you use, so a context or a registry created in one module is the same object in another. The next section is built on exactly that.&lt;/p&gt;

&lt;p&gt;It also has a cost, and it is a deliberate one: a module has no &lt;code&gt;package.json&lt;/code&gt;, so the JavaScript of an installed module can only import npm packages the application has installed itself. A packaged module has to say in its README what it needs. I find that an honest trade. The application owns its dependency tree, and nothing in &lt;code&gt;vendor&lt;/code&gt; gets to grow a second one.&lt;/p&gt;

&lt;p&gt;Installing those packages by hand is the rough edge, and I plan to make it easier: a way for a module to say which npm packages it needs, and for the application to install them in one step, without giving up the single dependency tree. How that should look is still open. If you have an opinion, or a setup this would break, you are welcome to start a discussion on the &lt;a href="https://github.com/protibimbok/laramod/discussions" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modules that build on each other
&lt;/h3&gt;

&lt;p&gt;Splitting an application into modules is the easy half. The hard half is letting them work together without tying them into a knot. Using a class of another module needs nothing at all in Laramod: all modules live under one autoload root, so &lt;code&gt;use Modules\Blog\Models\Post;&lt;/code&gt; works from anywhere, with no &lt;code&gt;require&lt;/code&gt; between packages. The interesting case is the other direction, a module that wants &lt;em&gt;other modules to plug into it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The contracts are not a closed list. They are plain interfaces, so a module can bring one of its own. Say a &lt;code&gt;Search&lt;/code&gt; module wants every other module to be able to put its models into the index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Modules\Search\Contracts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ProvidesSearchables&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;searchables&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A module that wants in implements it, next to everything else it provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogModule&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProvidesRoutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProvidesSearchables&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;searchables&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the search module asks the registry who does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modules&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;providing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProvidesSearchables&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$module&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;searchables&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole extension mechanism: an interface, and one call that returns the modules implementing it, in the order they are listed. There are no events to subscribe to, no hook names to remember and no registration step to forget. The blog does not know how the search works, the search has never heard of the blog, and your editor can still list every module that plugs in, because it is a "find implementations" away.&lt;/p&gt;

&lt;p&gt;The front end works the same way, thanks to the single build: a module imports from another through its alias, and both ends get the same instance. This is roughly what a module adding its pages to an admin looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Modules/Blog/resources/js/dashboard.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@admin/js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;registerModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;menuFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Posts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That snippet is from &lt;code&gt;laramod-admin&lt;/code&gt;, a React and shadcn/ui admin that is itself a Laramod module. It is released too, &lt;code&gt;composer require protibimbok/laramod-admin&lt;/code&gt;, and may get a post of its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guidance for AI agents
&lt;/h3&gt;

&lt;p&gt;A module carries its instructions for coding agents in an &lt;code&gt;ai-workflow&lt;/code&gt; directory of its own, and with Laravel Boost the application's agent is told to run &lt;code&gt;laramod:list --json&lt;/code&gt; and to read a module's workflow before it touches the module. There are no &lt;code&gt;AGENTS.md&lt;/code&gt; files scattered through the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a module?&lt;/strong&gt; In laravel-modules, a mini application: &lt;code&gt;module.json&lt;/code&gt;, &lt;code&gt;composer.json&lt;/code&gt;, &lt;code&gt;vite.config.js&lt;/code&gt;, service providers. In Modular, a Composer package: &lt;code&gt;composer.json&lt;/code&gt;, &lt;code&gt;src/&lt;/code&gt;, a service provider. In Laramod, a class in a directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the application know about it?&lt;/strong&gt; laravel-modules scans for modules and keeps their state in &lt;code&gt;modules_statuses.json&lt;/code&gt;. Modular uses a path repository plus package discovery, so a new module needs a &lt;code&gt;composer update&lt;/code&gt;. Laramod has a PHP array you edit by hand, and &lt;code&gt;make:module&lt;/code&gt; adds the line for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is a module wired?&lt;/strong&gt; laravel-modules: by the module's service providers. Modular: by conventions that are discovered, with an optional cache. Laramod: by the contracts the module class implements, and by nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generators.&lt;/strong&gt; laravel-modules has its own &lt;code&gt;module:make-*&lt;/code&gt; family. Modular and Laramod both extend the stock &lt;code&gt;make:*&lt;/code&gt; commands with &lt;code&gt;--module&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switching a module off.&lt;/strong&gt; laravel-modules has &lt;code&gt;module:disable&lt;/code&gt;, and it is the only one of the three where this is a feature. In Modular you remove the package, in Laramod you remove the line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Front-end assets.&lt;/strong&gt; laravel-modules gives every module its own &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;vite.config.js&lt;/code&gt;. A module is built on its own into &lt;code&gt;public/build-blog&lt;/code&gt; and loaded with &lt;code&gt;module_vite()&lt;/code&gt;, or a published &lt;code&gt;vite-module-loader.js&lt;/code&gt; collects the paths every module's config exports into the root build. Modular's README leaves assets to you. Laramod has one build and one &lt;code&gt;package.json&lt;/code&gt; for the application, fed by the entries the modules declare in PHP, with a &lt;code&gt;@module&lt;/code&gt; alias, page reloads and dev-server restarts for every module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modules from Composer.&lt;/strong&gt; In Modular every module is a Composer package already. In Laramod an installed module is listed like a local one and is read-only. In laravel-modules, modules are primarily directories of the application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework support.&lt;/strong&gt; laravel-modules reaches back to Laravel 5.4, Laramod needs Laravel 13 and PHP 8.3.&lt;/p&gt;

&lt;h3&gt;
  
  
  When not to pick Laramod
&lt;/h3&gt;

&lt;p&gt;Laramod is at v0.1. If you are on an older Laravel, if you need modules that are switched on and off at runtime, or if you want a package with ten years of answered questions behind it, take laravel-modules. If your modules are truly packages in waiting and you want Composer to be the boundary between them, Modular is a very good fit.&lt;/p&gt;

&lt;p&gt;Pick Laramod if you would rather write one line than wonder why a convention did not fire. It trades a little typing for being able to read, in two files, everything your application is made of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require protibimbok/laramod
php artisan laramod:init
php artisan make:module Blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Laramod: &lt;a href="https://github.com/protibimbok/laramod" rel="noopener noreferrer"&gt;https://github.com/protibimbok/laramod&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The Vite plugin: &lt;a href="https://github.com/protibimbok/laramod-vite-plugin" rel="noopener noreferrer"&gt;https://github.com/protibimbok/laramod-vite-plugin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The admin: &lt;a href="https://github.com/protibimbok/laramod-admin" rel="noopener noreferrer"&gt;https://github.com/protibimbok/laramod-admin&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laramod is young, and what it becomes depends on the applications people build with it. If you are interested, if you get stuck while trying it, or if your project needs something it does not do yet, share your ideas: open a &lt;a href="https://github.com/protibimbok/laramod/discussions" rel="noopener noreferrer"&gt;discussion&lt;/a&gt; or an issue on GitHub, or leave a comment here. I read all of them, and I am glad to help.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>ai</category>
      <category>php</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
