Giter VIP home page Giter VIP logo

auryn's Introduction

auryn Build Status

auryn is a recursive dependency injector. Use auryn to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications.

How It Works

Among other things, auryn recursively instantiates class dependencies based on the parameter type-hints specified in class constructor signatures. This requires the use of Reflection. You may have heard that "reflection is slow". Let's clear something up: anything can be "slow" if you're doing it wrong. Reflection is an order of magnitude faster than disk access and several orders of magnitude faster than retrieving information (for example) from a remote database. Additionally, each reflection offers the opportunity to cache the results if you're worried about speed. auryn caches any reflections it generates to minimize the potential performance impact.

auryn is NOT a Service Locator. DO NOT turn it into one by passing the injector into your application classes. Service Locator is an anti-pattern; it hides class dependencies, makes code more difficult to maintain and makes a liar of your API! You should only use an injector for wiring together the disparate parts of your application during your bootstrap phase.

The Guide

Basic Usage

Advanced Usage

Example Use Cases

Requirements and Installation

  • auryn requires PHP 5.3 or higher.

Installation

Github

You can clone the latest auryn iteration at anytime from the github repository:

$ git clone git://github.com/rdlowrey/auryn.git
Composer

You may also use composer to include auryn as a dependency in your projects composer.json. The relevant package is rdlowrey/auryn.

Alternatively require the package using composer cli:

composer require rdlowrey/auryn
Manual Download

Archived tagged release versions are also available for manual download on the project tags page

Basic Usage

To start using the injector, simply create a new instance of the Auryn\Injector ("the Injector") class:

<?php
$injector = new Auryn\Injector;

Basic Instantiation

If a class doesn't specify any dependencies in its constructor signature there's little point in using the Injector to generate it. However, for the sake of completeness consider that you can do the following with equivalent results:

<?php
$injector = new Auryn\Injector;
$obj1 = new SomeNamespace\MyClass;
$obj2 = $injector->make('SomeNamespace\MyClass');

var_dump($obj2 instanceof SomeNamespace\MyClass); // true
Concrete Type-hinted Dependencies

If a class only asks for concrete dependencies you can use the Injector to inject them without specifying any injection definitions. For example, in the following scenario you can use the Injector to automatically provision MyClass with the required SomeDependency and AnotherDependency class instances:

<?php
class SomeDependency {}

class AnotherDependency {}

class MyClass {
    public $dep1;
    public $dep2;
    public function __construct(SomeDependency $dep1, AnotherDependency $dep2) {
        $this->dep1 = $dep1;
        $this->dep2 = $dep2;
    }
}

$injector = new Auryn\Injector;
$myObj = $injector->make('MyClass');

var_dump($myObj->dep1 instanceof SomeDependency); // true
var_dump($myObj->dep2 instanceof AnotherDependency); // true
Recursive Dependency Instantiation

One of the Injector's key attributes is that it recursively traverses class dependency trees to instantiate objects. This is just a fancy way of saying, "if you instantiate object A which asks for object B, the Injector will instantiate any of object B's dependencies so that B can be instantiated and provided to A". This is perhaps best understood with a simple example. Consider the following classes in which a Car asks for Engine and the Engine class has concrete dependencies of its own:

<?php
class Car {
    private $engine;
    public function __construct(Engine $engine) {
        $this->engine = $engine;
    }
}

class Engine {
    private $sparkPlug;
    private $piston;
    public function __construct(SparkPlug $sparkPlug, Piston $piston) {
        $this->sparkPlug = $sparkPlug;
        $this->piston = $piston;
    }
}

$injector = new Auryn\Injector;
$car = $injector->make('Car');
var_dump($car instanceof Car); // true

Injection Definitions

You may have noticed that the previous examples all demonstrated instantiation of classes with explicit, type-hinted, concrete constructor parameters. Obviously, many of your classes won't fit this mold. Some classes will type-hint interfaces and abstract classes. Some will specify scalar parameters which offer no possibility of type-hinting in PHP. Still other parameters will be arrays, etc. In such cases we need to assist the Injector by telling it exactly what we want to inject.

Defining Class Names for Constructor Parameters

Let's look at how to provision a class with non-concrete type-hints in its constructor signature. Consider the following code in which a Car needs an Engine and Engine is an interface:

<?php
interface Engine {}

class V8 implements Engine {}

class Car {
    private $engine;
    public function __construct(Engine $engine) {
        $this->engine = $engine;
    }
}

To instantiate a Car in this case, we simply need to define an injection definition for the class ahead of time:

<?php
$injector = new Auryn\Injector;
$injector->define('Car', ['engine' => 'V8']);
$car = $injector->make('Car');

var_dump($car instanceof Car); // true

The most important points to notice here are:

  1. A custom definition is an array whose keys match constructor parameter names
  2. The values in the definition array represent the class names to inject for the specified parameter key

Because the Car constructor parameter we needed to define was named $engine, our definition specified an engine key whose value was the name of the class (V8) that we want to inject.

Custom injection definitions are only necessary on a per-parameter basis. For example, in the following class we only need to define the injectable class for $arg2 because $arg1 specifies a concrete class type-hint:

<?php
class MyClass {
    private $arg1;
    private $arg2;
    public function __construct(SomeConcreteClass $arg1, SomeInterface $arg2) {
        $this->arg1 = $arg1;
        $this->arg2 = $arg2;
    }
}

$injector = new Auryn\Injector;
$injector->define('MyClass', ['arg2' => 'SomeImplementationClass']);

$myObj = $injector->make('MyClass');

NOTE: Injecting instances where an abstract class is type-hinted works in exactly the same way as the above examples for interface type-hints.

Using Existing Instances in Injection Definitions

Injection definitions may also specify a pre-existing instance of the requisite class instead of the string class name:

<?php
interface SomeInterface {}

class SomeImplementation implements SomeInterface {}

class MyClass {
    private $dependency;
    public function __construct(SomeInterface $dependency) {
        $this->dependency = $dependency;
    }
}

$injector = new Auryn\Injector;
$dependencyInstance = new SomeImplementation;
$injector->define('MyClass', [':dependency' => $dependencyInstance]);

$myObj = $injector->make('MyClass');

var_dump($myObj instanceof MyClass); // true

NOTE: Since this define() call is passing raw values (as evidenced by the colon : usage), you can achieve the same result by omitting the array key(s) and relying on parameter order rather than name. Like so: $injector->define('MyClass', [$dependencyInstance]);

Specifying Injection Definitions On the Fly

You may also specify injection definitions at call-time with Auryn\Injector::make. Consider:

<?php
interface SomeInterface {}

class SomeImplementationClass implements SomeInterface {}

class MyClass {
    private $dependency;
    public function __construct(SomeInterface $dependency) {
        $this->dependency = $dependency;
    }
}

$injector = new Auryn\Injector;
$myObj = $injector->make('MyClass', ['dependency' => 'SomeImplementationClass']);

var_dump($myObj instanceof MyClass); // true

The above code shows how even though we haven't called the Injector's define method, the call-time specification allows us to instantiate MyClass.

NOTE: on-the-fly instantiation definitions will override a pre-defined definition for the specified class, but only in the context of that particular call to Auryn\Injector::make.

Type-Hint Aliasing

Programming to interfaces is one of the most useful concepts in object-oriented design (OOD), and well-designed code should type-hint interfaces whenever possible. But does this mean we have to assign injection definitions for every class in our application to reap the benefits of abstracted dependencies? Thankfully the answer to this question is, "NO." The Injector accommodates this goal by accepting "aliases". Consider:

