DEV Community

Cover image for How I Accidentally Found Myself Rewriting Laravel (Just to Fix Laravel Modules 😅)
HichemTech
HichemTech

Posted on

How I Accidentally Found Myself Rewriting Laravel (Just to Fix Laravel Modules 😅)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

Everything’s great! 🎉

Time to create our first module:

php artisan module:make IOT
Enter fullscreen mode Exit fullscreen mode

Boom, module created! So easy, right? Next step: creating a model called Device. Without thinking, I went:

php artisan make:model Device
Enter fullscreen mode Exit fullscreen mode

❌ 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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

🥳 🎉 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
Enter fullscreen mode Exit fullscreen mode

And...

INFO  Model [C:\xampp\htdocs\alpha\app\Models\Device.php] created successfully.
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Rewrite a ton of Laravel’s internal command logic.
  2. 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)

Collapse
 
xwero profile image
david duymelinck

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.