Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use Laravel's IOC container outside of Laravel for method injection

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)
like image 558
Fred Read Avatar asked Mar 18 '23 11:03

Fred Read


1 Answers

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.
like image 92
damiani Avatar answered Mar 21 '23 04:03

damiani