<?php
interface Engine {}
class V8 implements Engine {}
class Car {
    private $engine;
    public function __construct(Engine $engine) {
        $this->engine = $engine;
    }
}

$injector = new Auryn\Injector;

// Tell the Injector class to inject an instance of V8 any time
// it encounters an Engine type-hint
$injector->alias('Engine', 'V8');

$car = $injector->make('Car');
var_dump($car instanceof Car); // bool(true)

In this example we've demonstrated how to specify an alias class for any occurrence of a particular interface or abstract class type-hint. Once an implementation is assigned, the Injector will use it to provision any parameter with a matching type-hint.

IMPORTANT: If an injection definition is defined for a parameter covered by an implementation assignment, the definition takes precedence over the implementation.

Non-Class Parameters

All of the previous examples have demonstrated how the Injector class instantiates parameters based on type-hints, class name definitions and existing instances. But what happens if we want to inject a scalar or other non-object variable into a class? First, let's establish the following behavioral rule:

IMPORTANT: The Injector assumes all named-parameter definitions are class names by default.

If you want the Injector to treat a named-parameter definition as a "raw" value and not a class name, you must prefix the parameter name in your definition with a colon character :. For example, consider the following code in which we tell the Injector to share a PDO database connection instance and define its scalar constructor parameters:

<?php
$injector = new Auryn\Injector;
$injector->share('PDO');
$injector->define('PDO', [
    ':dsn' => 'mysql:dbname=testdb;host=127.0.0.1',
    ':username' => 'dbuser',
    ':passwd' => 'dbpass'
]);

$db = $injector->make('PDO');

The colon character preceding the parameter names tells the Injector that the associated values ARE NOT class names. If the colons had been omitted above, auryn would attempt to instantiate classes of the names specified in the string and an exception would result. Also, note that we could just as easily specified arrays or integers or any other data type in the above definitions. As long as the parameter name is prefixed with a :, auryn will inject the value directly without attempting to instantiate it.

NOTE: As mentioned previously, since this define() call is passing raw values, you may opt to assign the values by parameter order rather than name. Since PDO's first three parameters are $dsn, $username, and $password, in that order, you could accomplish the same result by leaving out the array keys, like so: $injector->define('PDO', ['mysql:dbname=testdb;host=127.0.0.1', 'dbuser', 'dbpass']);

Global Parameter Definitions

Sometimes applications may reuse the same value everywhere. However, it can be a hassle to manually specify definitions for this sort of thing everywhere it might be used in the app. auryn mitigates this problem by exposing the Injector::defineParam() method. Consider the following example ...

<?php
$myUniversalValue = 42;

class MyClass {
    public $myValue;
    public function __construct($myValue) {
        $this->myValue = $myValue;
    }
}

$injector = new Auryn\Injector;
$injector->defineParam('myValue', $myUniversalValue);
$obj = $injector->make('MyClass');
var_dump($obj->myValue === 42); // bool(true)

Because we specified a global definition for myValue, all parameters that are not in some other way defined (as below) that match the specified parameter name are auto-filled with the global value. If a parameter matches any of the following criteria the global value is not used:

  • A typehint
  • A predefined injection definition
  • A custom call time definition

Advanced Usage

Instance Sharing

One of the more ubiquitous plagues in modern OOP is the Singleton anti-pattern. Coders looking to limit classes to a single instance often fall into the trap of using static Singleton implementations for things like configuration classes and database connections. While it's often necessary to prevent multiple instances of a class, the Singleton method spells death to testability and should generally be avoided. Auryn\Injector makes sharing class instances across contexts a triviality while allowing maximum testability and API transparency.

Let's consider how a typical problem facing object-oriented web applications is easily solved by wiring together your application using auryn. Here, we want to inject a single database connection instance across multiple layers of an application. We have a controller class that asks for a DataMapper that requires a PDO database connection instance:

<?php
class DataMapper {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
}

class MyController {
    private $mapper;
    public function __construct(DataMapper $mapper) {
        $this->mapper = $mapper;
    }
}

$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');

$injector = new Auryn\Injector;
$injector->share($db);

$myController = $injector->make('MyController');

In the above code, the DataMapper instance will be provisioned with the same PDO database connection instance we originally shared. This example is contrived and overly simple, but the implication should be clear:

By sharing an instance of a class, Auryn\Injector will always use that instance when provisioning classes that type-hint the shared class.

A Simpler Example

Let's look at a simple proof of concept:

<?php
class Person {
    public $name = 'John Snow';
}

$injector = new Auryn\Injector;
$injector->share('Person');

$person = $injector->make('Person');
var_dump($person->name); // John Snow

$person->name = 'Arya Stark';

$anotherPerson = $injector->make('Person');
var_dump($anotherPerson->name); // Arya Stark
var_dump($person === $anotherPerson); // bool(true) because it's the same instance!

Defining an object as shared will store the provisioned instance in the Injector's shared cache and all future requests to the provider for an injected instance of that class will return the originally created object. Note that in the above code, we shared the class name (Person) instead of an actual instance. Sharing works with either a class name or an instance of a class. The difference is that when you specify a class name, the Injector will cache the shared instance the first time it is asked to create it.

NOTE: Once the Injector caches a shared instance, call-time definitions passed to Auryn\Injector::make will have no effect. Once shared, an instance will always be returned for instantiations of its type until the object is un-shared or refreshed:

Instantiation Delegates

Often factory classes/methods are used to prepare an object for use after instantiation. auryn allows you to integrate factories and builders directly into the injection process by specifying callable instantiation delegates on a per-class basis. Let's look at a very basic example to demonstrate the concept of injection delegates:

<?php
class MyComplexClass {
    public $verification = false;
    public function doSomethingAfterInstantiation() {
        $this->verification = true;
    }
}

$complexClassFactory = function() {
    $obj = new MyComplexClass;
    $obj->doSomethingAfterInstantiation();

    return $obj;
};

$injector = new Auryn\Injector;
$injector->delegate('MyComplexClass', $complexClassFactory);

$obj = $injector->make('MyComplexClass');
var_dump($obj->verification); // bool(true)

In the above code we delegate instantiation of the MyComplexClass class to a closure, $complexClassFactory. Once this delegation is made, the Injector will return the results of the specified closure when asked to instantiate MyComplexClass.

Available Delegate Types

Any valid PHP callable may be registered as a class instantiation delegate using Auryn\Injector::delegate. Additionally you may specify the name of a delegate class that specifies an __invoke method and it will be automatically provisioned and have its __invoke method called at delegation time. Instance methods from uninstantiated classes may also be specified using the ['NonStaticClassName', 'factoryMethod'] construction. For example:

<?php
class SomeClassWithDelegatedInstantiation {
    public $value = 0;
}
class SomeFactoryDependency {}
class MyFactory {
    private $dependency;
    function __construct(SomeFactoryDependency $dep) {
        $this->dependency = $dep;
    }
    function __invoke() {
        $obj = new SomeClassWithDelegatedInstantiation;
        $obj->value = 1;
        return $obj;
    }
    function factoryMethod() {
        $obj = new SomeClassWithDelegatedInstantiation;
        $obj->value = 2;
        return $obj;
    }
}

// Works because MyFactory specifies a magic __invoke method
$injector->delegate('SomeClassWithDelegatedInstantiation', 'MyFactory');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
var_dump($obj->value); // int(1)

// This also works
$injector->delegate('SomeClassWithDelegatedInstantiation', 'MyFactory::factoryMethod');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
var_dump($obj->value); // int(2)

Prepares and Setter Injection

