4

How to use Dependency Injection container when I have a variable non-static parameter that I need to supply?

What I want in my code is this:

$staff = $container->get(Staff::class);

What I have now is this:

$staff = new Staff("my_great_username");

Note that username can change and is supplied at run-time.

I cannot seem to put Staff into my DI Container, because there is no way to specify a variable parameter there.

My Problem is ...

I am using Factory-based container, namely Zend\ServiceManager\ServiceManager.This is a factory that I use to hide instantiation details:

class StaffFactory
{
    function __invoke(ContainerInterface $container): Staff
    {
        /*
         * I do not seem to know how to get my username here
         * nor if it is the place to do so here
         */
        $staff = new Staff(????????);
        return $staff;
    }
}

The way I set up the container in my config is this:

'factories' => [
    Staff::class => StaffFactory::class
]

Note: even though the parameter is a "variable", I want Staff to be immutable. Namely, once it is created, it stays that way. So I do not particularly wish to make a setter method for the username as that will imply that the class is mutable, when it is not.

What do you suggest?

Dennis
  • 7,907
  • 11
  • 65
  • 115
  • if this stumps you too, upvote this question xD – Dennis Sep 06 '17 at 21:52
  • Im confused - why are you using the IOC container as a sevice locator, and in particular with concrete classes - doing it like that does not appear to have any benefit over `new` – Steve Sep 06 '17 at 22:03
  • where do you see usage of IoC as a service locator? As I understand it, it only becomes a service locator when I inject `$container` into my own classes, where I can then pull whatever it is I want. With `ServiceManager` being a factory-based IoC, as long as I use `$container` as part of boot-strap, it remains an IoC. aka, injecting `$container` into Factory is an accepted use-case for IoC, and that injection remains localized to that Factory and does not spill into my own classes, thereby avoiding service locator pattern. – Dennis Sep 06 '17 at 22:11
  • But then, if you are talking about `$staff = $container->get(Staff::class);` if that ends up inside one of my classes, then yeah it is service locator pattern and best practice is to avoid that. – Dennis Sep 06 '17 at 22:11
  • i was referring to "What I want in my code is this:" `$staff = $container->get(Staff::class);` – Steve Sep 06 '17 at 22:12
  • in that case I can refactor that line to a Factory, to where use of `$container` is pulled out of my code and into a top-level bootstrap factory, which is part of the IoC. I can have something like `return new Ticket(new Category($container->get(Staff::class)))`, in my particular code, But then how do I get `$username` into my `Staff` class in that Factory? – Dennis Sep 06 '17 at 22:17
  • 2
    But i guess thats besides the point. I have no experience with zend, but as a general case of DI, where you need to construct with runtime parameters, but use dependancy injection, you inject a factory. So instead of excepting a `Staff` instance (or better `StaffInterface` - concrete injection doesnt offer much), your method excepts a `StaffCreatorInterface` which defines a method `create(string $username):StaffInterface` – Steve Sep 06 '17 at 22:19
  • more so on to "why I'm using.. ": I am refactoring legacy code, slowly putting things into DI container. I cannot put everything into it at once, so I am doing it in steps. That's why my code may be using Service Locator pattern in places, while refactoring is going on. – Dennis Sep 06 '17 at 22:19
  • So your method that was once `someHrThing(){ $john = new Staff('john'); //...}` becomes `someHrThing(StaffCreatorInterface $staffCreator){ $john = $staffCreator->create('john'); //...}` – Steve Sep 06 '17 at 22:23
  • 1
    Oh and good luck with the refactor, they are always painfull! – Steve Sep 06 '17 at 22:26
  • 1
    ok that was a very cool yet a bit convoluted design pattern on how it works.. Thank you. I see this as *instead of injecting `Staff`, inject `StaffCreator`, and then use that to create `Staff`*. So, instead of creating class instance yourself you delegate it to the creator, where creator does not have variable parameters and thus can be injected, or constructed via its own Factory. And the interfaces make this DI reusable. I think that will work for me. Thanks. Feel free to make this into an answer. – Dennis Sep 06 '17 at 22:59
  • 1
    Glad i could help you. Yes i guess it was a bit convoluted! Im on a phone atm, so cant write much formatted code, and dont know the specifics for zend, so i wont write an answer. Once you get it working you should add an answer yourself, it will be usefull for future visitors. – Steve Sep 06 '17 at 23:09

1 Answers1

2

My problem was that I had a variable parameter that was being passed to the constructor of my Staff class.

The solution is to create a StaffCreator class that does not have variable parameters in its constructor, and then write a StaffCreator::create method, which takes the variable parameter. Then, instead of injecting Staff into whatever class where Staff is needed, inject StaffCreator instead, and then use it to create a Staff instance.

i.e.

//Inject this wherever you need Staff
$staffCreator = $container->get(StaffCreator::class);

//use it:
$staff = $this->staffCreator->create("my_great_username");

//code:
class StaffCreatorFactory
{    
    function __invoke(ContainerInterface $container)
    {
        return new StaffCreator();
    }
}

class StaffCreator
{
    function __construct()
    {
        //additional creation parameters possible here
    }

    function create(string $username): Staff
    {
        return new Staff($username);
    }
}

with credit to Steve

Note: You can create and add Interfaces to the above code to make DI reusable. i.e. StaffCreatorInterface, StaffInterface. In my case I kept it simple, as I do not (yet) have a strong use case for reuse of the interface.

Dennis
  • 7,907
  • 11
  • 65
  • 115