Search code examples
phplaravelcomposer-phplumen

Lumen loaded wrong controller when vendor directory is softlinked


In my example setup, I have 2 directory (each is lumen 8.x project, created by running composer create-project --prefer-dist laravel/lumen lumenNUMBER)

  • ~/www/lumen1/
  • ~/www/lumen2/

In my setup, webserver is apache/httpd with document root set to ~/www/

In lumen1 project:

  • file edited: routes/web.php
$router->get('/route1', function () use ($router) {
    return "this is first route";
});
$router->get('/route2', function () use ($router) {
    return "this is second route";
});
$router->get("/route5", ["uses"=>"CommonController@route5"] );

  • file added: app/Http/Controllers/CommonController.php
<?php
namespace App\Http\Controllers;
class CommonController extends Controller
{
    public function route5() { "this is fifth route"; }
}

In lumen2 project:

  • file edited: routes/web.php
$router->get('/route1', function () use ($router) {
    return "this is first route";
});
$router->get('/route2', function () use ($router) {
    return "this is second route";
});
$router->get('/route3', function () use ($router) {
    return "this is third route";
});
$router->get("/route4", ["uses"=>"SomeController@route4"] );
$router->get("/route5", ["uses"=>"CommonController@route5"] );
$router->get("/route6", ["uses"=>"CommonController@route6"] );
  • file added: app/Http/Controllers/CommonController.php
<?php
namespace App\Http\Controllers;
class CommonController extends Controller
{
    public function route5() { "this is fifth route"; }
    public function route6() { "this is sixth route"; }
}
  • file added: app/Http/Controllers/SomeController.php
<?php
namespace App\Http\Controllers;
class SomeController extends Controller
{
    public function route4() { "this is fourth route"; }
}

Problem

Everything is running fine and good, I can make such request and get expected response:

For lumen1:

  • request GET http://localhost/lumen1/public/route1 response this is first route
  • request GET http://localhost/lumen1/public/route2 response this is second route
  • request GET http://localhost/lumen1/public/route3 response error (expected): 404
  • request GET http://localhost/lumen1/public/route4 response error (expected): 404
  • request GET http://localhost/lumen1/public/route5 response this is fifth route
  • request GET http://localhost/lumen1/public/route6 response error (expected): 404

For lumen2:

  • request GET http://localhost/lumen2/public/route1 response this is first route
  • request GET http://localhost/lumen2/public/route2 response this is second route
  • request GET http://localhost/lumen2/public/route3 response this is third route
  • request GET http://localhost/lumen2/public/route4 response this is fourth route
  • request GET http://localhost/lumen2/public/route5 response this is fifth route
  • request GET http://localhost/lumen2/public/route6 response this is sixth route

But now I tried to get creative and deleted lumen2/vendor directory, then make symlink in lumen2/vendor to point to ../lumen1/vendor

  • ln -s ../lumen1/vendor vendor
  • output of ls -al on ~/www/lumen2:
    • lrwxrwxrwx 1 kristian kristian 16 Apr 21 08:49 vendor -> ../lumen1/vendor

The reason is that I'm low on disk space (this is not the only lumen project, I know it's only 40-50mb but the size is multiplied by number of project)

Now the request and their responses is (note that lumen1 is omitted since it's same with above):

  • request GET http://localhost/lumen2/public/route1 response this is first route
  • request GET http://localhost/lumen2/public/route2 response this is second route
  • request GET http://localhost/lumen2/public/route3 response this is third route
  • request GET http://localhost/lumen2/public/route4 response error (NOT expected): 500 Target class [App\Http\Controllers\SomeController] does not exist.
  • request GET http://localhost/lumen2/public/route5 response this is fifth route
  • request GET http://localhost/lumen2/public/route6 response error (NOT expected): 404

My analysis

  • closure routes do not get affected with softlinked vendor directory
  • route that use softlinked vendor directory will read it's controller from link target's controller instead of it's own

The question is:

  • why softlinked vendor directory makes lumen reads it's controller from 'lumen1' directory?
  • if this approach is not feasible, then how to reduce disk space usage on lumen and/or laravel vendor directory?

Solution

  • I have found the problem:

    I created 4 directory, each called a, b, c, d, each contains such php files:

    • view.php: <?php require_once("vendor/autoload.php"); echo "view-[x]";

    • vendor/autoload.php: <?php require_once(dirname( __FILE__ )."/../controller.php");

    • controller.php: <?php echo "app-[x]"; In which [x] is replaced with directory name (so a/controller.php contains <?php echo "app-a"; , b/controller.php contains <?php echo "app-b"; , etc...)

    • directory a is the 'master'/'main'

    • directory b is copied from directory a cp -r a b

    • directory c is copied from directory a cp -r a c, then I removed c/vendor and then softlink it from a's vendor directory: cd c; ln -s ../a/vendor vendor

    • directory d is copied from directory a cp -r a d, then I removed d/vendor and then hardlink it from a's vendor directory: cp -al a/vendor d/vendor

    I found out that output from:

    • GET http://localhost/a/view.php is app-a view-a
    • GET http://localhost/b/view.php is app-b view-b
    • GET http://localhost/c/view.php is app-a view-c
    • GET http://localhost/d/view.php is app-d view-d

    The solution: don't softlink, just hardlink.

    This solves:

    • lower disk usage compared to plainly copying vendor directory (it still consume inodes though, my laravel project have 80mb+ vendor directory, hardlinking consume about 6mb when observed via df -h)
    • http request is served by correct controllers

    Why the trouble?

    As stated by NicoHaase in comment of my question, two separate application HAVE TO stay on two seperate vendor folders.

    Yes, I know, I know this and I strongly agree. But in case it's not two separate applications. It's one application inside git repository that's made into multiple directory (via git worktree). And I have an ironclad rule that when I update one branch/worktree's composer.lock/composer.json (dependency), then I have to update the others. So the goal here is to save disk space.

    Why don't I just use git's checkout command then? That's because I want to be able to simultaneously work on multiple branch at same time. Stashing and/or committing unfinished changes is out of the question.

    Why bother about saving disk space? When you have this much free space you will try to save disk space:

    $ # this is the size of my vendor directory
    du -sh vendor
    83M     vendor
    $ df -h
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/sda1       XXXX  XXX  413M  XXX /
    
    $ rm -r vendor
    $ # this is my free space when I deleted vendor directory
    $ df -h       
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/sda1       XXXX  XXX  419M  XXX /
    
    $ cp -r ../../myproject/vendor/ vendor
    $ # this is my free space when copied vendor directory
    $ df -h
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/sda1       XXXX  XXX  336M  XXX /
    
    $ cp -al ../myproject/vendor/ vendor
    $ # this is my free space when using hardlinked vendor directory
    $ df -h
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/sda1       XXXX  XXX  413M  XXX /
    

    TL;DR: don't softlink, do hardlink instead

    instead of ln -s [target] [link], do cp -al [target] [link]