Constructor injection is almost always preferable to setter injection. However, some APIs require additional post-instantiation mutations. auryn accommodates these use cases with its Injector::prepare() method. Users may register any class or interface name for post-instantiation modification. Consider:

<?php

class MyClass {
    public $myProperty = 0;
}

$injector->prepare('MyClass', function($myObj, $injector) {
    $myObj->myProperty = 42;
});

$myObj = $injector->make('MyClass');
var_dump($myObj->myProperty); // int(42)

While the above example is contrived, the usefulness should be clear.

Injecting for Execution

In addition to provisioning class instances using constructors, auryn can also recursively instantiate the parameters of any valid PHP callable. The following examples all work:

<?php
$injector = new Auryn\Injector;
$injector->execute(function(){});
$injector->execute([$objectInstance, 'methodName']);
$injector->execute('globalFunctionName');
$injector->execute('MyStaticClass::myStaticMethod');
$injector->execute(['MyStaticClass', 'myStaticMethod']);
$injector->execute(['MyChildStaticClass', 'parent::myStaticMethod']);
$injector->execute('ClassThatHasMagicInvoke');
$injector->execute($instanceOfClassThatHasMagicInvoke);
$injector->execute('MyClass::myInstanceMethod');

Additionally, you can pass in the name of a class for a non-static method and the injector will automatically provision an instance of the class (subject to any definitions or shared instances already stored by the injector) before provisioning and invoking the specified method:

<?php
class Dependency {}
class AnotherDependency {}
class Example {
    function __construct(Dependency $dep){}
    function myMethod(AnotherDependency $arg1, $arg2) {
        return $arg2;
    }
}

$injector = new Auryn\Injector;

// outputs: int(42)
var_dump($injector->execute('Example::myMethod', $args = [':arg2' => 42]));

Dependency Resolution

Auryn\Injector resolves dependencies in the following order:

  1. If a shared instance exists for the class in question, the shared instance will always be returned
  2. If a delegate callable is assigned for a class, its return result will always be used
  3. If a call-time definition is passed to Auryn\Injector::make, that definition will be used
  4. If a pre-defined definition exists, it will be used
  5. If a dependency is type-hinted, the Injector will recursively instantiate it subject to any implementations or definitions
  6. If no type-hint exists and the parameter has a default value, the default value is injected
  7. If a global parameter value is defined that value is used
  8. Throw an exception because you did something stupid

Example Use Cases

Dependency Injection Containers (DIC) are generally misunderstood in the PHP community. One of the primary culprits is the misuse of such containers in the mainstream application frameworks. Often, these frameworks warp their DICs into Service Locator anti-patterns. This is a shame because a good DIC should be the exact opposite of a Service Locator.

auryn Is NOT A Service Locator!

There's a galaxy of differences between using a DIC to wire together your application versus passing the DIC as a dependency to your objects (Service Locator). Service Locator (SL) is an anti-pattern -- it hides class dependencies, makes code difficult to maintain and makes a liar of your API.

When you pass a SL into your constructors it makes it difficult to determine what the class dependencies really are. A House object depends on Door and Window objects. A House object DOES NOT depend on an instance of ServiceLocator regardless of whether the ServiceLocator can provide Door and Window objects.

In real life you wouldn't build a house by transporting the entire hardware store (hopefully) to the construction site so you can access any parts you need. Instead, the foreman (__construct()) asks for the specific parts that will be needed (Door and Window) and goes about procuring them. Your objects should function in the same way; they should ask only for the specific dependencies required to do their jobs. Giving the House access to the entire hardware store is at best poor OOP style and at worst a maintainability nightmare. The takeaway here is this:

IMPORTANT: do not use auryn like a Service Locator!

Avoiding Evil Singletons

A common difficulty in web applications is limiting the number of database connection instances. It's wasteful and slow to open up new connections each time we need to talk to a database. Unfortunately, using singletons to limit these instances makes code brittle and hard to test. Let's see how we can use auryn to inject the same PDO instance across the entire scope of our application.

Say we have a service class that requires two separate data mappers to persist information to a database:

<?php

class HouseMapper {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    public function find($houseId) {
        $query = 'SELECT * FROM houses WHERE houseId = :houseId';

        $stmt = $this->pdo->prepare($query);
        $stmt->bindValue(':houseId', $houseId);

        $stmt->setFetchMode(PDO::FETCH_CLASS, 'Model\\Entities\\House');
        $stmt->execute();
        $house = $stmt->fetch(PDO::FETCH_CLASS);

        if (false === $house) {
            throw new RecordNotFoundException(
                'No houses exist for the specified ID'
            );
        }

        return $house;
    }

    // more data mapper methods here ...
}

class PersonMapper {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    // data mapper methods here
}

class SomeService {
    private $houseMapper;
    private $personMapper;
    public function __construct(HouseMapper $hm, PersonMapper $pm) {
        $this->houseMapper = $hm;
        $this->personMapper = $pm;
    }
    public function doSomething() {
        // do something with the mappers
    }
}

In our wiring/bootstrap code, we simply instantiate the PDO instance once and share it in the context of the Injector:

<?php
$pdo = new PDO('sqlite:some_sqlite_file.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$injector = new Auryn\Injector;

$injector->share($pdo);
$mapper = $injector->make('SomeService');

In the above code, the DIC instantiates our service class. More importantly, the data mapper classes it generates to do so are injected with the same database connection instance we originally shared.

Of course, we don't have to manually instantiate our PDO instance. We could just as easily seed the container with a definition for how to create the PDO object and let it handle things for us:

<?php
$injector->define('PDO', [
    ':dsn' => 'sqlite:some_sqlite_file.db'
]);
$injector->share('PDO');
$service = $injector->make('SomeService');

In the above code, the injector will pass the string definition as the $dsn argument in the PDO::__construct method and generate the shared PDO instance automatically only if one of the classes it instantiates requires a PDO instance!

App-Bootstrapping

DICs should be used to wire together the disparate objects of your application into a cohesive functional unit (generally at the bootstrap or front-controller stage of the application). One such usage provides an elegant solution for one of the thorny problems in object-oriented (OO) web applications: how to instantiate classes in a routed environment where the dependencies are not known ahead of time.

Consider the following front controller code whose job is to:

  1. Load a list of application routes and pass them to the router
  2. Generate a model of the client's HTTP request
  3. Route the request instance given the application's route list
  4. Instantiate the routed controller and invoke a method appropriate to the HTTP request
<?php

define('CONTROLLER_ROUTES', '/hard/path/to/routes.xml');

$routeLoader = new RouteLoader();
$routes = $routeLoader->loadFromXml(CONTROLLER_ROUTES);
$router = new Router($routes);

$requestDetector = new RequestDetector();
$request = $requestDetector->detectFromSuperglobal($_SERVER);

$requestUri = $request->getUri();
$requestMethod = strtolower($request->getMethod());

$injector = new Auryn\Injector;
$injector->share($request);

try {
    if (!$controllerClass = $router->route($requestUri, $requestMethod)) {
        throw new NoRouteMatchException();
    }

    $controller = $injector->make($controllerClass);
    $callableController = array($controller, $requestMethod);

    if (!is_callable($callableController)) {
        throw new MethodNotAllowedException();
    } else {
        $callableController();
    }

} catch (NoRouteMatchException $e) {
    // send 404 response
} catch (MethodNotAllowedException $e) {
    // send 405 response
} catch (Exception $e) {
    // send 500 response
}

And elsewhere we have various controller classes, each of which ask for their own individual dependencies:

<?php

class WidgetController {
    private $request;
    private $mapper;
    public function __construct(Request $request, WidgetDataMapper $mapper) {
        $this->request = $request;
        $this->mapper = $mapper;
    }
    public function get() {
        // do something for HTTP GET requests
    }
    public function post() {
        // do something for HTTP POST requests
    }
}

In the above example the auryn DIC allows us to write fully testable, fully OO controllers that ask for their dependencies. Because the DIC recursively instantiates the dependencies of objects it creates we have no need to pass around a Service Locator. Additionally, this example shows how we can eliminate evil Singletons using the sharing capabilities of the auryn DIC. In the front controller code, we share the request object so that any classes instantiated by the Auryn\Injector that ask for a Request will receive the same instance. This feature not only helps eliminate Singletons, but also the need for hard-to-test static properties.

auryn's People

Contributors

4d47 avatar alexssssss avatar aliselcuk avatar ascii-soup avatar bwoebi avatar danack avatar furgas avatar garrettw avatar gforsythe-godaddy avatar gigablah avatar hajo-p avatar j7mbo avatar jeichorn avatar kelunik avatar lencse avatar lt avatar m6w6 avatar morrisonlevi avatar orthographic-pedant avatar peter279k avatar rdlowrey avatar sam159 avatar shadowhand avatar staabm avatar stanangeloff avatar szepeviktor avatar vlakarados avatar zvax avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

auryn's Issues

Executing aliased classes, chooses wrong class

The code

 $injector->alias('SomeClass', 'ExtendClass');
 $injector->execute('SomeClass', 'renderImage']);

