Unlike other dependency injectors, this is not meant as a DI-container. It calls methods for you, filling in any missing parameters instead of forcing you to pull them out of the container manually.
$di = new DependencyInjector();
$di->construct(\DateTime::class); // constructs a new DateTime object using default parameters (i.e. current time)
$di->call('time'); // calls time()
Not so exciting? The real magic happens when you use type-hinted parameters:
class MyController {
private $req;
public function __construct(Request $req) {
dump(__METHOD__, $req);
$this->req = $req;
}
public function action(Response $res) {
dump(__METHOD__, $res);
}
}
class Request {
private $uri;
public function __construct(Uri $uri) {
dump(__METHOD__, $uri);
$this->uri = $uri;
}
}
class Response {
}
class Uri {
private $uriParts;
public function __construct($uriString = null) {
dump(__METHOD__, $uriString);
if(!strlen($uriString)) {
$this->uriParts = parse_url('http://username:password@hostname:9090/path?arg=value#anchor'); // TODO: get from $_REQUEST
} else {
$this->uriParts = parse_url($uriString);
}
}
}
$di = new \mpen\DI\DependencyInjector();
$di->call('\MyController::action');
What this does is:
- Tries to call
\MyController::action
- Notices
\MyController::action
is non-static, so it tries to construct a newMyController
MyController
requires aRequest
so it tries to create one of thoseRequest
needs aUri
Uri
wants a$uriString
but we didn't supply one so it uses the default (null
)- Now that we have a fully-instantiated
MyController
, we can invoke->action()
on it, but it needs aResponse
- Constructs a
Response
and invokes the method
If that wasn't clear, here's what the output looks like:
Uri::__construct"
null
"Request::__construct"
Uri {#18
-uriParts: array:8 [
"scheme" => "http"
"host" => "hostname"
"port" => 9090
"user" => "username"
"pass" => "password"
"path" => "/path"
"query" => "arg=value"
"fragment" => "anchor"
]
}
"MyController::__construct"
Request {#14
-uri: Uri {#18
-uriParts: array:8 [
"scheme" => "http"
"host" => "hostname"
"port" => 9090
"user" => "username"
"pass" => "password"
"path" => "/path"
"query" => "arg=value"
"fragment" => "anchor"
]
}
}
"MyController::action"
Response {#7}
In the above example, what if we did have a URI and wanted to use that instead of the default? We can register it as a global variable early in the application life-cycle:
$di->registerGlobal('uriString', 'http://example.com:3000');
Then whenever a parameter called $uriString
is encountered and no other value is provided, it will check if one
exists in the globals and use that instead!
You can register your $_GET
and/or $_POST
variables as globals if you want to use them as defaults for your
controller actions, for example (if you're using MVC).
Alternatively, you can register a class instead,
$di->registerObject(new Uri('http://google.com'));
Now when the DependencyInjector
encounters a Uri
, it will use your registered instance instead of trying
to construct a new one on its own.
If you don't want to instantiate an object until it's needed, and you want full control over how it's created, you can register a callback instead:
$di->registerCallback(Uri::class, function() {
return new Uri('https://bing.com');
});
What if you function is type-hinted against an interface? How will the DI know which class to substitute? For this, you can register an interface!
$di->registerInterface(\Psr\Http\Message\RequestInterface::class, \GuzzleHttp\Psr7\Request::class);
Note that you can also use this to register abstract and inherited classes.
What if you aren't using singletons? What if, for example, you have two different database connections?
You can register them using the $namePatt
option so that they will be injected when the argument name
matches a regex pattern.
$di->registerObject($appDb, '~app(?!\\p{Ll})~A');
$di->registerObject($pcsDb, '~pcs(?!\\p{Ll})~A');
Now $appDb
will be provided when the function argument starts with "app", and $pcsDb
will be provided when the
argument starts with "pcs".
If neither pattern matches, the DI will try to construct a database object on its own, like usual, which will likely fail because it doesn't know your DSN.
You can always $di->registerCallback(YourDB::class, ...)
as a fallback if you don't want this to happen. You might
just want to throw an exception in this case. As powerful as this DI is, it can't hack your DB and steal your
credentials out of thin air :-)
See the unit tests for more examples.
Option | Type | Default | Description |
---|---|---|---|
cacheObjects |
bool | true |
Cache constructed objects for future re-use |
globals |
array | [] |
Global keyword arguments (automatically injected if parameter name matches) |
memoizeFunctions |
bool | true |
Not implemented |
memoizeMethods |
bool | false |
Not implemented |
coercePosArgs |
bool | true |
Implicitly convert positional arguments to the correct type if they do not match |
coerceKwArgs |
bool | true |
Implicitly convert keyword arguments to the correct type if they do not match |
coerceGlobals |
bool | false |
Implicitly convert global vars to the correct type if they do not match |
coerceCallback |
callable | construct | Default function to use for the coercion if no callback is registered for the specific type. If not provided, will try using the constructor, passing in the one arg that was in the position of this type. |
propagateKwArgs |
bool | false |
Match keyword arguments against top-level call (false ) or propagate keyword arguments to recursively construct dependencies (true )? |
MIT.