Alright, buckle up folks, because this is the tragic tale of how I thought I was fixing Laravel Modules… only to realize I'd have to rewrite Laravel itself. 🙃
Did I succeed? Nope.
Did I at least break something? Oh, absolutely.
Did I still release a package out of it? You bet. 😎
The Beginning: A New Project 🌟
So here I am, about to start a new project at my company—kind of a big one. And since I’m the most experienced with Laravel (not the boss dev lol, just the Laravel wizard 🧙♂️), I had the honor of setting up the project structure.
Now, you might ask, "Why Laravel if the team isn't fully experienced?" Well, actually, they know it—but let's say I'm a bit ahead of them. Plus, honestly, Laravel is just amazing for quick, flexible, easy development. (If you're still thinking, "eeeh PHP nvm," no—get your ass on the computer, and run:
composer create-project laravel/laravel
Try it. Try ittttt. You'll thank me later. 😏
Anyway, back to the story. So I start preparing our setup and, of course, Laravel Modules comes to mind. Perfect for modular architecture, right?
Only... I hadn’t used it in a while. And I totally forgot about the artisan issue. 😭
The Artisan Problem: Oh No, Here We Go Again
First things first: I open my console and run:
laravelfs new alpha
(Alpha is the fake project name; I can’t share the real one, so let’s just pretend it’s some next-gen AI startup or whatever.)
(Oh, and by the way, laravelfs
is my own Laravel installer (LaravelFS)! 🚀 I open-sourced it because Laravel 12 removed Breeze, and I was like, *"Oh hell no!" So I made my own installer to fix that and add some cool features. You should check it out! [LaravelFS]) and the story behind :
I picked "none starter," meaning just pure Blade. Why not Inertia? Well, for this scale, Blade feels safer. No big risks, just flexibility.
So, with the project ready, I opened Google and searched "Laravel Modules" (yep, I forgot the vendor name, haha). Nice! The website updated since last time I used it (it was v6, now v12—mostly compatibility changes, but still cool). I quickly typed the usual:
composer require nwidart/laravel-modules
composer dump-autoload
Everything’s great! 🎉
Time to create our first module:
php artisan module:make IOT
Boom, module created! So easy, right? Next step: creating a model called Device. Without thinking, I went:
php artisan make:model Device
❌ ERROR? No. But... wrong path.
Instead of going into Modules/IOT/Models/Device.php
, it gets created in App\Models\Device.php
.
Wait... hold on, oh damn! 🤦♂️ It hit me immediately. This command places the model in \App\Models, not the module I created. How did I forget this?! Right, Artisan doesn’t care about Laravel Modules.*
The Annoyance: Long Commands & No Third-Party Support
The right command was:
php artisan module:make-model IOT Device
So yeah, instead of just using Laravel’s built-in make:model
, I now have to specify the module name every single time.
Which is already annoying, but wait, there’s more!
👉 Not all Artisan commands have a "module" version.
👉 Third-party package commands won’t work inside modules.
👉 Spatie packages? Say goodbye to easy module integration.
At this moment, my genius brain (remember that ts-runtime-picker story? Yeah, genius mode activated again 😅) woke up and said, "Hichem, why can't you fix this? No one ever thought of it before, right?"
(spoiler: no, you won’t, Hichem. 😂).
(Hichem is my name btw, no shit sharlok :D).
The Overconfident Attempt: Let’s Hack Artisan! 😎
So here's the deal: the real issue is Artisan commands run only from Laravel's root, So I had this brilliant idea:
"What if I make Artisan work everywhere, dynamically detecting the module?"
💡 Genius, right?! (Totally never been done before, right? Haha. Ha.)
I didn’t even test the theory—I just dove straight in and started coding.
🚀 Introducing… Artisan Everywhere! 🚀 (Yes, another package. Because why not? 😂)
Opened my console, typed composer init
and created the pacakge (hichemtab-tech/artisan-everywhere—epic naming skills, I know 😎).
Opened a new PhpStorm window, created a bin
folder, and inside it, an artisan.php
. At first, I tried to make a batch file (don't ask why, haha), but PHP made more sense for Composer.
What my script did was genius:
- It caught any "artisan" command you typed.
- Then, it searched upward through your directories, found the root artisan file, and executed commands from anywhere inside the project—no more annoying
cd ../../../..
. Genius, right?
Here’s the script I wrote:
#!/usr/bin/env php
<?php
/**
* Global Artisan Wrapper - Artisan Everywhere
*
* This script searches upward from the current working directory
* for a local "artisan" file. When found, it delegates all command-line
* arguments to that artisan file.
*/
// Get the current working directory.
$cwd = getcwd();
$foundArtisan = false;
$artisanPath = '';
// Traverse up the directory hierarchy looking for a local artisan file.
while (true) {
$possibleArtisan = $cwd . DIRECTORY_SEPARATOR . 'artisan';
if (file_exists($possibleArtisan) && is_file($possibleArtisan)) {
$foundArtisan = true;
$artisanPath = $possibleArtisan;
break;
}
// If we've reached the root directory, stop.
$parent = dirname($cwd);
if ($parent === $cwd) {
break;
}
$cwd = $parent;
}
if (!$foundArtisan) {
fwrite(STDERR, "No local artisan file found in the directory hierarchy.\n");
exit(1);
}
// Capture command-line arguments (excluding the script name).
$arguments = array_slice($argv, 1);
// Escape each argument to safely pass them to the shell.
$escapedArguments = array_map('escapeshellarg', $arguments);
$argumentsString = implode(' ', $escapedArguments);
// Prepare the command.
// If the artisan file is executable, run it directly; otherwise, use PHP.
if (is_executable($artisanPath)) {
$command = escapeshellcmd($artisanPath) . ' ' . $argumentsString;
} else {
$command = 'php ' . escapeshellarg($artisanPath) . ' ' . $argumentsString;
}
// Optionally, you can enable a debug output to see the final command.
// fwrite(STDERR, "Executing command: $command\n");
// Execute the command, relaying its output and exit status.
passthru($command, $returnVar);
exit($returnVar);
🥳 🎉 IT WORKED!
Now I could run Artisan commands from anywhere in my project. No more cd ../../../
nonsense!
The “Oh No” Moment: Laravel’s Internal Paths :))))
Super hyped, I went to my project and ran:
cd Modules/IOT
artisan make:model Device
And...
INFO Model [C:\xampp\htdocs\alpha\app\Models\Device.php] created successfully.
Wait...
WHAT?!! 😳
Why did it go into app/Models/
instead of Modules/IOT/Models/
?!
And then it hit me.
💀 Laravel doesn’t let you change the base path for Artisan commands. 💀
Turns out, when you run make:model
, Laravel internally hardcodes the app/
path. It does not dynamically detect modules.
Here’s the problem code inside Illuminate\Console\GeneratorCommand
:
protected function getPath($name)
{
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'.php';
}
So even though I could now run artisan
commands from anywhere…
🥲 Artisan would still default to app/Models/
.
The Conclusion: I Gave Up. 😂
I had two choices:
- Rewrite a ton of Laravel’s internal command logic.
- Accept defeat and keep using
php artisan module:make-model
.
😂 I took Option 2.
No way Taylor Otwell is accepting a PR that modifies the entire Artisan command structure.
So yeah, I failed. BUT! At least I made Artisan Everywhere, which is still useful. 🎉
And hey, if anyone actually solves this issue, please let me know. I’d love to see it work. But for now… I’ll just keep manually moving files. 😭
That’s it. My failed attempt at rewriting Laravel. Hope you enjoyed my suffering. 😂
Top comments (1)
It was a very entertaining read!
I' m also in the situation I'm not liking the file structure that comes out of the box with frameworks. And like you I'm thinking about a way to make it possible make file creation possible however the directories are structured.
I'm taking the long way around by building a command that is framework agnostic. So it will work with any directory structure you can throw at it.
For the future I'm thinking about a templates feature, that will be more like the artisan make commands.
But for now I want to have a good base to build upon.