5

Short story: I can not get method injection working with Laravel container installed using composer (https://packagist.org/packages/illuminate/container). Injection only works if used in the constructor of objects. For example:

class SomeClass {
    function __construct(InjectedClassWorksHere $obj) {}
    function someFunction(InjectedClassFailsHere $obj) {}
}

Long story: I was looking at re-factoring a major project to using Laravel, but due to business pressure, I am not able to invest in the time I would like. In an effort to not throw the "baby out with the bath water", I am using the individual Laravel components to up the elegance of the code being developed in the old branch. One of my favourite new techniques I picked up when evaluating Laravel was the concept of dependency injection. I was delighted to find out later that I could use that OUTSIDE of a Laravel project. I now have this working and all is well, except the dev version of the container found online does not seem to support method injection.

Has anyone else been able to get the container to work and do method injection outside of a Laravel project?

My approach so far...

composer.json

"illuminate/support": "5.0.*@dev",
"illuminate/container": "5.0.*@dev",

Application bootstrap code:

use Illuminate\Container\Container;

$container = new Container();
$container->bind('app', self::$container); //not sure if this is necessary

$dispatcher = $container->make('MyCustomDispatcher');
$dispatcher->call('some URL params to find controller');

With the above, I am able to inject in constructors of my controllers, but not their methods methods. What am I missing?

Full source... (C:\workspace\LMS>php cmd\test_container.php)

<?php

// This sets up my include path and calls the composer autoloader
require_once "bare_init.php";

use Illuminate\Container\Container;
use Illuminate\Support\ClassLoader;
use Illuminate\Support\Facades\Facade;

// Get a reference to the root of the includes directory
$basePath = dirname(dirname(__FILE__));

ClassLoader::register();
ClassLoader::addDirectories([
    $basePath
]);

$container = new Container();
$container->bind('app', $container);
$container->bind('path.base', $basePath);

class One {
    public $two;
    public $say = 'hi';
    function __construct(Two $two) {
        $this->two = $two;
    }
}

Class Two {
    public $some = 'thing';
    public function doStuff(One $one) {
        return $one->say;
    }
}

/* @var $one One */
$one = $container->make(One);
var_dump($one);
print $one->two->doStuff();

When I run the above, I get...

C:\workspace\LMS>php cmd\test_container.php
object(One)#9 (2) {
  ["two"]=>
  object(Two)#11 (1) {
    ["some"]=>
    string(5) "thing"
  }
  ["say"]=>
  string(2) "hi"
}

PHP Catchable fatal error:  Argument 1 passed to Two::doStuff() must be an instance of One, none 
given, called in C:\workspace\LMS\cmd\test_container.php on line 41
and defined in C:\workspace\LMS\cmd\test_container.php on line 33

Catchable fatal error: Argument 1 passed to Two::doStuff() must be an instance of One, none  
given, called in C:\workspace\LMS\cmd\test_container.php on line 41 and
defined in C:\workspace\LMS\cmd\test_container.php on line 33

Or, a more basic example that illustrates the injection working in a constructor but not a method...

class One {
    function __construct(Two $two) {}
    public function doStuff(Three $three) {}
}

class Two {}
class Three {}

$one = $container->make(One); // totally fine. Injection works
$one->doStuff(); // Throws Exception. (sad trombone)
Fred Read
  • 453
  • 5
  • 8
  • Yes, this setup worked for me, both in the constructor and method (though I did need to include `"illuminate/contracts": "5.0.*@dev"` in composer.json). Can you share the code that's calling `SomeFunction` in `SomeClass` and what gets returned? – damiani Sep 18 '14 at 02:23
  • Incidentally, Matt Stauffer has a great talk that discusses your incremental approach, a process which he calls ["Undercover Laravel"](https://www.youtube.com/watch?v=Qu6o4wTMo38). You might also find his project [Illuminate Non-Laravel](https://github.com/mattstauffer/IlluminateNonLaravel) interesting, though he's using v4.2.8, not v5. – damiani Sep 18 '14 at 02:28
  • I will have a look at that talk tonight. Looks interesting and certainly relevant! – Fred Read Sep 18 '14 at 14:29
  • Will edit my post to add full code of my sample – Fred Read Sep 18 '14 at 14:29

1 Answers1

8

Simply pass the instance of One into your call to Two:

$one = $container->make('One');
var_dump($one);
print $one->two->doStuff($one);

returns...

object(One)#8 (2) {
  ["two"]=>
  object(Two)#10 (1) {
    ["some"]=>
    string(5) "thing"
  }
  ["say"]=>
  string(2) "hi"
}
hi

Update: Corrected answer after further research