and

$injector->alias('SomeClass', 'ExtendClass');
$object = $injector->make('SomeClass');
$injector->execute([$object, 'renderImage']);

Don't result in the same object being instantiated and called, instead the first version always ignores the class aliasing and always instantiates the base class.

I'll almost certainly fix this in my refactored branch. Just recording this here so when done I can reference the relevant changes.

class BaseExecutableClass {
    function foo() {
        return 'This is the BaseExecutableClass';
    }
}

class ExtendsExecutableClass extends BaseExecutableClass {
    function foo() {
        return 'This is the ExtendsExecutableClass';
    }
}

public function testExecutableAliasing() {
       $provider = new Provider();
        $provider->alias('BaseExecutableClass', 'ExtendsExecutableClass');
        $result = $provider->execute(['BaseExecutableClass', 'foo']);
        $this->assertEquals('This is the ExtendsExecutableClass', $result);
}

Injecting dynamic dependency for dependency.

@rdlowrey The other day you asked what features I think are still needed - here is a second one:


I have some code that without DI looks like this:

class FetchImageTask implements BackgroundTask {}

class TaskRunner {
    function __construct(BackgroundTask $taskToRun, Logger $logger, $taskName) {}
}

class ControllerClass {
    function __construct() {
        $fetchImageTask = new FetchImageTask();
        $taskRunner = new TaskRunner($fetchImageTask, new Logger(), 'fetchImage');
    }
}

Converting it to use DI with Auryn, I apparently have to create an extra class to make it possible e.g.


class FetchImageTaskRunner extends TaskRunner {
    function __construct(FetchImageTask $fetchImageTask, Logger $logger, $taskName) {
        parent::__construct($fetchImageTask, $logger, $taskName);
    }
}

class ControllerClass {
    functon __construct(FetchImageTaskRunner $fetchImageTaskRunner) {
    }
}

This is a totally feasible solution, however it smells quite a bit as we're creating a class that does nothing, just due to a limitation on the language/DI tool being used.

It could also be solved by passing in a factory:


class TaskRunnerFactory {
    function create(BackgroundTask $taskToRun, Logger $logger, $taskName) {
        return new TaskRunner($taskToRun, $logger, $taskName)
    }
}

class ControllerClass {
    function __construct(TaskRunnerFactory $taskRunnerFactory, FetchImageTask $fetchImageTask, Logger $logger, $taskName) {
        $taskRunner = $taskRunnerFactory->create($fetchImageTask, $logger, $taskName);
    }
}

However that also smells. Again, it's creating an extra class, just because we the tool can't do the DI directly as we'd like it. It also means that parameters that need to be passed into the class 'TaskRunner' have to be passed into the 'ControllerClass' which shouldn't have to know about them.

Can you think of a better way to do this?

Change / Remove unparsable json file

In some IDE's (in my case, Netbeans), after downloading Auryn via compser, my IDE shows errors for a file - specifically with config_with_unparsable.json.

I can't specifically choose to ignore this file, so it's constantly throwing up a nasty red exclamation mark on my file.

Any chance of an alternative test to check for invalid json? Perhaps creating a file in memory using SplTempFileObject or similar?

Note: This is of course an extremely tiny issue - so at the bottom of the to-do list, if at all.

Automatic provisioning for delegates

Now that Injector::execute is able to provision dependencies it may be worth using this functionality to simply the use of delegate/factory instantiators. Currently, Provider::delegate requires a valid PHP callable. Consider the following scenario:

<?php
interface Auto {}
class Carerra911 implements Auto {}
class FordPrefect implements Auto {}
class CarSelector {
    function select() {
        return (extension_loaded('porsche'))
            ? new Carerra911
            : new FordPrefect;
    }
}

Because I use the Auto interface all over my application I want to delegate its
instantiation to my Injector. Currently I'd need to do this:

<?php
$injector->delegate('Auto', [new CarSelector, 'select']);

It would be a nice feature to have the delegate automatically detect callable classes (those with __invoke) and class methods for (as yet) uninstantiated delegate classes.

So in the above scenario, instead of manually instantiating the CarSelector and passing a callable to Injector::delegate I'd be able to do this:

<?php
$injector->delegate('Auto', ['CarSelector', 'select']);

Or, if CarSelector used __invoke instead of select:

<?php
$injector->delegate('Auto', 'CarSelector');

All that's needed is to check if the delegate passed is callable. If not, we attempt to use Injector::execute to create a callable. If that fails, we error out like normal. The obvious advantage here is that you get the full power of configured dependency provision for your delegates instead of being forced to supply them in a callable state.

Reusing the injector inside delegated instantiators

When you use an delegated instantiator: https://github.com/rdlowrey/Auryn#instantiation-delegates

Is it possible to reuse the $injector inside the function? So something like this:

$complexClassFactory = function() {
    $obj = new MyComplexClass($injector->make('AnotherDifferentClass'));
    $obj->doSomethingAfterInstantiation();

    return $obj;
};

For example, Pimple allows the container to be reused inside the function since it injects the container inside the function.

Provision function delegates with dependencies

This may be a bug, or a feature request. When using a delegate function, either as a closure or a named function, I expected the delegate to be provisioned with hinted dependencies as though it were called by execute(). In actuality, it receives a string as an argument -- the name of the class being delegated.

A change to this behavior may have BC issues, if someone is using that string for a centralized provider.

Here is a repo using a closure: https://gist.github.com/GRMule/addd42fe35263b3f518a

And a similar repo, using a named function: https://gist.github.com/Danack/e54e9d9fa84914facb24

My real use-case, I have a database wrapper, and I want to pass in the host, user, password, etc. Those items are coming from a Configuration object. So, I set up a delegate like this for my database class:

$injector->delegate('DataAccessMysql', function (Configuration $config) {
    return new DataAccessMySql(
        $config->get('database.host'),
        $config->get('database.user'),
        $config->get('database.password'),
        $config->get('database.database')
    );
});

I expected my delegate to get the dependency injected, as though I had called it with execute(), but instead, it gets a string with the name of the class. Which, of course, throws an error.

