Laravel is a great framework written in PHP & at this point needs no introduction. Everyone who uses Laravel has their own way of doing things, organising their code etc. Some build the app the default way as a big monolith while some prefer to organise their app in a modular way. Now the modular approach has more than one way of doing things – you could create composer packages for each module and load them as needed in your app or you can just organise your code in different folders in your app, segregating them module wise. Its the latter approach that we’ll talk about here.
Prerequisites
There are some prerequisites that are assumed in this how-to guide.
- Laravel – v11.x (v11.9.2 is the latest as I write this)
- Laravel Breeze – Inertia.js, React, Typescript etc. was setup by it on my app
- InterNACHI Modular – a module package for Laravel
- Some level of familiarity with PHP, Laravel & React
Setup Laravel for modules & React components in those modules
Install InterNACHI Modular package for Laravel.
composer require internachi/modular
Now we need to configure the React app so that it can find components to load even from outside the main resources folder where it looks by default. By default its configured to look only in the resources/js
folder in the project root. Open the <project>/resources/js/app.tsx
file in your editor of choice. In there, createInertiaApp()
is called and an object is passed to it with different properties. The resolve
property is passed a function by default which tells it where to find the page components:
resolve: (name) => resolvePageComponent(
`./Pages/${name}.tsx`,
import.meta.glob('./Pages/**/*.tsx')
)
This looks for react components only in the `resources/js/` folder of the project root. So we will change this function to the one below:
resolve: (name) => {
const pages = import.meta.glob([
"./Pages/**/*.tsx",
"../../app-modules/*/resources/js/Pages/**/*.tsx",
]);
const regex = /([^:]+)::(.+)/;
const matches = regex.exec(name);
if (matches && matches.length > 2) {
// hyphenate the Pascal case name as is done
// for directory names by the internachi/modular package
const module = matches[1].replace(
/[A-Z]/g,
( m, offset ) => ( offset > 0 ? '-' : '' ) + m.toLowerCase()
);
const pageName = matches[2];
return pages[`../../app-modules/${module}/resources/js/Pages/${pageName}.tsx`]();
} else {
return pages[`./Pages/${name}.tsx`]();
}
},
Here we check if our component name (passed to Inertia.js by our controller) has the module namespace or not (in <module-name>::<component-path>
format). If it has then we find the component in our module’s resources folder else we fallback to our root resources folder.
Another thing to note here is that internachi/modular
package puts all modules in app-modules
folder which it creates in the project root. So the folder structure looks something like this:
app-modules/
<module-name>/
composer.json
src/
tests/
routes/
<module-name>-routes.php
resources/
database/
Now that we have configured our app.tsx
file, we need to make sure our view file does not try to override our logic. Open the <project>/resources/views/app.blade.php
view file. In the <head>
tag it passes component path to Vite as following:
@vite(['resources/js/app.tsx', "resources/js/Pages/{$page['component']}.tsx"])
We need to remove the second parameter & just pass the first one for our app.tsx
like this:
@vite(['resources/js/app.tsx'])
Next we need to make our Laravel app aware of module specific routing. One way to do this is to keep adding all module route files to Laravel’s bootstrap app file. Another way is to create a manifest file of sorts and load that in Laravel’s bootstrap and all future module route files are added to that manifest. We’ll take the latter approach. Create a blank PHP file at <project>/app-modules/module-routes-manifest.php
. Running the following command on your terminal (make sure you are in your project root) should work.
mkdir app-modules && touch app-modules/module-routes-manifest.php
Now we can register this manifest file with Laravel so that it will be loaded every time for all web requests. To do that, open <project>/routes/web.php
. Here we add a require statement which will load up our modules routes manifest like below:
require dirname(__DIR__) . '/app-modules/module-routes-manifest.php';
Now our setup is done. We can now create a module & its corresponding React components.
Create a Laravel module & its React component
We will create a module named Foobar
, add a /baz
route to it and its corresponding controller & React component.
To create a module, run the following command on your terminal.
php artisan make:module foobar
Once the module is created, run the below command to set up class auto-loading via Composer.
composer update modules/foobar
Now we will create BazController
controller for the module.
php artisan make:controller BazController --module=foobar
In the BazController
you can do Inertia::render()
to render the React component like you would normally do.
namespace Modules\Foobar\Http\Controllers;
use Illuminate\Foundation\Application;
use Inertia\Inertia;
class BazController
{
public function get() {
return Inertia::render(
'Foobar::Baz',
[
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]
);
}
}
As we set up earlier, we will use Module name as namespace for our module’s components. So here our component is Baz
but we pass Foobar::Baz
to Inertia.js telling it that Baz
component is in Foobar
module.
Now create Baz
component in <project>/app-modules/foobar/resources/js/Pages/Baz.tsx
.
import { Head } from '@inertiajs/react';
import { PageProps } from '@/types';
export default function Baz({ auth, laravelVersion, phpVersion }: PageProps<{ laravelVersion: string, phpVersion: string }>) {
return (
<>
<Head title="Baz page" />
<ul>
<li><strong>PHP version:</strong> {phpVersion}</li>
<li><strong>Laravel version:</strong> {laravelVersion}</li>
</ul>
<p>
{ auth.user ? (
<>
You are <strong>logged</strong> in!
</>
) : (
<>
You are <strong>not</strong> logged in!
</>
)}
</p>
</>
);
}
Next we need to set up a route for our controller. Open the file app-modules/foobar/routes/foobar-routes.php
and add the following:
use Illuminate\Support\Facades\Route;
use Modules\Foobar\Http\Controllers\BazController;
Route::get('/baz', [BazController::class, 'get'])->name('foobar.baz.get');
Next we need to make sure this module’s routes file is always loaded by Laravel. Open up app-modules/module-routes-manifest.php
that we created earlier & add the following code to it:
require __DIR__ . '/foobar/routes/foobar-routes.php';
There is a reason for using require
here and not require_once
. It seems that Laravel loads this file more than once. If you use require_once
here then the app vars, which are automatically injected into React components, will not be available for use.
Now all that is left is to compile typescript. On your terminal navigate to your project root & run the following:
npm run build
Once Vite finishes compiling the app, navigate to https://<project-domain>/baz
in your browser. You should see the Baz
component you created being rendered displaying your PHP & Laravel versions along with your current logged in status.
Parting words
After this whenever you create new modules in this project, just follow the module creation part in this guide. internachi/modular
package adds & integrates with a number of Laravel console commands so that resource creation is straightforward. Only manual part is to add resources
folder to your module and load up its route file in the manifest file at app-modules/module-routes-manifest.php
.
internachi/modular
is a very nice package with good integration into Laravel & its console commands to make it easier to create controllers, resources, database migrations etc. just as you would normally do in Laravel. Hopefully we will see Inertia.js integration with it sometime in future. 🙂
Wow what a great article! Yeah I haven’t found a guide to setup Inertia with a modular web apps before. Having a Vue.js version would be great!
The setup is same for Vue as well in this case. Just change the file extensions from
.tsx
to.vue
if you are using that extension or whatever else you are using.Still missing npm dependency management for each module
In this case dependencies are for the app & not for individual modules. This modular approach is to just organize features/code in a more sane way instead of clumping it all together. The modules are not autonomous.
However if you want then you can do dependencies for each module separately, just create
composer.json
&package.json
as required in a module’s top level folder.