first commit

This commit is contained in:
2023-09-12 21:41:04 +02:00
commit 3361a7f053
13284 changed files with 2116755 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-2014 Daniel Lowrey, Levi Morrison, Dan Ackroyd
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

View File

@@ -0,0 +1,847 @@
# auryn [![Build Status](https://travis-ci.org/rdlowrey/auryn.svg?branch=master)](https://travis-ci.org/rdlowrey/auryn)
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**
* [Basic Instantiation](#basic-instantiation)
* [Injection Definitions](#injection-definitions)
* [Type-Hint Aliasing](#type-hint-aliasing)
* [Non-Class Parameters](#non-class-parameters)
* [Global Parameter Definitions](#global-parameter-definitions)
**Advanced Usage**
* [Instance Sharing](#instance-sharing)
* [Instantiation Delegates](#instantiation-delegates)
* [Prepares and Setter Injection](#prepares-and-setter-injection)
* [Injecting for Execution](#injecting-for-execution)
* [Dependency Resolution](#dependency-resolution)
**Example Use Cases**
* [Avoiding Evil Singletons](#avoiding-evil-singletons)
* [Application Bootstrapping](#app-bootstrapping)
## 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:
```bash
$ 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:
```bash
composer require rdlowrey/auryn
```
##### Manual Download
Archived tagged release versions are also available for manual download on the project
[tags page](https://github.com/rdlowrey/auryn/tags)
## Basic Usage
To start using the injector, simply create a new instance of the `Auryn\Injector` ("the Injector")
class:
```php
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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](http://php.net/manual/en/language.types.callable.php).
The following examples all work:
```php
<?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
<?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
<?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
<?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
<?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
<?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
<?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.

View File

@@ -0,0 +1,37 @@
<?php
require __DIR__ . "/../vendor/autoload.php";
class A {
private $a;
private $b;
public function __construct(stdClass $a, stdClass $b) {
$this->a = $a;
$this->b = $b;
}
public function print() {
print $this->a->foo;
print $this->b->foo;
print PHP_EOL;
}
}
$injector = new WPML\Auryn\Injector;
$injector->define(A::class, [
"+a" => function () {
$std = new stdClass;
$std->foo = "foo";
return $std;
},
"+b" => function () {
$std = new stdClass;
$std->foo = "bar";
return $std;
},
]);
$a = $injector->make(A::class);
$a->print();

View File

@@ -0,0 +1,24 @@
<?php
use Auryn\Injector;
require __DIR__ . "/../vendor/autoload.php";
$injector = new Injector;
class A {
public $std;
public function __construct(stdClass $std) {
$this->std = $std;
}
}
$stdClass = new stdClass;
$stdClass->foo = "foobar";
$a = $injector->make(A::class, [
":std" => $stdClass,
]);
print $a->std->foo . PHP_EOL;

View File

@@ -0,0 +1,108 @@
<?php
namespace WPML\Auryn;
class CachingReflector implements Reflector
{
const CACHE_KEY_CLASSES = 'auryn.refls.classes.';
const CACHE_KEY_CTORS = 'auryn.refls.ctors.';
const CACHE_KEY_CTOR_PARAMS = 'auryn.refls.ctor-params.';
const CACHE_KEY_FUNCS = 'auryn.refls.funcs.';
const CACHE_KEY_METHODS = 'auryn.refls.methods.';
private $reflector;
private $cache;
public function __construct(Reflector $reflector = null, ReflectionCache $cache = null)
{
$this->reflector = $reflector ?: new StandardReflector;
$this->cache = $cache ?: new ReflectionCacheArray;
}
public function getClass($class)
{
$cacheKey = self::CACHE_KEY_CLASSES . strtolower($class);
if (($reflectionClass = $this->cache->fetch($cacheKey)) === false) {
$this->cache->store($cacheKey, $reflectionClass = $this->reflector->getClass($class));
}
return $reflectionClass;
}
public function getCtor($class)
{
$cacheKey = self::CACHE_KEY_CTORS . strtolower($class);
if (($reflectedCtor = $this->cache->fetch($cacheKey)) === false) {
$this->cache->store($cacheKey, $reflectedCtor = $this->reflector->getCtor($class));
}
return $reflectedCtor;
}
public function getCtorParams($class)
{
$cacheKey = self::CACHE_KEY_CTOR_PARAMS . strtolower($class);
if (($reflectedCtorParams = $this->cache->fetch($cacheKey)) === false) {
$this->cache->store($cacheKey, $reflectedCtorParams = $this->reflector->getCtorParams($class));
}
return $reflectedCtorParams;
}
public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param)
{
$lowParam = strtolower($param->name);
if ($function instanceof \ReflectionMethod) {
$lowClass = strtolower($function->class);
$lowMethod = strtolower($function->name);
$paramCacheKey = self::CACHE_KEY_CLASSES . "{$lowClass}.{$lowMethod}.param-{$lowParam}";
} else {
$lowFunc = strtolower($function->name);
$paramCacheKey = (strpos($lowFunc, '{closure}') === false)
? self::CACHE_KEY_FUNCS . ".{$lowFunc}.param-{$lowParam}"
: null;
}
$typeHint = ($paramCacheKey === null) ? false : $this->cache->fetch($paramCacheKey);
if (false === $typeHint) {
$typeHint = $this->reflector->getParamTypeHint($function, $param);
if ($paramCacheKey !== null) {
$this->cache->store($paramCacheKey, $typeHint);
}
}
return $typeHint;
}
public function getFunction($functionName)
{
$lowFunc = strtolower($functionName);
$cacheKey = self::CACHE_KEY_FUNCS . $lowFunc;
if (($reflectedFunc = $this->cache->fetch($cacheKey)) === false) {
$this->cache->store($cacheKey, $reflectedFunc = $this->reflector->getFunction($functionName));
}
return $reflectedFunc;
}
public function getMethod($classNameOrInstance, $methodName)
{
$className = is_string($classNameOrInstance)
? $classNameOrInstance
: get_class($classNameOrInstance);
$cacheKey = self::CACHE_KEY_METHODS . strtolower($className) . '.' . strtolower($methodName);
if (($reflectedMethod = $this->cache->fetch($cacheKey)) === false) {
$this->cache->store($cacheKey, $reflectedMethod = $this->reflector->getMethod($classNameOrInstance, $methodName));
}
return $reflectedMethod;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace WPML\Auryn;
class ConfigException extends InjectorException
{
}

View File

@@ -0,0 +1,82 @@
<?php
namespace WPML\Auryn;
class Executable
{
private $callableReflection;
private $invocationObject;
private $isInstanceMethod;
public function __construct(\ReflectionFunctionAbstract $reflFunc, $invocationObject = null)
{
if ($reflFunc instanceof \ReflectionMethod) {
$this->isInstanceMethod = true;
$this->setMethodCallable($reflFunc, $invocationObject);
} else {
$this->isInstanceMethod = false;
$this->callableReflection = $reflFunc;
}
}
private function setMethodCallable(\ReflectionMethod $reflection, $invocationObject)
{
if (is_object($invocationObject)) {
$this->callableReflection = $reflection;
$this->invocationObject = $invocationObject;
} elseif ($reflection->isStatic()) {
$this->callableReflection = $reflection;
} else {
throw new \InvalidArgumentException(
'ReflectionMethod callables must specify an invocation object'
);
}
}
public function __invoke()
{
$args = func_get_args();
$reflection = $this->callableReflection;
if ($this->isInstanceMethod) {
return $reflection->invokeArgs($this->invocationObject, $args);
}
return $this->callableReflection->isClosure()
? $this->invokeClosureCompat($reflection, $args)
: $reflection->invokeArgs($args);
}
/**
* @TODO Remove this extra indirection when 5.3 support is dropped
*/
private function invokeClosureCompat($reflection, $args)
{
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
$scope = $reflection->getClosureScopeClass();
$closure = \Closure::bind(
$reflection->getClosure(),
$reflection->getClosureThis(),
$scope ? $scope->name : null
);
return call_user_func_array($closure, $args);
} else {
return $reflection->invokeArgs($args);
}
}
public function getCallableReflection()
{
return $this->callableReflection;
}
public function getInvocationObject()
{
return $this->invocationObject;
}
public function isInstanceMethod()
{
return $this->isInstanceMethod;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace WPML\Auryn;
class InjectionException extends InjectorException
{
public $dependencyChain;
public function __construct(array $inProgressMakes, $message = "", $code = 0, \Exception $previous = null)
{
$this->dependencyChain = array_flip($inProgressMakes);
ksort($this->dependencyChain);
parent::__construct($message, $code, $previous);
}
/**
* Add a human readable version of the invalid callable to the standard 'invalid invokable' message.
*/
public static function fromInvalidCallable(
array $inProgressMakes,
$callableOrMethodStr,
\Exception $previous = null
) {
$callableString = null;
if (is_string($callableOrMethodStr)) {
$callableString .= $callableOrMethodStr;
} else if (is_array($callableOrMethodStr) &&
array_key_exists(0, $callableOrMethodStr) &&
array_key_exists(0, $callableOrMethodStr)) {
if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) {
$callableString .= $callableOrMethodStr[0].'::'.$callableOrMethodStr[1];
} else if (is_object($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) {
$callableString .= sprintf(
"[object(%s), '%s']",
get_class($callableOrMethodStr[0]),
$callableOrMethodStr[1]
);
}
}
if ($callableString) {
// Prevent accidental usage of long strings from filling logs.
$callableString = substr($callableString, 0, 250);
$message = sprintf(
"%s. Invalid callable was '%s'",
Injector::M_INVOKABLE,
$callableString
);
} else {
$message = Injector::M_INVOKABLE;
}
return new self($inProgressMakes, $message, Injector::E_INVOKABLE, $previous);
}
/**
* Returns the hierarchy of dependencies that were being created when
* the exception occurred.
* @return array
*/
public function getDependencyChain()
{
return $this->dependencyChain;
}
}

View File

@@ -0,0 +1,757 @@
<?php
namespace WPML\Auryn;
class Injector
{
const A_RAW = ':';
const A_DELEGATE = '+';
const A_DEFINE = '@';
const I_BINDINGS = 1;
const I_DELEGATES = 2;
const I_PREPARES = 4;
const I_ALIASES = 8;
const I_SHARES = 16;
const I_ALL = 31;
const E_NON_EMPTY_STRING_ALIAS = 1;
const M_NON_EMPTY_STRING_ALIAS = "Invalid alias: non-empty string required at arguments 1 and 2";
const E_SHARED_CANNOT_ALIAS = 2;
const M_SHARED_CANNOT_ALIAS = "Cannot alias class %s to %s because it is currently shared";
const E_SHARE_ARGUMENT = 3;
const M_SHARE_ARGUMENT = "%s::share() requires a string class name or object instance at Argument 1; %s specified";
const E_ALIASED_CANNOT_SHARE = 4;
const M_ALIASED_CANNOT_SHARE = "Cannot share class %s because it is currently aliased to %s";
const E_INVOKABLE = 5;
const M_INVOKABLE = "Invalid invokable: callable or provisional string required";
const E_NON_PUBLIC_CONSTRUCTOR = 6;
const M_NON_PUBLIC_CONSTRUCTOR = "Cannot instantiate protected/private constructor in class %s";
const E_NEEDS_DEFINITION = 7;
const M_NEEDS_DEFINITION = "Injection definition required for %s %s";
const E_MAKE_FAILURE = 8;
const M_MAKE_FAILURE = "Could not make %s: %s";
const E_UNDEFINED_PARAM = 9;
const M_UNDEFINED_PARAM = "No definition available to provision typeless parameter \$%s at position %d in %s()%s";
const E_DELEGATE_ARGUMENT = 10;
const M_DELEGATE_ARGUMENT = "%s::delegate expects a valid callable or executable class::method string at Argument 2%s";
const E_CYCLIC_DEPENDENCY = 11;
const M_CYCLIC_DEPENDENCY = "Detected a cyclic dependency while provisioning %s";
const E_MAKING_FAILED = 12;
const M_MAKING_FAILED = "Making %s did not result in an object, instead result is of type '%s'";
private $reflector;
private $classDefinitions = array();
private $paramDefinitions = array();
private $aliases = array();
private $shares = array();
private $prepares = array();
private $delegates = array();
private $inProgressMakes = array();
public function __construct(Reflector $reflector = null)
{
$this->reflector = $reflector ?: new CachingReflector;
}
public function __clone()
{
$this->inProgressMakes = array();
}
/**
* Define instantiation directives for the specified class
*
* @param string $name The class (or alias) whose constructor arguments we wish to define
* @param array $args An array mapping parameter names to values/instructions
* @return self
*/
public function define($name, array $args)
{
list(, $normalizedName) = $this->resolveAlias($name);
$this->classDefinitions[$normalizedName] = $args;
return $this;
}
/**
* Assign a global default value for all parameters named $paramName
*
* Global parameter definitions are only used for parameters with no typehint, pre-defined or
* call-time definition.
*
* @param string $paramName The parameter name for which this value applies
* @param mixed $value The value to inject for this parameter name
* @return self
*/
public function defineParam($paramName, $value)
{
$this->paramDefinitions[$paramName] = $value;
return $this;
}
/**
* Define an alias for all occurrences of a given typehint
*
* Use this method to specify implementation classes for interface and abstract class typehints.
*
* @param string $original The typehint to replace
* @param string $alias The implementation name
* @throws ConfigException if any argument is empty or not a string
* @return self
*/
public function alias($original, $alias)
{
if (empty($original) || !is_string($original)) {
throw new ConfigException(
self::M_NON_EMPTY_STRING_ALIAS,
self::E_NON_EMPTY_STRING_ALIAS
);
}
if (empty($alias) || !is_string($alias)) {
throw new ConfigException(
self::M_NON_EMPTY_STRING_ALIAS,
self::E_NON_EMPTY_STRING_ALIAS
);
}
$originalNormalized = $this->normalizeName($original);
if (isset($this->shares[$originalNormalized])) {
throw new ConfigException(
sprintf(
self::M_SHARED_CANNOT_ALIAS,
$this->normalizeName(get_class($this->shares[$originalNormalized])),
$alias
),
self::E_SHARED_CANNOT_ALIAS
);
}
if (array_key_exists($originalNormalized, $this->shares)) {
$aliasNormalized = $this->normalizeName($alias);
$this->shares[$aliasNormalized] = null;
unset($this->shares[$originalNormalized]);
}
$this->aliases[$originalNormalized] = $alias;
return $this;
}
private function normalizeName($className)
{
return ltrim(strtolower($className), '\\');
}
/**
* Share the specified class/instance across the Injector context
*
* @param mixed $nameOrInstance The class or object to share
* @throws ConfigException if $nameOrInstance is not a string or an object
* @return self
*/
public function share($nameOrInstance)
{
if (is_string($nameOrInstance)) {
$this->shareClass($nameOrInstance);
} elseif (is_object($nameOrInstance)) {
$this->shareInstance($nameOrInstance);
} else {
throw new ConfigException(
sprintf(
self::M_SHARE_ARGUMENT,
__CLASS__,
gettype($nameOrInstance)
),
self::E_SHARE_ARGUMENT
);
}
return $this;
}
private function shareClass($nameOrInstance)
{
list(, $normalizedName) = $this->resolveAlias($nameOrInstance);
$this->shares[$normalizedName] = isset($this->shares[$normalizedName])
? $this->shares[$normalizedName]
: null;
}
private function resolveAlias($name)
{
$normalizedName = $this->normalizeName($name);
if (isset($this->aliases[$normalizedName])) {
$name = $this->aliases[$normalizedName];
$normalizedName = $this->normalizeName($name);
}
return array($name, $normalizedName);
}
private function shareInstance($obj)
{
$normalizedName = $this->normalizeName(get_class($obj));
if (isset($this->aliases[$normalizedName])) {
// You cannot share an instance of a class name that is already aliased
throw new ConfigException(
sprintf(
self::M_ALIASED_CANNOT_SHARE,
$normalizedName,
$this->aliases[$normalizedName]
),
self::E_ALIASED_CANNOT_SHARE
);
}
$this->shares[$normalizedName] = $obj;
}
/**
* Register a prepare callable to modify/prepare objects of type $name after instantiation
*
* Any callable or provisionable invokable may be specified. Preparers are passed two
* arguments: the instantiated object to be mutated and the current Injector instance.
*
* @param string $name
* @param mixed $callableOrMethodStr Any callable or provisionable invokable method
* @throws InjectionException if $callableOrMethodStr is not a callable.
* See https://github.com/rdlowrey/auryn#injecting-for-execution
* @return self
*/
public function prepare($name, $callableOrMethodStr)
{
if ($this->isExecutable($callableOrMethodStr) === false) {
throw InjectionException::fromInvalidCallable(
$this->inProgressMakes,
$callableOrMethodStr
);
}
list(, $normalizedName) = $this->resolveAlias($name);
$this->prepares[$normalizedName] = $callableOrMethodStr;
return $this;
}
private function isExecutable($exe)
{
if (is_callable($exe)) {
return true;
}
if (is_string($exe) && method_exists($exe, '__invoke')) {
return true;
}
if (is_array($exe) && isset($exe[0], $exe[1]) && method_exists($exe[0], $exe[1])) {
return true;
}
return false;
}
/**
* Delegate the creation of $name instances to the specified callable
*
* @param string $name
* @param mixed $callableOrMethodStr Any callable or provisionable invokable method
* @throws ConfigException if $callableOrMethodStr is not a callable.
* @return self
*/
public function delegate($name, $callableOrMethodStr)
{
if ($this->isExecutable($callableOrMethodStr) === false) {
$errorDetail = '';
if (is_string($callableOrMethodStr)) {
$errorDetail = " but received '$callableOrMethodStr'";
} elseif (is_array($callableOrMethodStr) &&
count($callableOrMethodStr) === 2 &&
array_key_exists(0, $callableOrMethodStr) &&
array_key_exists(1, $callableOrMethodStr)
) {
if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) {
$errorDetail = " but received ['".$callableOrMethodStr[0]."', '".$callableOrMethodStr[1]."']";
}
}
throw new ConfigException(
sprintf(self::M_DELEGATE_ARGUMENT, __CLASS__, $errorDetail),
self::E_DELEGATE_ARGUMENT
);
}
$normalizedName = $this->normalizeName($name);
$this->delegates[$normalizedName] = $callableOrMethodStr;
return $this;
}
/**
* Retrieve stored data for the specified definition type
*
* Exposes introspection of existing binds/delegates/shares/etc for decoration and composition.
*
* @param string $nameFilter An optional class name filter
* @param int $typeFilter A bitmask of Injector::* type constant flags
* @return array
*/
public function inspect($nameFilter = null, $typeFilter = null)
{
$result = array();
$name = $nameFilter ? $this->normalizeName($nameFilter) : null;
if (empty($typeFilter)) {
$typeFilter = self::I_ALL;
}
$types = array(
self::I_BINDINGS => "classDefinitions",
self::I_DELEGATES => "delegates",
self::I_PREPARES => "prepares",
self::I_ALIASES => "aliases",
self::I_SHARES => "shares"
);
foreach ($types as $type => $source) {
if ($typeFilter & $type) {
$result[$type] = $this->filter($this->{$source}, $name);
}
}
return $result;
}
private function filter($source, $name)
{
if (empty($name)) {
return $source;
} elseif (array_key_exists($name, $source)) {
return array($name => $source[$name]);
} else {
return array();
}
}
/**
* Instantiate/provision a class instance
*
* @param string $name
* @param array $args
* @throws InjectionException if a cyclic gets detected when provisioning
* @return mixed
*/
public function make($name, array $args = array())
{
list($className, $normalizedClass) = $this->resolveAlias($name);
if (isset($this->inProgressMakes[$normalizedClass])) {
throw new InjectionException(
$this->inProgressMakes,
sprintf(
self::M_CYCLIC_DEPENDENCY,
$className
),
self::E_CYCLIC_DEPENDENCY
);
}
$this->inProgressMakes[$normalizedClass] = count($this->inProgressMakes);
// isset() is used specifically here because classes may be marked as "shared" before an
// instance is stored. In these cases the class is "shared," but it has a null value and
// instantiation is needed.
if (isset($this->shares[$normalizedClass])) {
unset($this->inProgressMakes[$normalizedClass]);
return $this->shares[$normalizedClass];
}
try {
if (isset($this->delegates[$normalizedClass])) {
$executable = $this->buildExecutable($this->delegates[$normalizedClass]);
$reflectionFunction = $executable->getCallableReflection();
$args = $this->provisionFuncArgs($reflectionFunction, $args, null, $className);
$obj = call_user_func_array(array($executable, '__invoke'), $args);
} else {
$obj = $this->provisionInstance($className, $normalizedClass, $args);
}
$obj = $this->prepareInstance($obj, $normalizedClass);
if (array_key_exists($normalizedClass, $this->shares)) {
$this->shares[$normalizedClass] = $obj;
}
unset($this->inProgressMakes[$normalizedClass]);
}
catch (\Throwable $exception) {
unset($this->inProgressMakes[$normalizedClass]);
throw $exception;
}
catch (\Exception $exception) {
unset($this->inProgressMakes[$normalizedClass]);
throw $exception;
}
return $obj;
}
private function provisionInstance($className, $normalizedClass, array $definition)
{
try {
$ctor = $this->reflector->getCtor($className);
if (!$ctor) {
$obj = $this->instantiateWithoutCtorParams($className);
} elseif (!$ctor->isPublic()) {
throw new InjectionException(
$this->inProgressMakes,
sprintf(self::M_NON_PUBLIC_CONSTRUCTOR, $className),
self::E_NON_PUBLIC_CONSTRUCTOR
);
} elseif ($ctorParams = $this->reflector->getCtorParams($className)) {
$reflClass = $this->reflector->getClass($className);
$definition = isset($this->classDefinitions[$normalizedClass])
? array_replace($this->classDefinitions[$normalizedClass], $definition)
: $definition;
$args = $this->provisionFuncArgs($ctor, $definition, $ctorParams, $className);
$obj = $reflClass->newInstanceArgs($args);
} else {
$obj = $this->instantiateWithoutCtorParams($className);
}
return $obj;
} catch (\ReflectionException $e) {
throw new InjectionException(
$this->inProgressMakes,
sprintf(self::M_MAKE_FAILURE, $className, $e->getMessage()),
self::E_MAKE_FAILURE,
$e
);
}
}
private function instantiateWithoutCtorParams($className)
{
$reflClass = $this->reflector->getClass($className);
if (!$reflClass->isInstantiable()) {
$type = $reflClass->isInterface() ? 'interface' : 'abstract class';
throw new InjectionException(
$this->inProgressMakes,
sprintf(self::M_NEEDS_DEFINITION, $type, $className),
self::E_NEEDS_DEFINITION
);
}
return new $className;
}
private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array $definition, array $reflParams = null, $className = null)
{
$args = array();
// @TODO store this in ReflectionStorage
if (!isset($reflParams)) {
$reflParams = $reflFunc->getParameters();
}
foreach ($reflParams as $i => $reflParam) {
$name = $reflParam->name;
if (isset($definition[$i]) || array_key_exists($i, $definition)) {
// indexed arguments take precedence over named parameters
$arg = $definition[$i];
} elseif (isset($definition[$name]) || array_key_exists($name, $definition)) {
// interpret the param as a class name to be instantiated
$arg = $this->make($definition[$name]);
} elseif (($prefix = self::A_RAW . $name) && (isset($definition[$prefix]) || array_key_exists($prefix, $definition))) {
// interpret the param as a raw value to be injected
$arg = $definition[$prefix];
} elseif (($prefix = self::A_DELEGATE . $name) && isset($definition[$prefix])) {
// interpret the param as an invokable delegate
$arg = $this->buildArgFromDelegate($name, $definition[$prefix]);
} elseif (($prefix = self::A_DEFINE . $name) && isset($definition[$prefix])) {
// interpret the param as a class definition
$arg = $this->buildArgFromParamDefineArr($definition[$prefix]);
} elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) {
$arg = $this->buildArgFromReflParam($reflParam, $className);
if ($arg === null && PHP_VERSION_ID >= 50600 && $reflParam->isVariadic()) {
// buildArgFromReflParam might return null in case the parameter is optional
// in case of variadics, the parameter is optional, but null might not be allowed
continue;
}
}
$args[] = $arg;
}
return $args;
}
private function buildArgFromParamDefineArr($definition)
{
if (!is_array($definition)) {
throw new InjectionException(
$this->inProgressMakes
// @TODO Add message
);
}
if (!isset($definition[0], $definition[1])) {
throw new InjectionException(
$this->inProgressMakes
// @TODO Add message
);
}
list($class, $definition) = $definition;
return $this->make($class, $definition);
}
private function buildArgFromDelegate($paramName, $callableOrMethodStr)
{
if ($this->isExecutable($callableOrMethodStr) === false) {
throw InjectionException::fromInvalidCallable(
$this->inProgressMakes,
$callableOrMethodStr
);
}
$executable = $this->buildExecutable($callableOrMethodStr);
return $executable($paramName, $this);
}
private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam)
{
$typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam);
if (!$typeHint) {
$obj = null;
} elseif ($reflParam->isDefaultValueAvailable()) {
$normalizedName = $this->normalizeName($typeHint);
// Injector has been told explicitly how to make this type
if (isset($this->aliases[$normalizedName]) ||
isset($this->delegates[$normalizedName]) ||
isset($this->shares[$normalizedName])) {
$obj = $this->make($typeHint);
} else {
$obj = $reflParam->getDefaultValue();
}
} else {
$obj = $this->make($typeHint);
}
return $obj;
}
private function buildArgFromReflParam(\ReflectionParameter $reflParam, $className = null)
{
if (array_key_exists($reflParam->name, $this->paramDefinitions)) {
$arg = $this->paramDefinitions[$reflParam->name];
} elseif ($reflParam->isDefaultValueAvailable()) {
$arg = $reflParam->getDefaultValue();
} elseif ($reflParam->isOptional()) {
// This branch is required to work around PHP bugs where a parameter is optional
// but has no default value available through reflection. Specifically, PDO exhibits
// this behavior.
$arg = null;
} else {
$reflFunc = $reflParam->getDeclaringFunction();
$classDeclare = ($reflFunc instanceof \ReflectionMethod)
? " declared in " . $reflFunc->getDeclaringClass()->name . "::"
: "";
$classWord = ($reflFunc instanceof \ReflectionMethod)
? $className . '::'
: '';
$funcWord = $classWord . $reflFunc->name;
throw new InjectionException(
$this->inProgressMakes,
sprintf(
self::M_UNDEFINED_PARAM,
$reflParam->name,
$reflParam->getPosition(),
$funcWord,
$classDeclare
),
self::E_UNDEFINED_PARAM
);
}
return $arg;
}
private function prepareInstance($obj, $normalizedClass)
{
if (isset($this->prepares[$normalizedClass])) {
$prepare = $this->prepares[$normalizedClass];
$executable = $this->buildExecutable($prepare);
$result = $executable($obj, $this);
if ($result instanceof $normalizedClass) {
$obj = $result;
}
}
$interfaces = @class_implements($obj);
if ($interfaces === false) {
throw new InjectionException(
$this->inProgressMakes,
sprintf(
self::M_MAKING_FAILED,
$normalizedClass,
gettype($obj)
),
self::E_MAKING_FAILED
);
}
if (empty($interfaces)) {
return $obj;
}
$interfaces = array_flip(array_map(array($this, 'normalizeName'), $interfaces));
$prepares = array_intersect_key($this->prepares, $interfaces);
foreach ($prepares as $interfaceName => $prepare) {
$executable = $this->buildExecutable($prepare);
$result = $executable($obj, $this);
if ($result instanceof $normalizedClass) {
$obj = $result;
}
}
return $obj;
}
/**
* Invoke the specified callable or class::method string, provisioning dependencies along the way
*
* @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
* @param array $args Optional array specifying params with which to invoke the provisioned callable
* @throws \Auryn\InjectionException
* @return mixed Returns the invocation result returned from calling the generated executable
*/
public function execute($callableOrMethodStr, array $args = array())
{
list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
$executable = new Executable($reflFunc, $invocationObj);
$args = $this->provisionFuncArgs($reflFunc, $args, null, $invocationObj === null ? null : get_class($invocationObj));
return call_user_func_array(array($executable, '__invoke'), $args);
}
/**
* Provision an Executable instance from any valid callable or class::method string
*
* @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
* @return \Auryn\Executable
*/
public function buildExecutable($callableOrMethodStr)
{
try {
list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
} catch (\ReflectionException $e) {
throw InjectionException::fromInvalidCallable(
$this->inProgressMakes,
$callableOrMethodStr,
$e
);
}
return new Executable($reflFunc, $invocationObj);
}
private function buildExecutableStruct($callableOrMethodStr)
{
if (is_string($callableOrMethodStr)) {
$executableStruct = $this->buildExecutableStructFromString($callableOrMethodStr);
} elseif ($callableOrMethodStr instanceof \Closure) {
$callableRefl = new \ReflectionFunction($callableOrMethodStr);
$executableStruct = array($callableRefl, null);
} elseif (is_object($callableOrMethodStr) && is_callable($callableOrMethodStr)) {
$invocationObj = $callableOrMethodStr;
$callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
$executableStruct = array($callableRefl, $invocationObj);
} elseif (is_array($callableOrMethodStr)
&& isset($callableOrMethodStr[0], $callableOrMethodStr[1])
&& count($callableOrMethodStr) === 2
) {
$executableStruct = $this->buildExecutableStructFromArray($callableOrMethodStr);
} else {
throw InjectionException::fromInvalidCallable(
$this->inProgressMakes,
$callableOrMethodStr
);
}
return $executableStruct;
}
private function buildExecutableStructFromString($stringExecutable)
{
if (function_exists($stringExecutable)) {
$callableRefl = $this->reflector->getFunction($stringExecutable);
$executableStruct = array($callableRefl, null);
} elseif (method_exists($stringExecutable, '__invoke')) {
$invocationObj = $this->make($stringExecutable);
$callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
$executableStruct = array($callableRefl, $invocationObj);
} elseif (strpos($stringExecutable, '::') !== false) {
list($class, $method) = explode('::', $stringExecutable, 2);
$executableStruct = $this->buildStringClassMethodCallable($class, $method);
} else {
throw InjectionException::fromInvalidCallable(
$this->inProgressMakes,
$stringExecutable
);
}
return $executableStruct;
}
private function buildStringClassMethodCallable($class, $method)
{
$relativeStaticMethodStartPos = strpos($method, 'parent::');
if ($relativeStaticMethodStartPos === 0) {
$childReflection = $this->reflector->getClass($class);
$class = $childReflection->getParentClass()->name;
$method = substr($method, $relativeStaticMethodStartPos + 8);
}
list($className, $normalizedClass) = $this->resolveAlias($class);
$reflectionMethod = $this->reflector->getMethod($className, $method);
if ($reflectionMethod->isStatic()) {
return array($reflectionMethod, null);
}
$instance = $this->make($className);
// If the class was delegated, the instance may not be of the type
// $class but some other type. We need to get the reflection on the
// actual class to be able to call the method correctly.
$reflectionMethod = $this->reflector->getMethod($instance, $method);
return array($reflectionMethod, $instance);
}
private function buildExecutableStructFromArray($arrayExecutable)
{
list($classOrObj, $method) = $arrayExecutable;
if (is_object($classOrObj) && method_exists($classOrObj, $method)) {
$callableRefl = $this->reflector->getMethod($classOrObj, $method);
$executableStruct = array($callableRefl, $classOrObj);
} elseif (is_string($classOrObj)) {
$executableStruct = $this->buildStringClassMethodCallable($classOrObj, $method);
} else {
throw InjectionException::fromInvalidCallable(
$this->inProgressMakes,
$arrayExecutable
);
}
return $executableStruct;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace WPML\Auryn;
class InjectorException extends \Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace WPML\Auryn;
interface ReflectionCache
{
public function fetch($key);
public function store($key, $data);
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WPML\Auryn;
class ReflectionCacheApc implements ReflectionCache
{
private $localCache;
private $timeToLive = 5;
public function __construct(ReflectionCache $localCache = null)
{
$this->localCache = $localCache ?: new ReflectionCacheArray;
}
public function setTimeToLive($seconds)
{
$seconds = (int) $seconds;
$this->timeToLive = ($seconds > 0) ? $seconds : $this->timeToLive;
return $this;
}
public function fetch($key)
{
$localData = $this->localCache->fetch($key);
if ($localData != false) {
return $localData;
} else {
$success = null; // stupid by-ref parameter that scrutinizer complains about
$data = apc_fetch($key, $success);
return $success ? $data : false;
}
}
public function store($key, $data)
{
$this->localCache->store($key, $data);
apc_store($key, $data, $this->timeToLive);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace WPML\Auryn;
class ReflectionCacheArray implements ReflectionCache
{
private $cache = array();
public function fetch($key)
{
// The additional isset() check here improves performance but we also
// need array_key_exists() because some cached values === NULL.
return (isset($this->cache[$key]) || array_key_exists($key, $this->cache))
? $this->cache[$key]
: false;
}
public function store($key, $data)
{
$this->cache[$key] = $data;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace WPML\Auryn;
interface Reflector
{
/**
* Retrieves ReflectionClass instances, caching them for future retrieval
*
* @param string $class
* @return \ReflectionClass
*/
public function getClass($class);
/**
* Retrieves and caches the constructor (ReflectionMethod) for the specified class
*
* @param string $class
* @return \ReflectionMethod
*/
public function getCtor($class);
/**
* Retrieves and caches an array of constructor parameters for the given class
*
* @param string $class
* @return \ReflectionParameter[]
*/
public function getCtorParams($class);
/**
* Retrieves the class type-hint from a given ReflectionParameter
*
* There is no way to directly access a parameter's type-hint without
* instantiating a new ReflectionClass instance and calling its getName()
* method. This method stores the results of this approach so that if
* the same parameter type-hint or ReflectionClass is needed again we
* already have it cached.
*
* @param \ReflectionFunctionAbstract $function
* @param \ReflectionParameter $param
*/
public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param);
/**
* Retrieves and caches a reflection for the specified function
*
* @param string $functionName
* @return \ReflectionFunction
*/
public function getFunction($functionName);
/**
* Retrieves and caches a reflection for the specified class method
*
* @param mixed $classNameOrInstance
* @param string $methodName
* @return \ReflectionMethod
*/
public function getMethod($classNameOrInstance, $methodName);
}

View File

@@ -0,0 +1,53 @@
<?php
namespace WPML\Auryn;
class StandardReflector implements Reflector
{
public function getClass($class)
{
return new \ReflectionClass($class);
}
public function getCtor($class)
{
$reflectionClass = new \ReflectionClass($class);
return $reflectionClass->getConstructor();
}
public function getCtorParams($class)
{
return ($reflectedCtor = $this->getCtor($class))
? $reflectedCtor->getParameters()
: null;
}
public function getParamTypeHint( \ReflectionFunctionAbstract $function, \ReflectionParameter $param ) {
if ( version_compare( '7.1.0', phpversion(), '<' ) ) {
$reflectionClass = $param->getType() && ! $param->getType()->isBuiltin()
? new \ReflectionClass( $param->getType()->getName() )
: null;
} else {
$reflectionClass = $param->getClass();
}
return ( $reflectionClass )
? $reflectionClass->getName()
: null;
}
public function getFunction($functionName)
{
return new \ReflectionFunction($functionName);
}
public function getMethod($classNameOrInstance, $methodName)
{
$className = is_string($classNameOrInstance)
? $classNameOrInstance
: get_class($classNameOrInstance);
return new \ReflectionMethod($className, $methodName);
}
}