I was able to work around the problem using this, which gives a fair idea of the expected behavior:

    $injector->delegate('DataAccessMysql', function () use ($injector) {
        return $injector->execute(function (Configuration $config) {
            return new DataAccessMySql(
                $config->get('database.host'),
                $config->get('database.user'),
                $config->get('database.password'),
                $config->get('database.database')
            );
        });
    });

Examples showing non-existent class

Several examples show passing a new instance of Auryn\ReflectionPool into Injector constructor. However, that class does not seem to be defined anywhere in the library.

The Injector constructor itself creates a new CachingReflector if no dependency is passed -- is the documentation out of date in this respect?

"Injection definition/implementation required for non-concrete parameter" exception is a liar

The exception which gets thrown when an implementation is needed is lying about the parameter name.

interface Foo {}

class Bar
{
    public function __construct(Foo $foo) {}
}

$injector = new Provider();

$injector->make('Bar');

results in:

Injection definition/implementation required for non-concrete parameter $interface of type Foo' in

where I would expect it to say the parameter is called $foo.

Unexpected InjectionException

I have uploaded the basic code for which I'm hitting the following exception:

'Auryn\InjectionException' with message 'Injection definition/implementation required for non-concrete parameter $em of type Doctrine\ORM\EntityManager'

After forking / using composer update, the only things this application is doing is:

  • Creating a new silex application
  • Registering a service provider in it
  • Retrieving the registered service provider and sharing it with Auryn
  • Running execute()

The execute hits the App\Controllers\IndexController::indexAction(), the constructor of which looks like the following:

public function indexAction(Doctrine\ORM\EntityManager $em) { ... }

CTRL + Clicking this in my IDE does in fact take me to the final class that is the EntityManager.

Please could you advise? I'm hoping this is what you meant when you wanted it cut down to it's bare minimum :)

generateExecutableReflection needs to give more information

Currently it doesn't tell you what is wrong. It needs to give appropriate information about the callable that is failing e.g. in generateExecutableFromString the exception message should include the string that was attempted to be executed.

Make not following inheritance chain to the delegated interface of abstract extended by concrete

I don't like the title, but that was my 3rd try, so we'll go with it.

I have an interface, with an abstract class that implements the interface. Elsewhere, there is a concrete class which extends the abstract.

If I establish a delegate for the interface, then try to provision the concrete, make() will not call that delegate, but instead directly attempt to provision the concrete class.

    include('../vendor/autoload.php');
    $injector = new Auryn\Injector();

    // given an interface
    interface iFoo {}
    // with an abstract implementation
    abstract class Foo implements iFoo {
        public function __construct(
            $argumentFactoryWouldProvide
        ) {
        }
        abstract public function someMethod();
    }

    // and a concrete implementation
    class Bar extends Foo {
        public function someMethod() {}
    }

    // this factory can make any iFoo's
    class iFooFactory {
        public function makeMe() {
            return new Bar('factory-provided-argument');
        }
    }

    //$injector->alias('iFoo', 'Foo'); // this (and variations thereof) doesn't make a difference
    $injector->delegate('iFoo', 'iFooFactory::makeMe');

    $bar = $injector->make('Bar'); // expected: talk to my iFooFactory
                         // actual: exception about non-class Foo param
    var_dump($bar); // nope

Simplifying raw parameter injection

This is something I've been meaning to do for awhile ...

Currently you're required to to use the raw injection prefix when defining parameters for any argument type even if it's not a string.

<?php
class MyClass {
    function __construct(StdClass $obj){}
}
$injector->define('MyClass', [':obj' => new StdClass]);

However, the : prefix should only be necessary if the passed argument is a string (so the injector can differentiate between a class name and an actual raw argument). The injector should be smart enough to know that if a non-string array value was passed in the class's injection definition that it's meant to be a raw parameter. This is a small change that would eliminate one of the major sources of confusion for new users.

Soft / optional dependencies

Forgive me if this has been asked before, and I know there's a way to specify them, but I'm trying to address the more "automatic" behavior as much as possible with this question.

As it stands, and as far as I can tell, Auryn will not try to instantiate dependencies which are typehinted, but which have been made optional, i.e it would not provide __construct(Foo $foo = NULL) with an instance of Foo.

It is possible to call make and directly say ':foo' => new $foo, use a factory, etc... but I'm wondering if there's a particular reason that the default behavior is not really the opposite, i.e. unless explicitly told to give it NULL, try to construct those dependencies.

I realize these are not "dependencies" in a proper sense, as being optional implies the code is not dependent on them, but that they might provide some additional benefit (like a caching class that, when passed might make things faster), vs. if not passed everything is regenerated all the time.

That said, if my caching class can be built and passed automatically, why wouldn't I care to have it?

Post Instantiation Callbacks

It would be ideal if we could register post-instantiation callbacks based on interface or trait name. If my understanding is correct, the current methodology is essentially to fix interfaces to implementations, but one always needs a concrete implementation.

Could we get an API addition to add something similar to delegate... but for the interfaces/traits?

I can think of one really good example of this where I'd have a shared EventManager and would like any objects that use the emitter trait to receive the emitter based on a trait method (not their constructor) like:

class EmittableObject {
    use EmitterTrait;
    ...
}

Then when I instantiate those badboys, something like:

$injector->prepare('EmitterTrait', function($object, $injector) {
      $object->addEventManager($injector->make('EventManager'));
});

Does this make sense?

Or even using execute internally without having the explicitly make it.

Injection definition required for non-concrete parameter $interface of type Http\Request

Hey, I get that error when I had this code:

$injector->alias('Http\Response', 'Http\HttpResponse');
$injector->share('Http\HttpRequest');
$injector->define('Http\HttpRequest', [
':get' => $_GET,
':post' => $_POST,
':cookies' => $_COOKIE,
':files' => $_FILES,
':server' => $_SERVER,
]);

$injector->alias('Http\Request', 'Http\HttpRequest');
$injector->share('Http\HttpResponse');

However, when I use an old version of Auryn, it works perfectly fine! I bet this is a bug.

Custom definitions not consistent with other defines.

These two blocks of code should produce the same object shouldn't they?

$injector->defineParam('em', $entityManager);
$testClass = $injector->make("InjectTest");
$testClass = $injector->make("InjectTest", [':em' => $entityManager]);

The second one doesn't act the same. The custom definition doesn't get passed through when making the dependencies of InjectTest, and so the error:

'No definition available while attempting to provision typeless non-concrete parameter em'

is generated.

Pass delegates call context information to allow dynamic instantiation in different scenarios

Delegates cannot currently do much in terms of dynamic instantiation -- they're essentially specified once and will always return the same instances. By passing them information about the context in which they are being invoked delegates will be better equipped to return instances dynamically based on what's needed at that specific time and by the context requesting an object.

lifecycle support

I used Auryn for the first time in a project. Since I have already used the DI/IoC concept in other langues (e.g. java) the question came up if Auryn supports something like lifecycles for the objects it provides.

What I found so far is share which makes a object behave like a singleton.
In java you can have things like a session-scoped object, so the DI-Container will return always the same object based on the http session. So no matter when you make a object, you get the very same object for the current user, even across severall http requests.

There are even more scopes, so for example you can define that the DI-Container will return the very same object as long as you stay within a certain wizzard. As soon as you leave the wizzard and later on come back to it, you get a fresh copy of your required 'wizzard-scoped' object. this object is automatically destroyed when you leave the wizzard.

This are only some samples, but maybe it is something a future version of Auryn could provide?

