Jump to content

Dependency Injection via setter method


Envrin

Recommended Posts

 

Could use some help with dependency injection, as I can't seem to find the answer anywhere.  Ok, take a quick PHP class:

 

	<?php
	 
	namespace myapp;
	 
	use myapp\template;
	use myapp\order;
	 
	class user {
	 
	    private $template;
	 
	    public function __construct(template $template) {
	        $this->template = $template;
	    }
	 
	    public function add_order(order $order) {
	        // do something with $order here
	    }
	 
	}
	

 

I'm using the php-di package from http://php-di.org/ right now, but open to changing.  My question is, how do I call that "add_order()" method, and have $order injected into it?  Constructor injection is easy, and just use:

 

	$container = new Di\Container();
	$container->make(myapp\user);
	

 

How do I do that, but calling the add_order() method instead?  I want something like:

 

	$container = new Di\Container();
	$container->make(myapp\user::add_order);
	

 

Any help?

 

Thanks,

Matt

 

Link to comment
Share on other sites

->make creates an object. If you do not want it to create an object then ->make is not the answer. Look through the documentation to see if something else looks more appropriate.

But this... this doesn't feel right. Your user class is a dependency? Again, look at the documentation but this time specifically for ->make to see how it's supposed to be used.

Link to comment
Share on other sites

  • 2 weeks later...


I need to get this right, and am a little confused.  Been using the popular http://php-di.org/ package for dependency injection, and although I fully understand the concept, I'm quite obviously missing something when it comes to implementation.

Many of you know a project I've been working on:  https://github.com/envrin/apex/

You guys tore my head off for not using DI, and using too many static methods.  Ok, so I've basically got DI implemented now, but it's not really adding up for me

The concept of DI is excellent, and I do understand it.  My main issue is basically, if you use DI anywhere, then you have ti use it every where 100%, because everything has dependencies of some kind, which means everything has to flow through the container.  I'm struggling to see how this is more efficient than just including a class via "use" statement, then calling it via static methods.

Will do my best, but take an example order class:

    namespace myapp;
	    use myapp\products;
    use myapp\utils\forms;
    use myapp\utils\components;
	    class orders {
	        private $app;
	        public function __construct(app $app) {
            $this->app = $app;
        }
	        public function add($product_id, $country) {
	            $this->verify($product_id);
	        }
	        private function verify($product_id) {
	            $client = new products();
            $client->lookup($product_id);
	        }
    }

In Apex right now, I'd either just leave the above class as is without "app" being injected, or create a "lookup" static method within the products class and just use:

    products::lookup();

We're using DI though, so that's out.  In the above class, "app" gets injected, which is our container itself.  Now there's no way we can just createa new instance of the "products" class like that, as it will have its own dependencies, hence it either has to get called from the container or injected into the class.  We can always call it from the container with:

    $client = $this->app->get(products::class);

Not the greatest, so there's always constructor injection same as we did with "app"..  That means:

- Define a "$client" property
- Add "products $client" to constructor arguments list
- Define "$this->client = $client" within constructor
- Use "$this->products->lookup()" in that method.

Again, not the greatest.  We could do parameter injection via annotation with:

    /**
     * @Inject
     * @var products
     */
    private $client;

Then we can just use "$this->client->lookup()" within our method.  Again, still not great.  Then there's always method / setter injection, such as:

    /**
     * Verify the order
    *
     * @Inject
    * @param products $client
     */
    private function verify($product_id, products $client) {
	            $client->lookup();
    }

That seems decent enough and workable, except now we can't just call "$this->verify($product_id)" within our class anymore, because it has to go through the container instead so injection occurs.  Instead, you have to call it with something like:

    $this->app->call([orders::class, 'verify'], ['product_id' => $product_id]);

That's definitely no good.  Now put this into context of a larger class with 50 methods that has a total of say 12 dependencies, each of which has their own dependency so everything MUST go through the container, and it creates a real mess.  This is especially true when you have say that class with 50 methods, and there's one dependency class used one time in one method, and you're forced to do either:

- Constructor injection
- Parameter injection
- Method injection with annotations, but call the method via the container and not directly.

At least from my POV, none of those seem like a good option.  I do really like the concept of DI, and that method / setter injection seems to the best, but obviously I'm missing something.  Is there maybe some type of "call handler" similar to an error / exception handler, so I can call methods directly within the code and still have injection occur without having to call the methods via the container?

I really like the idea of just having everything you need at your fingertips via reflection / injection like this, but not really at the expense of all this extra code.  What am I missing here?  Could someone please kindly help?  I need to get this right.

 

 

 

 


