Yii2 Dependency Injection Container - interface registration not resolving dependent object instance - php

In the Yii2 Guide it describes various methods of Registering Dependencies with the Dependency Injection Container. See here: https://www.yiiframework.com/doc/guide/2.0/en/concept-di-container#registering-dependencies
// register an interface
// When a class depends on the interface, the corresponding class
// will be instantiated as the dependent object
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
So I have tested this and it doesn't appear to function as described because the registered mapping of an interface to class does not resolve in the created object. See my test code examples below...
Custom Test Class Requiring Dependency Interface:
namespace api\components\Test;
class DependencyTest
{
public function __construct(\api\components\Test\DependencyInterface $dependency, $config = [])
{
echo '<pre>';
echo '$dependency->getSummary() ' . print_r($dependency->getSummary(), true);
echo '</pre>';
exit;
parent::__construct($config);
}
}
Custom Dependency Interface:
namespace api\components\Test;
interface DependencyInterface
{
public function getSummary();
}
Custom Dependency Class:
namespace api\components\Test;
class DependencyClass implements DependencyInterface
{
public function getSummary() {
return [
'currentClassMethod' => __METHOD__,
];
}
}
In Controller:
$container = new \yii\di\Container;
$container->set('\api\components\Test\DependencyInterface', '\api\components\Test\DependencyClass');
try {
$dependencyTest = $container->get('\api\components\Test\DependencyTest');
} catch (\Exception $e) {
echo '<pre>';
echo '$e->getMessage() ' . print_r($e->getMessage(), true);
echo '</pre>';
die();
}
Running this in the controller, I see the following response:
$e->getMessage() Can not instantiate api\components\Test\DependencyInterface.
Why does DI container not resolve the registered interface to the mapped class (DependencyClass) and inject an instance of this into the constructor of DependencyTest? From the generated exception, it would seem that it is ignoring this and trying to instantiate the interface itself.
Is the guide simply misleading here? Do I have to additionally do this?
$dependencyTest = $container->get(
'\api\components\Test\DependencyTest',
// constructor params
[$container->get('\api\components\Test\DependencyInterface')]
);
Now it does work, but it seems to destroy the whole objective of using this DI container in the first place instead of simply instantiating in a regular php way!

Related

PHP-DI: Interface being injected into constructor won't resolve correctly