Delegated factory not being passed class name parameter.

The documentation for delegates says:

NOTE: Auryn\Provider will pass the class name to be instantiated to the specified callable as its lone argument. This doesn't seem to be working.

Test is as follows.

class DelegationInvokeTestFactory {

    function __invoke($class){
        $numberOfArgs = func_num_args();

        if ($numberOfArgs == 0) {
            throw new \Exception("Error in delegation, __invoke is being called with no parameters, so factory can't see class requested.");    
        }

        if ($class === null) {
            throw new \Exception("Error in delegation, __invoke called with null parameter instead of class name.");
        }

        return new DelegationInvokeTest();
    }
}

function testInvokeDelegationParameters() {
    $injector = new Auryn\Provider(new Auryn\ReflectionPool);
    $injector->delegate('DelegationInvokeTest', 'DelegationInvokeTestFactory');
    $testObject = $injector->make('DelegationInvokeTest');
}

Expected behaviour

No exception

Actual behaviour

'Error in delegation, __invoke called with null parameter instead of class name.'

Implementing container-interop interface

I'm using Auryn together with XStatic as a way to get something similar to Laravel Facades.
The problem is XStatic requires an instance of a Service Container that implements this Container Interface already being used by other Dependency Injector Containers.

It only requires implementing these two methods in the Provider (which you already has, but with other names).

public function get($id)
{
    return $this->make($id);
}

public function has($id)
{
    return $this->isDefined($id);
}

I'm currently extending Auryn\Provider so it is not much of a hassle, just wondering if it is wort the consideration.

Sharing aliased interfaces by class name fails.

I think the tests below should pass - two don't. Hopefully it's an easy fix but there is a workaround to share instance via the first instance created rather than via the interface name.

interface SharedAliasedInterface {
    function foo();
}

class SharedClass implements SharedAliasedInterface {    
    function foo(){}
}

class ClassWithAliasAsParameter {

    private $sharedClass;

    function    __construct(SharedClass $sharedClass) {
        $this->sharedClass = $sharedClass;
    }

    function getSharedClass() {
        return $this->sharedClass;
    }
}

public function testSharedByAliasedInterfaceName() {
    $provider = new Auryn\Provider();
    $provider->alias('SharedAliasedInterface', 'SharedClass');
    $provider->share('SharedAliasedInterface');
    $class = $provider->make('SharedAliasedInterface');
    $class2 = $provider->make('SharedAliasedInterface');
    $this->assertSame($class, $class2);
}

public function testSharedByAliasedInterfaceNameWithParameter() {
    $provider = new Auryn\Provider();
    $provider->alias('SharedAliasedInterface', 'SharedClass');
    $provider->share('SharedAliasedInterface');
    $sharedClass = $provider->make('SharedAliasedInterface');
    $childClass = $provider->make('ClassWithAliasAsParameter');
    $this->assertSame($sharedClass, $childClass->getSharedClass());
}

public function testSharedByAliasedInstance() {
    $provider = new Auryn\Provider();
    $provider->alias('SharedAliasedInterface', 'SharedClass');
    $sharedClass = $provider->make('SharedAliasedInterface');
    $provider->share($sharedClass);
    $childClass = $provider->make('ClassWithAliasAsParameter');
    $this->assertSame($sharedClass, $childClass->getSharedClass());
}

cheers
Dan

Using a factory that creates objects with different constructor parameter typehints

I have a factory class that can instantiate two differing objects, each with different constructor parameters.

I have abstracted out an event (action) to be one of a few types. This list could be added to in the future.

- Torrent
- Notification

These inherit common methods from an Action abstract class, which implements ActionInterface. Hierarchy:

interface ActionInterface { }

abstract class Action implements ActionInterface { }

class Torrent extends Action 
{ 
    public function __construct(TorrentDataProvider $provider, $var)
    {
        $this->provider = $provider;
        $this->var = $var;
    }
}

class Notification extends Action 
{
    public function __construct(NotificationRepository $repo, $var2, $var3)
    {
        $this->repo = $repo;
        $this->var2 = $var2;
        $this->var3 = $var3;
    } 
}

The relevant action object should only be instantiated when needed (after verification of some other stuff - userId, unique token etc). Therefore, it shall be created via an ActionProvider (a factory class).

class ActionProvider implements ActionProviderInterface
{
    public function build($type)
    {
        switch ($type) 
        {
            // Is it a torrent?
            case (Action::TYPE_TORRENT):
                $action = new Torrent;
                $action->setSomeValue("a value");
            break;
            // Is it a notification?
            case (Action::TYPE_NOTIFICATION):
                $action = new Notification;
                $action->setAnotherValueDifferently("a value")->andAnother("-.-");
            break;
            // Or has some other bollocks been passed in?
            default:
                throw new Exception\AreYouRetardedException("Yes");
            break;
        }
    }
}

As you can see, the Torrent action object and Notification action object have different constructor parameters. They still extend the Action abstract class, because in the context I am using them, this makes sense.

How can I use Auryn to say:

  1. Here's a factory capable of creating objects from the ActionInterface (abstract class = "Action")
  2. I am now defining the Action to be created will be of type Notification
  3. When I call ActionFactory::build($type), please instantiate the right object with all it's relevant dependencies

Cyclic dependency exception handling.

Hi guys,

As I mentioned in the other issue, I'm not a fan of re-throwing errors. I've refactored it in my fork as that already had some changes that made the refactor easier.

Before I make the same changes based on rdlowrey's main branch please can you give me feedback on whether you think it's acceptable or whether you hate it, as there's no point me doing it on the main fork if it's not going to be accepted.

The short version of the changes are:

  • Provider::make() calls makeInternal() so that there is a single entry point.
  • On each call to makeInternal() the class name that is being constructed gets pushed onto the array chainClassConstructors.
  • No InjectionExceptions are caught within the Auryn code.
  • When InjectionException is thrown it can take the chainClassConstructors as a parameter and add that to the exception message.

The result is that the cyclic dependency exception message looks like:

Detected a cyclic dependency while provisioning class RecursiveClassA. Constructor chain is DependsOnCyclic->RecursiveClassA->RecursiveClassB->RecursiveClassC->RecursiveClassA

i.e. it tells you which class causing the actual problem, as well as how that class was reached including the original class that you called make on.

The reason I prefer it like this, is so that you can see the whole trace of calls in one go without having to call $e->getPrevious().

Thoughts?

cheers
Dan

ProviderBuilder -> InjectorBuilder name change

I think InjectorBuilder would be a more appropriate name than ProviderBuilder because the builder uses the public Injector API and nothing specific to the Provider itself. Unless anyone raises valid concerns in the interim, the next tagged release will contain this change.

/cc @ascii-soup

Dependency with default is never instantiated

Currently when a class has a constructor with a typehint on one of the params, and that param also has a default value Auryn will never create the dependency.

class DependencyClass {}

class DependencyClassHasDefault {
    public $dependencyClass;
    function __construct(DependencyClass $dependencyClass = null) {
        $this->dependencyClass = $dependencyClass;
    }
}


interface DependencyInterface {}

class DependencyInterfaceHasDefault {
    public $dependencyInterface;
    function __construct(DependencyInterface $dependencyInterface = null) {
        $this->dependencyInterface = $dependencyInterface;
    }
}

e.g. calling $obj = $injector->make('DependencyClassHasDefault'); will always result in $obj->dependencyClass being null, even though the dependency is a concrete class and so should be instantiable.