As mentioned below, in Laravel 5.0, method injection is only available on routes and controllers. So you can pull those into your project as well, and get a little more Laravel-y in the process. Here's how:

In composer.json, need to add in illuminate/routing and illuminate/events:

{
    "require-dev": {
        "illuminate/contracts": "5.0.*@dev",
        "illuminate/support": "5.0.*@dev",
        "illuminate/container": "5.0.*@dev",
        "illuminate/routing": "5.0.*@dev",
        "illuminate/events": "5.0.*@dev"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

In routing.php, set up Laravel's routing and controller services:

/**
 * routing.php
 *
 * Sets up Laravel's routing and controllers
 *
 * adapted from http://www.gufran.me/post/laravel-components
 * and http://www.gufran.me/post/laravel-illuminate-router-package-in-your-application
 */
$basePath = str_finish(dirname(__FILE__), '/app/');

$controllersDirectory = $basePath . 'Controllers';

// Register directories into the autoloader
Illuminate\Support\ClassLoader::register();
Illuminate\Support\ClassLoader::addDirectories($controllersDirectory);

// Instantiate the container
$app = new Illuminate\Container\Container();
$app['env'] = 'production';

$app->bind('app', $app); // optional
$app->bind('path.base', $basePath); // optional

// Register service providers
with (new Illuminate\Events\EventServiceProvider($app))->register();
with (new Illuminate\Routing\RoutingServiceProvider($app))->register();

require $basePath . 'routes.php';

$request = Illuminate\Http\Request::createFromGlobals();
$response = $app['router']->dispatch($request);
$response->send();

In Controllers/One.php, create the class as a Controller, so we can use L5's method injection:

/**
 * Controllers/One.php
 */
Class One extends Illuminate\Routing\Controller {
    public $some = 'thingOne';
    public $two;
    public $three;

    function __construct(Two $two) {
        $this->two = $two;
        echo('<pre>');
        var_dump ($two);
        echo ($two->doStuffWithTwo().'<br><br>');
    } 

    public function doStuff(Three $three) {
        var_dump ($three);
        return ($three->doStuffWithThree());
    }
}

In routes.php, define our test route:

$app['router']->get('/', 'One@dostuff');

Finally, in index.php, boot everything up and define our classes to test the dependency injection:

/**
 * index.php
 */

// turn on error reporting
ini_set('display_errors',1);  
error_reporting(E_ALL);

require 'vendor/autoload.php';
require 'routing.php';

// the classes we wish to inject
Class Two {
    public $some = 'thing Two';
    public function doStuffWithTwo() {
        return ('Doing stuff with Two');
    }
}
Class Three {
    public $some = 'thing Three';
    public function doStuffWithThree() {
        return ('Doing stuff with Three');
    }
}

Hit index.php and you should get this:

object(Two)#40 (1) {
  ["some"]=>
  string(9) "thing Two"
}
Doing stuff with Two

object(Three)#41 (1) {
  ["some"]=>
  string(11) "thing Three"
}
Doing stuff with Three

Some notes...

  • There's no need to bind the classes explicitly. Laravel takes care of this.
  • Now you have the added bonus of Laravel's routing and controllers
  • This works because now we don't have to call $one->doStuff();, with the empty parameter that throws the exception (since doStuff is expecting an instance). Instead, the router calls doStuff and resolves the IoC container for us.
  • Credit to http://www.gufran.me/post/laravel-illuminate-router-package-in-your-application which walks you through all of this, and which appears to be the inspiration for Matt Stauffer's project referenced above. Both are very cool and worth a read.
Community
  • 1
  • 1
damiani
  • 7,071
  • 2
  • 23
  • 24
  • Yes, this would work, but misses the point. The point is that One should work by injection on the method "doStuff" the same way it does on the constructor. – Fred Read Sep 18 '14 at 19:59
  • Another basic example that shows that injection works in the constructor but not in a method... class One { function __construct(Two $two) {} public function doStuff(Three $three) {} } Class Two {} class Three {} $one = $container->make(One); $one->doStuff(); – Fred Read Sep 18 '14 at 20:44
  • I believe the issue here is that Laravel 5's method injection only works on Controller and Route methods, meaning you would have to require `"illuminate/routing": "5.0.*@dev"` and `"illuminate/events": "5.0.*@dev"` in `composer.json`, and then register your class as a controller or route. The code for method injection lives in `framework / src / Illuminate / Routing / ControllerDispatcher.php`, which you can see [here](https://github.com/laravel/framework/blob/4.2/src/Illuminate/Routing/ControllerDispatcher.php). I'm testing this at the moment... – damiani Sep 18 '14 at 20:58
  • Revised answer above...give it a shot. – damiani Sep 19 '14 at 02:04