```

 

Link to comment
Share on other sites

1 hour ago, Envrin said:

My main issue is basically, if you use DI anywhere, then you have ti use it every where 100%, because everything has dependencies of some kind, which means everything has to flow through the container.

Who says so?  A container is nice because it allows you to configure your potential objects without creating them, but I think containers are just a means to help implement DI.  Or maybe I should say DI is a good means to implement composition and contains are a good means to implement DI.  Just my two bits...

Link to comment
Share on other sites


Still struggling here a bit.  I can't seem to get the simplicity of static methods with dependency injection, but I know within the modern software world static methods are a crime against humanity.

I really enjoy the power of dependency injection, and definitely want to keep that.  I like the ability of easily swapping out classes for different ones just by modifying a small PHP configuration file, and as long as both classes implement the same interface, everything still continues to work like a charm.  For example, if you want to use a different log handler or http client than the default one, it's no problem to switch it out.

Problem I have is once you begin using DI, you have to basically call 100% of everything via the container because everything has dependencies that need to be injected.  This causes a real mess, and that's when I begin missing the simplicity of static methods.

So here's my workaround, and I'm just curious as to your thoughts.

First, I have all my base classes (database, log hander, debugger, template parser, event dispatcher, etc.) and these are all proper PHP classes with non-static methods that implement certain interfaces for structural integrity, etc.

Then I have some very basic "service" classes that look something like:

<?php
	namespace myapp;
	class db {
	    private static $instance = null;
	    public static method singleton($obj = null) {
	        if ($obj !== null && !self::$instance) {
            self::$instance = $obj;
        }
	        return self::$instance;
	    }
	    public static function __callstatic($method, $params) {
        return self::$instance->$method(...$params);
    }
}

I have one of these classes for each "service" I want (eg. db, log, debug, template, rpc, etc.).  Then within the bootstrap / config script fo the app, I "assign" the services with something like:

use myapp\interfaces\LoggerInterface;
use myapp\services\log;
$obj = $app->make(myapp\sys\log::class, ['channel_name' => 'default']);
$app->set(LoggerInterface::class, $obj);
log::singleton($obj);

So it creates an instance of the logger class via the DI container's "make()" method allowing for injection if necessary, then sets it into the container as the LoggerInterface::class, plus sets it as the instance within the services\log class.

If I want to switch out to a different log hander, I simply modify that config file and change the creation of the class to say "monolog\monolog\Logger::class", or whatever.  As long as it's PSR3 compliant and implements that LoggerInterface, it will work fine.

It's also sitting in the DI container and available for injection where ever necessary, plus also available via my small services class to keep accessibility simple.  I can then access methods of the log handler anywhere within my code without any injection required by simply using:

use myapp\services\log;
log::warning("This is a warning");

This is the equivalent of have the "LoggerInterface $obj" injected into a class / method, and calling the "$log->warning()" method.

I end up with the power and flexibility of dependency injection, the simplicity and accessibility of static methods, and proper non-static PHP classes allowing for the developer of proper unit tests.  Plus I don't have to worry about writing all that extra code and creating a mess due to paramater / constructor / method injection, or calling methods via the container instead of directly, etc.

Would love to hear any thoughts anyone has about this, and if it's a horrible methodology, why?

 

Link to comment
Share on other sites

Spending some time with a framework that makes heavy use of DI might be helpful in getting a better feel for it.   I started using it more when I started using Symfony, and it took some time to really get my head around it and switch from my previous coding style which was also heavily based on classes with static methods or singletons.

For DI to be really useful it also requires good application design and you also do really have to shift to that mind set of doing a lot of things through the container.  It may seem daunting and annoying when viewed from the old singleton/statics mindset but it's really not that bad once you start designing around that fact.

It helps along to really think about what should be part of a given service and divide things up appropriately.  For example, your initial post doesn't really make sense.  Your User class shouldn't really be a service that gets injected.  It would just store your user's information and maybe have a few helper methods.  Likewise with an Order class, it'd just contain the order details and possibly some helper methods.   Where you would use DI is for some service that makes use of those two data objects, for example maybe a service that takes an order and generates a receipt.

class ReceiptGenerator {
    private $renderer;
    private $template;

    public function __construct(TemplateRenderer $renderer, string $template){
    }

    public function generate(Order $order){
        return $this->renderer->render($this->template, ['order' => $order]);
    }
}

 

As another example that may help show how you can make using the container less of a concern, instead of having all your pages be separate .php files, have a single entry point and then implement a router.  You'd then configure your router similar to how you configure your container.  The router class would have a reference to the container and thus be able to get your controllers, which means your controllers can all be injected automatically with their necessary dependencies.  Those dependencies would likewise be injected automatically as they are made by the container.

//Router.php
class Router {
    private $container;
    private $routes;

    public function __construct(Container $container){
        $this->container = $container;
    }

    public function addRoute($url, $controller, $method){
        $this->routes[$url] = [$controller, $method];
    }

    public function route(){
        $url = $_SERVER['REQUEST_URI'];
        if (isset($this->routes[$url])){
            $route = $this->routes[$url];
            $controller = $this->container->get($route[0]);
            $this->container->call([$controller, $route[1]]);
        }
    }
}

//config.php
//Configure container
$container = new Container();
$container->addDefinition([
    ContactController::class => function (ContainerInterface $c){
        return new ContactController($c->get(TemplateRenderer::class), $c->get(Emailer::class));
    },

    TemplateRenderer::class => function (ContainerInterface $c){
        return new TemplateRenderer();
    },

    Emailer::class => function (ContainerInterface $c){
        return new Emailer();
    }
]);

//Configure router
$router = new Router($container);
$router->addRoute('/contact', ContactController::class, 'contact');

//TemplateRenderer.php
class TemplateRenderer { //Some implementation }

//Emailer.php
class Emailer { //Some implementation }

//ContactController.php
class ContactController {
    private $renderer;
    private $mailer;
    public function __construct(TemplateRenderer $renderer, Emailer $mailer){
        $this->renderer = $renderer;
        $this->mailer = $mailer;
    }

    public function contact(){
        //Process request
        if ($_SERVER['REQUEST_METHOD'] === 'post'){
            $this->mailer->send('contact@example.com', 'Contact message', $_POST['message']);
            $this->renderer->render('thankyou.html');
        } else {
            $this->renderer->render('contact.html');
        }
    }
}


Might look complicated, but it's not once you get used to the idea.  It can also be simplified much more.  For example, PHP-DI supports Autowiring which means it'll use Reflection to look at type hints and use that automatically determine which objects need created and injected.  In the above example it'd probably eliminate the container configuration entirely as the container would just look at the constructor type hints and construct and inject those classes automatically.

 

  • Like 1
Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.