I believe the correct behaviour should be:

    public function testClassDependencyWithTypeHintAndDefault() {
        $provider = new Provider();
        $obj = $provider->make('DependencyClassHasDefault');
        $this->assertInstanceOf('DependencyClass', $obj->dependencyClass);
    }

    public function testInterfaceDependencyWithDefault() {
        $provider = new Provider();
        $obj = $provider->make('DependencyInterfaceHasDefault');
        $this->assertNull($obj->dependencyInterface);
    }

This is fixed in Danack/Auryn@0f59695 However the code is very ugly, and there may be a better way.

Or, we can just point and laugh at people who have 'optional' dependencies.

Namespace aliases

When you use something like this inside a class

use Monolog\Logger as Monolog;

class Something{
    public function __construct(Monolog $logger){}
}

How does Auryn inject the Monolog instance? Does it instantiate Monolog\Logger? Or can I delegate the instantiation of "Monolog"?

Therefore should I be using "define" parameters with "Monolog\Logger" or "Monolog"?

$injector->delegate('Monolog\Logger')
//or
$injector->delegate('Monolog')

Add ability to define parameters by name for all classes.

Currently you can only define scalar values for injection on a per class basis. That is fine until you need to start defining the scalar values for dependencies, where:

i) That may require many 'defines' to be setup for each class that needs that define e.g. if many classes need to have the same value set for a $tmpDirectory parameter.

ii) You have to setup the values for every possible class that may require that scalar value, unless you can know which dependencies can be created.

So, should it be possible to call $provider->defineParam('thumbnailSize', 128); and then whenever a parameter called 'thumbnailSize' needs to be injected, and is not otherwise defined, used the defined value.

Reasons For

  • It's useful - I'm currently finding myself having to define things that should be a simple scalar as a full on class where the scalar value is just wrapped.
  • It will probably help people who are migrating to Auryn.

Reasons Against

  • It can probably be abused.
  • I'm not sure the names I've used for the implementation are very good, which usually implies that the idea is not very good.
  • It's probably best practice to wrap config settings into classes for all but the most simple ones e.g. this is bad:
class DBConnection {
    function __construct($username, $password, $host, $port, $socket) {...}
}

this is better:

abstract class DBConfig { }

class DevDBConfig extends DBConfig {
    function __construct($username, $password, $host, $port, $socket) { ...}
}

class DBConnection {
   function __construct(DBConfig $DBConfig){...}
}


$provider->alias('DBConfig', 'DevDBConfig');

However that is annoying to do when it's a single scalar value that is required.

  • It will lead to surprising bugs when you rename parameter names.

btw I don't think this should apply to type-hinted variables that lack an alias or shared implementation - it should only be for un-typehinted values to limit the 'surprise' factor.

Example implementation

Is at - https://github.com/Danack/Auryn/compare/typelessDefines?expand=1

It's pretty trivial, and would have been less code, except 'buildArgumentFromTypeHint' already had some stuff not related to TypeHinting in it which I've moved out.

Specifying existing instances on the fly

Docs describe it is possible to define an injection by specifingy a pre-existing instance instead of the string class name:

$injector->define('MyClass', [':dependency' => $dependencyInstance]);

However, make only allows to override definition by using a different class name and not a given instance:

$myObj = $injector->make('MyClass', ['dependency' => 'SomeImplementation']);

If I try to pass it an instance it throws:

Warning: strtolower() expects parameter 1 to be string, object given in /xxxxx/vendor/rdlowrey/auryn/lib/Provider.php on line 752

Is this allowed or I'm just doing it wrong?

Auto-wiring dependencies for setter injection

As the title explains, would it be possible to include this capability in here, so that when Auryn performs it's reflection to read constructor signatures, could the reflection also be performed for methods within the class?

Obviously this would increase overheard significantly, but if everything's cached - that shouldn't be an issue. I'm going to take a look at the source over the course of this week but, what are your opinions on this @rdlowrey?

Edge case for sharing and alias.

Is the behaviour for the below defined aka can you tell me what should happen without inspecting the code:

$provider = new Auryn\Provider();
$testClass = new TestSharingClass();
$provider->alias('TestSharingClass', 'AliasedTestSharingClass');
$provider->share($testClass);
$instance = $provider->make('TestSharingClass');

Is $instance meant to be of type TestSharingClass or AliasedTestSharingClass?

I'm guessing that it might be better just to throw an error somewhere if you try to share something that is aliased or vice-versa, to avoid the result of the provider being dependent on the order in which it was sent commands.

If that's the case I can have a look at implementing it once it's agreed.

Any reason I cannot use :password when defining PDO?

For some reason

$injector->share('PDO');
$injector->define('PDO', [
    ':dsn' => 'mysql:dbname=' . getenv('DB_DATABASE') . ';host=' . getenv('DB_HOST'),
    ':username' => getenv('DB_USERNAME'),
    ':passwd' => 'root',
    ':options' => []
]);

does work, but not:

$injector->share('PDO');
$injector->define('PDO', [
    ':dsn' => 'mysql:dbname=' . getenv('DB_DATABASE') . ';host=' . getenv('DB_HOST'),
    ':username' => getenv('DB_USERNAME'),
    ':password' => 'root',
    ':options' => []
]);

Why?

Configuration writer for production

With the new file-based configuration options becoming available, it might be nice to add a feature for development that uses the standard reflection-based auto-wiring and then writes out the equivalent configuration for increased performance in production (and for actual-dependency discovery purposes).

Argument definitions are not passed to delegated factories

I modified a test class to test something else and discovered that argument definitions are not being passed to delegated functions.

interface DelegatableInterface {
    function foo();
}

class ImplementsInterface implements DelegatableInterface{
    function foo(){
    }
}

class ImplementsInterfaceFactory{
    public function __invoke($arg1) {
        return new ImplementsInterface();
    }
}
   public function testInterfaceFactoryDelegation() {
        $injector = new Auryn\Provider(new Auryn\ReflectionPool);
        $injector->define('ImplementsInterfaceFactory', array(':arg1' => 'First argument'));
        $injector->delegate('DelegatableInterface', 'ImplementsInterfaceFactory');
        $requiresDelegatedInterface = $injector->make('RequiresDelegatedInterface');
        $requiresDelegatedInterface->foo();
    }

I think this should work, but it doesn't because the selectDefinition() function isn't called for delegated creation. https://github.com/rdlowrey/Auryn/blob/master/src/Auryn/Provider.php#L105

Aliasing concrete class

I'm not sure if aliasing a concrete class to another concrete class is meant to work or not:

class ConcreteClass1{}

class ConcreteClass2{}

function testAliasingConcreteClasses(){
    $provider = new Auryn\Provider();
    $provider->alias('ConcreteClass1', 'ConcreteClass2');
    $class = $provider->make('ConcreteClass1');
    $this->assertEquals('ConcreteClass2', get_class($class));
}

Expected :ConcreteClass2
Actual :ConcreteClass1

Allow for one-stop text file and/or PHP array configuration

Something I'd like to add is the ability to use a one-stop master configuration file allowing users and applications to eliminate all the manual calls to define(), alias(), share(), etc. This should be a format-agnostic option. By that I mean you should be able to specify the configuration in JSON/YAML/native PHP or whatever.

You'd have a configuration parser that would, based on the filename extension, parse the contents into a PHP array that looks something like this:

<?php
$injectorConfig = array(
    'aliases' => array(
        'SomeInterface' => 'SomeImplementationClass',
        'SomeAbstract' => 'OtherImplementationClass',
    ),
    'definitions' => array(
        'SomeImplementationClass' => array(
            ':scalarArg' => 42
        ),
        'MyUniversalConfigClass' => array(
            ':configFile' => '/path/to/myappconfig.php'
        )
    ),
    'shares' => array(
        'MyUniversalConfigClass',
        'MyDatabaseConnectionPoolClass'
    )
);