I can't seem to get PHP-DI to properly resolve an interface to it's configured class when injected in the constructor. In the following code, using the container to get \Foo\IDog returns a Poodle class, but when using the container to get \Foo\Kennel (which has a \Foo\IDog in the constructor, it no longer recognizes that it's configured to return a Poodle and errors saying:
"Entry "\Foo\Kennel" cannot be resolved: Entry "Foo\IDog" cannot be resolved: the class is not instantiable"
Here's the proof of concept:
<?php
namespace Foo;
require(__DIR__ . "/vendor/autoload.php");
interface IDog {
function bark();
}
class Poodle implements IDog {
public function bark() {
echo "woof!" . PHP_EOL;
}
}
class Kennel {
protected $dog;
public function __construct(\Foo\IDog $dog) {
$this->dog = $dog;
}
public function pokeDog() {
$this->dog->bark();
}
}
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->addDefinitions([
"\Foo\IDog" => \DI\autowire("\Foo\Poodle")
]);
$container = $containerBuilder->build();
//Works:
$mydog = $container->get("\Foo\IDog");
$mydog->bark();
//Does not work:
$kennel = $container->get("\Foo\Kennel");
$kennel->pokeDog();
The strange thing is that it works fine if I remove all namespacing from it (here it is with no namespacing: https://gist.github.com/brentk/51f58fafeee8029d7e8b1e838eca3d5b).
Any idea what I'm doing wrong?
I think this is because your class names are not valid in your configuration: "\Foo\IDog" is not valid, "Foo\IDog" is.
When in code \Foo\IDog works too but when in a string only Foo\IDog is valid.
A safe way to avoid that is to use \Foo\IDog::class. That way PHP will tell you that the class doesn't exist.

Extending classes and using traits for main class

I'm building an api with php and I am not that familiar with extending classes or using traits. Currently I'm using traits to better structure my main API class.
See below for the current way of working. I was wondering if I could create a class inside the API class. One which is for example responsible for the webhook methods. This needs to have access to all methods on both router and api for the current instance.
Example
When people access the api on /v1/{method}/{verb}/{args}
if (!array_key_exists('HTTP_ORIGIN', $_SERVER)) {
$_SERVER['HTTP_ORIGIN'] = $_SERVER['SERVER_NAME'];
}
try {
$API = new API($_REQUEST['request'], $_SERVER['HTTP_ORIGIN']);
echo $API->processAPI();
}
catch (Exception $e) {
echo json_encode(['error' => $e->getMessage()]);
}
Router class
abstract class router {
/*
Methods which seek out
* method
* verb
* args
And do authentication
*/
}
My main API class
class API extends router {
// some generic methods specifically for this class
// Other methods but in different files (for readability and oversight)
use otherMethod1;
use otherMethod2;
// ...
}
You can inject webhook class object to your API class. Small example, as requested.
class Webhook { // some methods }
class API extends router {
private $webhook;
public function __construct(Webhook $webhook)
{
$this->webhook = $webhook;
}
// some generic methods specifically for this class
public function useWebhook()
{
$result = $this->webhook->someWebhookMethod();
// ...
}
// Other methods but in different files (for readability and oversight)
use otherMethod1;
use otherMethod2;
// ...
}
When you can create your objects like this
$webhook = new Webhook();
$api = new API($webhook);
$api->useWebhook();
Are you looking for this?
You can read more about this approach called "dependency injection" right here: http://php-di.org/doc/understanding-di.html

How to inject framework into custom class?

I'm currently developing an application with the FlightPHP framework and wondering how am I able to inject FlightPHP into my custom class so that I am able to use specific classes I have injected into it's dependency container.
use Flight;
use Logger;
class DB{
public function __construct(...){
$this->app = $app; // Flight:: instance
}
public function doStuff($stuff){
return $this->app->log()->function($stuff);
}
}
Flight::register('log', 'Logger', ['app'], function($log) {
return $log->pushHandler(new StreamHandler('app.log'));
});
Flight::register('database', 'DB', array($data), function($db) {
return $db;
});
I'm attempting to inject Flight into my database class constructor so that I am able to use the log function which was previously injected into the Flight dependency container.
The "Logger" works in the index.php when used under the Flight instance "Flight::log()->function("test");", however when I attempt to use it in another scope(within the Database class), it doesn't allow me to use it in the context of "Flight".
Update:
Flight::register('log', 'Monolog\Logger', ['app'], function($log) {
return $log->pushHandler(new StreamHandler('app.log'));
});
class DB{
function __construct(Monolog\Logger $engine){
#var_dump($engine);
$engine->addInfo("injected"); // works
}
}
Flight::register('database', 'DB', array(Flight::log()), function($db) {
return $db;
});
Flight::database();
Is correct usage?
You could pass instance of \Flight\Engine in the array of third parameter at register method to pass framework instance in you DB controller. \Flight\Engine does not use interface sou you are coupling your code with framework implementation I guess. In this case you can use Flight::app() everywhere to obtain framework instance.
<?php error_reporting(E_ALL);
require 'vendor/autoload.php';
class DB
{
function __construct(\Flight\Engine $engine)
{
var_dump($engine->get('connectionString'));
}
}
Flight::set('connectionString', 'mssql');
Flight::register('database', 'DB', array(Flight::app()), function($db) {
return $db;
});
Flight::database();
Looks like that Flight does not have such a concept as Dependency Injection Container. You have to specify your parameter values explicitly.
Update:
By doing this ['app'] you are injecting string into constructor of Monolog\Logger. This line return $log->pushHandler(new StreamHandler('app.log')); should raise error.
Read more carefully http://flightphp.com/learn

Dependency injection on dynamically created objects

I'm using reflection to test if a class has a particular parent class and then returning an instance of it.
if (class_exists($classname)) {
$cmd_class = new \ReflectionClass($classname);
if($cmd_class->isSubClassOf(self::$base_cmd)) {
return $cmd_class->newInstance();
}
}
I want to be able to unit test these instantiated objects but I don't know if there is any way to use dependency injection in this case. My thought was to use factories to get dependencies. Is the factory pattern the best option?
My thought was to use factories to get dependencies
By using this method you still would not know which dependencies a class has.
I would recommend going a step further, and resolve the dependencies with the Reflection API, too.
You can actually type hint constructor arguments and the Reflection API is fully capable of reading the type hints.
Here is a very basic example:
<?php
class Email {
public function send()
{
echo "Sending E-Mail";
}
}
class ClassWithDependency {
protected $email;
public function __construct( Email $email )
{
$this->email = $email;
}
public function sendEmail()
{
$this->email->send();
}
}
$class = new \ReflectionClass('ClassWithDependency');
// Let's get all the constructor parameters
$reflectionParameters = $class->getConstructor()->getParameters();
$dependencies = [];
foreach( $reflectionParameters AS $param )
{
// We instantiate the dependent class
// and push it to the $dependencies array
$dependencies[] = $param->getClass()->newInstance();
}
// The last step is to construct our base object
// and pass in the dependencies array
$instance = $class->newInstanceArgs($dependencies);
$instance->sendEmail(); //Output: Sending E-Mail
Of course you need to do error checking by yourself (If there's no Typehint for a constructor argument for example). Also this example does not handle nested dependencies. But you should get the basic idea.
Thinking this a step further you could even assemble a small DI-Container where you configure which instances to inject for certain Type Hints.

Proper way to deal with class dependency in a custom class in Laravel 4

I'm adding a custom class to my Laravel library, and seem to be very close to success. Heres what I've done so far, followed by the error I'm getting:
My class is called Connector and is located in Acme/Api/Zurmo/Connector
This class requires another class, so this is the code for that:
use Acme\Api\Rest\ApiRestHelper;
class Connector implements ConnectorInterface {
protected $rest;
public function __construct(ApiRestHelper $rest)
{
$this->rest = $rest;
}
and my ApiRestHelper class starts like this:
namespace Acme\Api\Rest;
class ApiRestHelper {
Then I've just added a quick closure in routes.php to test this works, (which it doesn't):
Route::get('/', function()
{
$connector = new Acme\Api\Zurmo\Connector;
var_dump($connector);
});
This is the error I get:
Argument 1 passed to Acme\Api\Zurmo\Connector::__construct() must be
an instance of Acme\Api\Rest\ApiRestHelper, none given
I first assumed I'd screwed my namespacing, filenaming up, but Laravel can locate the class as I can do the following without error:
$rest = new Acme\Api\Rest\ApiRestHelper;
var_dump($rest);
Any ideas what I'm missing here? Thank you.
The constructor expects you to pass in an instance of resthelper:
//manual dependency injection
Route::get('/', function()
{
$connector = new Acme\Api\Zurmo\Connector(new Acme\Api\Rest\ApiRestHelper);
var_dump($connector);
});
OR change the constructor
//creates a hard dependency
use Acme\Api\Rest\ApiRestHelper;
class Connector implements ConnectorInterface {
protected $rest;
public function __construct()
{
$this->rest = new ApiRestHelper();
}
}
A more advanced option would be to use the IOC container for dependency inject, but thats beyond the scope of this answer
http://laravel.com/docs/ioc

Categories