With this array in hand the configurator could then generate and pre-populate an injector for you and obviate the need to manually define/alias/share everything in your application.

PDO optional argument $options must be provisioned

In the following example, if the options provision is removed it will result in a exception.
According to the docs the options argument should be optional.

anyFile.php, global scope

$injector = new Auryn\Provider;
  $injector->define('PDO', [
  ':dsn' => '...',
  ':username' => '...',
  ':passwd' => '...',
  ':options' => null // removed this line for exception
]);   

SomeRepo.php

namespace Example\SomeRepo; 
class SomeRepository {
        protected $pdo;
        public function __construct(\PDO $pdo){}
}

Exception from not definining 'options'

Uncaught exception 'Auryn\InjectionException' with message
'No definition available while attempting to provision typeless 
non-concrete parameter PDO(options)' in rdlowrey/auryn/lib/Provider.php:599

`

Executing aliased class executes wrong class

The test classes and test method probably explain this adequately:

class BaseExecutableClass {
    function foo() {
        return 'This is the BaseExecutableClass';
    }
}

class ExtendsExecutableClass extends BaseExecutableClass {
    function foo() {
        return 'This is the ExtendsExecutableClass';
    }
}


    public function testExecutableAliasing() {
        $provider = new Provider();
        $provider->alias('BaseExecutableClass', 'ExtendsExecutableClass');
        $result = $provider->execute(['BaseExecutableClass', 'foo']);
        $this->assertEquals('This is the ExtendsExecutableClass', $result);
    }

Fixed in Danack/Auryn@e14db9a but that is post PR-64

api docs and code quality

Unit tests are fine, but what about comments? When i e.g. run phpDocumentor2 there is not much to read. Your readme.md examples are nice, but a good API reference is what i would prefer in the long run.

In addition the directory structure seems weak. No separate directories for e.g. exceptions or reflection cache drivers?

Besides that i can't find any information on the coding rules and standards used for Auryn. That makes it hard to run tools like CodeSniffer on it.

Don't get me wrong, Auryn seems powerful and covers the right way to manage (static OOP language) dependencies in my opinion, but when i use external libraries for my private projects they have to meet some quality measures. I won't force a certain coding rule for example. I just make sure that something like that is used at all.

Keep up the good work :)

Use default param if available for non-concrete parameters

Currently the Auryn\Provider will error out for non-concrete typehints without a definition even if the argument in question has a default parameters. Instead, injectors should pass in the default parameter if no implementation or alias exists for the parameter.

Instantiating type-hinted parameters with different names

Sometimes you need to have two different instances of the same type of object injected into an object e.g.

class RedisSyncCheck {

    function __construct(RedisClient $sourceClient, RedisClient $targetClient) {
        //....
    }

    function summariseMissingKeys() {
         //A function that analyses stuff in the sourceClient compared to the targetClient
    } 
}

Currently there is no way for Auryn to support injecting different type-hinted parameters into the same class.

This is probably related to #35 - which is also about allowing more customisation of what gets injected based on the details of what is asking for the parameter.

buildArgumentFromTypeHint defaults to null rather than exception.

Hi,

Is there any reason why the code for buildArgumentFromTypeHint returns null rather than throwing an exception when a required parameter is missing?

https://github.com/rdlowrey/Auryn/blob/master/src/Auryn/Provider.php#L575

Unless there's a cunning reason behind it, it would seem better to throw a InjectionException to fail fast rather than allowing something to break later.

AKA I just forgot to set the defines for something and had to debug through the code to figure out the error, rather than having a nice Exception tell me straight away.

cheers
Dan

Support text config files

Support the specification of dependencies in a text configuration file.

For example:

OLD

<?php
$injector = new Auryn\Injector;
$injector->define('PhpAmqpLib\Connection\AMQPStreamConnection', ['localhost', 5672, 'guest', 'guest']);
$obj = $injector->make('ClassDependingOnAMQPStreamConnection');

NEW

You could define this in yaml/XML/whatever:

# wiring.yaml
wiring:
    - { class: PhpAmqpLib\Connection\AMQPStreamConnection, params: [localhost, '5672', guest, guest] }
<?php
$injector = new Auryn\Injector;
$injector->parseWiring(file_get_contents('wiring.yaml'));
$obj = $injector->make('ClassDependingOnAMQPStreamConnection');

getDeclaringClass() not safe to use

When you call Injector->execute on a function, and it requires a non-typehinted param which has not been defined, the error message is generated with the code:

$declaringClass = $param->getDeclaringClass()->getName();
            throw new InjectionException(
                sprintf($this->errorMessages[self::E_UNDEFINED_PARAM], $declaringClass,$param->name),
                self::E_UNDEFINED_PARAM
            );

This can be replicated with the code:

$provider = new \Auryn\Provider();
$provider->execute('functionMissingParamDefine');

exit(0);

$param->getDeclaringClass() does not always work as you would expect. If execute is called from within another class it returns that classes name. e.g. if you put the above code in a test case inside ProviderTest, it returns ProviderTest.

I encountered this by as it returns null when the execute is called from outside a class.

I will fix this when I've re-merged my code with Levi's ninja reorganisation.

buildNonConcreteImplementation is unreachable

Hi,

The function buildNonConcreteImplementation appears to be unreachable.

It is only called in one place, and the functions that call it are all called from make($className, array $customDefinition = array()) but right at that top level, the classname is converted to either an alias, shared or delegated value, and so it doesn't appear to be possible for buildNonConcreteImplementation to be called.

Removing it, and removing the else block that it's called from doesn't affect the tests, which is not surprising as that line is not covered byt the tests.

cheers
Dan

Ability to define different aliases for an interface?

@rdlowrey The other day you asked what features I think are still needed - here is one, which we've discussed this before, but I can't find a link.


Sometimes you need to be able to use different implementations for the same interface, based on external factors.

interface Logger{}

class UtilityClass {
    function __construct(Logger $logger){}
}

class ClassThatIsWorkingCorrectly{
    function __construct(UtilityClass $utilityClass) {}
}

class ClassRelatedToReportedBugs{
    function __construct(UtilityClass $utilityClass) {}
}

Most of our code is working fine and so we want the 'Logger' to be inserted to be one that only reports notices at the 'error' level.

However some of our users have reported bugs that we think is being caused by the code in ClassRelatedToReportedBugs, or code that it calls. We want to be able to configure our system so that any 'Logger' created by 'ClassRelatedToReportedBugs' or any of it's dependencies uses a Logger that reports notices at the 'info' level.

I think this does need to be configurable through configuration rather than modifying the code, as that the changes need to be done on a production server without downtime.

For the above case, my fork with it's (marginally crazy) ability to alias/share different classes based on the chain of class constructors solves that problem.

However there are probably other cases where some other solution would be needed e.g.

// Copies data from one server to another.
function archiveData(DBConnection $liveServer, DBConnection $backupServer){
}

Wouldn't currently be possible would it?

Obviously not every possible way of configuring which classes are instantiated needs to be supported, or should even if it is possible, however I think the first case above is something that most people would expect to be able to configure.

New versions?

Any chance for a new tag soon. It seems the last stable release was back in Oct 2014 (unless I'm missing something). I've had some longer projects (months) which are going to be going stable soon and been using dev, but I don't want to push them to prod on dev-master, is there a roadmap for a 1.0?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.