Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dependency Injection support with CONTAINER variable #221

Merged
merged 3 commits into from
Mar 24, 2018

Conversation

nanawel
Copy link
Contributor

@nanawel nanawel commented Jul 5, 2017

F3 works almost out-of-the-box with DI frameworks like PHP-DI.
The only thing it lacks is the DI "bootstrap" code that starts all the injection.

I found that the best way to achieve that is to add an alternative to the \Base::call() method. Currently it works like this (let's say to call \MyController->index):

  1. If the \MyController is a subclass of \Prefab, we call \MyController::instance()->index
  2. Otherwise, we instanciate \MyController and call the method directly on it

With the DI support I'm proposing, it's almost the same, but the 2nd alternative becomes the 3rd:

  1. If \MyController is a subclass of \Prefab, we call \MyController::instance()->index
  2. If there is a framework variable called CONTAINER defined, we use it as a \Prefab to retrieve an instance of the \MyController via its get() method
  3. Otherwise, we instanciate \MyController and call the method directly on it

Pretty simple, isn't it?
With this I had no other F3-related thing to do to integrate PHP-DI on my application, and everything works the same.

If you're asking "Why CONTAINER?", it's just the generic term for the dependency injection main component.

@nanawel
Copy link
Contributor Author

nanawel commented Jul 5, 2017

Here's an example of the configuration you could have to use such a DI Container in your config.ni

CONTAINER=\Container

and the corresponding \Container singleton that acts as a proxy with the DI framework (here with PHP-DI):

<?php
use DI\ContainerBuilder;

class Container extends \Prefab
{
    /** @var \DI\Container */
    protected $container;

    public function get($name) {
        return $this->getContainer()->get($name);
    }

    public function make($name, array $parameters = []) {
        return $this->getContainer()->make($name, $parameters);
    }

    /**
     * @return \DI\Container
     */
    protected function getContainer() {
        if (!$this->container) {
            $containerBuilder = new ContainerBuilder();
            $containerBuilder->addDefinitions('etc/di.php');
            $this->container = $containerBuilder->build();
        }
        return $this->container;
    }
}

@NeoVance
Copy link

-1

Use auryn instead or implement as a plugin.

@Rayne
Copy link
Member

Rayne commented Jul 26, 2017

@NeoVance The PR of @nanawel is a generic solution using a Prefab to wrap calls to a DIC. The DIC specific example above is not part of the PR.

@Rayne
Copy link
Member

Rayne commented Jul 26, 2017

Some notes:

  • CONTAINER should also support closures or already instantiated objects
  • It would be nice to be compatible with PSR-11
    1. Check if CONTAINER has a has method (This is a duck type checking workaround to avoid adding a real dependency). If it's there, assume it's the ContainerInterface->has() method and check if the expected ContainerInterface implementation knows how to create the requested object
    2. If the assumed ContainerInterface knows how to create the object, let it create and return it. Otherwise continue with the last step and let F3 create the object and call the specified method

@NeoVance
Copy link

Why would you need to create a method of wrapping a third party lib in F3? Just use your third party lib directly as intended?

@Rayne
Copy link
Member

Rayne commented Jul 26, 2017

The PR enables F3's routing system to use any DIC to create the first controller. Without the PR it is currently not possible to inject dependencies when creating a controller automatically by the routing system.

Currently there is only the option to use the beforeRoute() hook or the constructor to use service location (e.g. by using the global HIVE or Registry) or similar techniques.

@nanawel
Copy link
Contributor Author

nanawel commented Jul 27, 2017

@NeoVance As stated by @Rayne, using DI is not just like using any other third-party library. You need to bootstrap your class tree with it so that all dependencies can be resolved at once. In F3, the simplest and cleaner way is to do that when instanciating the controller (because it is the entry point of the user-code). My PR enables that.

@Rayne Thanks for your feedback and the defense of my solution while I was away :) I really like your suggestions, I'll think about them and submit an improvement when I'm back from my vacations.

As an additional note, my PR allows to use the DI library with every call to \Base::call(), which means mainly when routing to the controller, but also when using this method to call any other callable supported by F3. I personally also use this when registering listeners I declare in a dedicated [events] section in the INI files.

@nanawel
Copy link
Contributor Author

nanawel commented Aug 2, 2017

I've updated my PR with @Rayne's suggestions. Below is a test file I've written to validate the implementation. Feel free to comment.

require 'vendor/autoload.php';
require 'base.php';

class FakeClass {
	public function test() {
		return 'OK';
	}
}
class FakeContainer {
	public function has($class) {
		return true;
	}
	public function get($class) {
		return new $class;
	}
}
class FakeContainerProxy extends Prefab {
	public function get($class) {
		return new $class;
	}
}

$fw = \Base::instance();

$fw->set('CONTAINER', new FakeContainer());
$fw->call('FakeClass->test') == 'OK' or die('ERROR');

$fw->set('CONTAINER', function($class) { return new $class; });
$fw->call('FakeClass->test') == 'OK' or die('ERROR');

$fw->set('CONTAINER', 'FakeContainerProxy');
$fw->call('FakeClass->test') == 'OK' or die('ERROR');

@ikkez
Copy link
Member

ikkez commented Aug 8, 2017

Don't you think that it might be enough to just add a callable hook at the grab method to allow you to put your own container implementation there? Something simple as

elseif ($this->exists('CONTAINER',$container))
	$parts[1]=call_user_func_array($container,[$parts[1]]);

Then you can adjust your container implementation the way you need it for your project:

$fw = \Base::instance();
$container = new FakeContainer;
$fw->set('CONTAINER', function($class) use ($container) {
	if (is_callable([$container,'has']) && $container->has($class))	//PSR11
		$class=call_user_func([$container,'get'],$class);
	elseif (is_callable($container))
		$class=call_user_func($container,$class);
	elseif (is_string($container) && is_subclass_of($container,'Prefab'))
		$class=call_user_func($container.'::instance')->get($class);
	return new $class;
});

@nanawel
Copy link
Contributor Author

nanawel commented Aug 8, 2017

@ikkez I could, but I'd really like to be able to define my CONTAINER variable from the config file and not from the code.

@ikkez
Copy link
Member

ikkez commented Aug 8, 2017

Then what about this?

elseif ($this->exists('CONTAINER',$container) && $container!=$func) {
	if (is_string($container))
		$container=$this->grab($container);
	$parts[1]=call_user_func_array($container,[$parts[1]]);
}
class FakeContainer {
	public function has($class) {
		return true;
	}
	public function get($class) {
		return new $class;
	}
	public function load($class) {
		if ($this->has($class))	{
			$class=$this->get($class);
			return new $class;
		} else
			user_error('class not found');
	}
}
$fw->set('CONTAINER','FakeContainer->load');

@nanawel
Copy link
Contributor Author

nanawel commented Aug 8, 2017

Well, I can't say it won't work, but nothing more.

I think the goals I want to keep in mind here are as follows:

  • allow the use of an external DI library via config only
  • straight call to the container if it is PSR11 compatible

But I'm ok to adjust the implementation if needed. How about a vote? Then I'll make the necessary changes.

@ikkez
Copy link
Member

ikkez commented Aug 8, 2017

My only intention here was:

  • keep the base as short as possible
  • make it most flexible, extensible

It's still as easy as CONTAINER = \Container->get but you could add your PSR11 support (or any other in the future) by a custom function (rather than pushing more code in the base class).
At least just an idea,.. so let's see what others say.

@nanawel
Copy link
Contributor Author

nanawel commented Mar 5, 2018

Hi there, I'd like to come to an accepted solution on that issue soon. It's been 7 months already, and my implementation has successfully passed the production tests so far.

I'd like to hear from @bcosca if possible, and then make the final adjustments.
So far we have:

  • my current implementation (presented earlier)
  • @ikkez's proposal, which is similar but excludes a config-only option as it needs to set a callable to CONTAINER variable, which obviously cannot be done from INI files

Thanks for your participation

@ikkez
Copy link
Member

ikkez commented Mar 5, 2018

Well yeah I can understand that a simple way of defining the CONTAINER via config would be convenient. @xfra35 any thoughts about this topic? Actually I'm thinking about merging this as it is right now 🤔

@xfra35
Copy link
Member

xfra35 commented Mar 8, 2018

I don't have a strong opinion on this. The solution proposed by @nanawel is simple and makes it easy to use external DI librairies. We just need to keep in mind that the framework could, in the future, embed its own service locator or DI container.

Would be interesting to hear @bcosca's views.

@nanawel, although your PR targets $f3->call(), I understood from your comments that your primary need is the ability to inject dependencies into controllers. How do you plan to pass custom dependencies to controllers, considering the fact that $f3 and $params are already passed by the framework?

@nanawel
Copy link
Contributor Author

nanawel commented Mar 8, 2018

@xfra35 You're right, when using DI to instanciate the controller class (or any other class that uses $f3->grab(), the $args parameter is ignored.

In the cases of a PSR11-compatible or a \Prefab container, I don't think it's a good practice to pass an argument when instanciating a singleton (it should be instead defined in the DI configuration, which is what it is for in the first place).

In the case of a callable I agree I should pass this args when calling it. So I guess

    ...
    elseif (is_callable($container))
        $parts[1]=call_user_func($container,$parts[1]);
    ....

should be instead

    ...
    elseif (is_callable($container))
        $parts[1]=call_user_func($container,$parts[1],$args);
    ....

What do you think?

@nanawel
Copy link
Contributor Author

nanawel commented Mar 9, 2018

@xfra35 I think you meant to summon @bcosca instead :)

@xfra35
Copy link
Member

xfra35 commented Mar 9, 2018

Argh indeed :) Fixed.

As for your remark, I agree. I'd even say that there's no reason to pass method arguments ($args) to the grab() function. In particular, I don't think the controller constructor needs $f3 and $params, since these arguments are useful at routing time (route handler + beforeRoute/afterRoute hooks), not instantiation time.

But we can't change that part without breaking BC, so we should flag this for v4.

@nanawel
Copy link
Contributor Author

nanawel commented Mar 9, 2018

Erf... I agree, reluctantly. So v4 it is.

And I agree that grab() should not need these arguments at all. Should I make the change regarding $args anyway when using a closure?

@ikkez
Copy link
Member

ikkez commented Mar 9, 2018

Actually I don't see a break in BC right now for this.. but it could be that for v4, we'll introduce another solution, which then works differently than what is here supposed to be merged..that's true. was that your concerns, @xfra35 ?

@xfra35
Copy link
Member

xfra35 commented Mar 9, 2018

No, I'm just saying that the current PR won't fulfill the original need of @nanawel, which was to automatically inject dependencies into controllers. In order to fulfill it, we would need to prevent the framework from passing $f3 and $params to the controller constructor, which would break BC.

@nanawel
Copy link
Contributor Author

nanawel commented Mar 9, 2018

The current PR works for my need, but for that it does not inject $f3 and $args in the constructor that's right, so in this sense it breaks BC.

@ikkez
Copy link
Member

ikkez commented Mar 9, 2018

Is that the case? I mean, for me it looks like the framework actually only passes $f3 and $args as the method arguments to the route handler but not to the class instantiation itself. Adding $f3 and $args to the route handler happens elsewhere - grab() just transforms a string expression like Foo->bar to a callable, means it creates the class object, where another $args could be passed through, but that doesn't explicitly belong to the routing. So IMO it's all fine.

@xfra35
Copy link
Member

xfra35 commented Mar 9, 2018

That's the case:

  1. $f3->run() calls $this->call($handler,[$this,$args,$handler],'beforeroute,afterroute')
  2. which in turn calls $this->grab($func,$args)
  3. which in turn calls $ref->newinstanceargs($args)

IMHO $args could be removed from steps 2 and 3, but that would break BC.

@nanawel
Copy link
Contributor Author

nanawel commented Mar 12, 2018

One point regarding BC break if I may: it must be noticed that my PR does not break BC in its current form, because it only adds a new way of instanciating objects through $f3->grab().

There can't be existing implementations that use any old way, because... there is no old way using CONTAINER since it did not exist before.

Of course, it means that converting an existing implementation to DI will require refactoring as the existing constructors might need $f3 and $args as discussed. But converting any existing implementation to DI is a huge refactoring in the first place, so IMHO we're talking about a detail here.

@ikkez
Copy link
Member

ikkez commented Mar 18, 2018

Valid points. It will not break BC, because CONTAINER would be a new way to instantiate objects and I don't know if anyone is actually using the $f3 and $args from the routing class constructor, I didn't know about it and have not even found this documented. So yes @xfra35 I think we could remove that in a later major release.
For now I think a DI container option would be nice. What happens with v4 is not even set to stone and maybe we offer both (DIC & ServiceLocator).

@ikkez
Copy link
Member

ikkez commented Mar 23, 2018

I played with it using Dice and it's pretty easy to setup:

class A {
	protected $b;

	function __construct(B $b) {
		$this->b = $b;
	}
	function get() {
		return $this->b->data();
	}
}

class B {
	protected $data = 123;

	function data() {
		return $this->data;
	}
}


$dice = new \Dice\Dice;
$f3->set('CONTAINER',function($class) use ($dice) {
	return $dice->create($class);
});

echo $f3->call('A->get'); // 123

@ikkez ikkez merged commit 848238f into f3-factory:master Mar 24, 2018
@ikkez
Copy link
Member

ikkez commented Mar 24, 2018

I'm merging this now because I can see the needs, especially for larger applications and using Dependency Injection for better decoupling of components. Having a support for this at the core level is a good thing and it's flexible enough to allow usage of any container solution you'd prefer.

ikkez added a commit that referenced this pull request Mar 26, 2018
@tuzoftware
Copy link

How can I use with controller, service and repossitory?

@ikkez
Copy link
Member

ikkez commented May 9, 2018

@tuzoftware What do you mean by this? the CONTAINER is "just" an option to integrate an existing DIC into the F3 core, meaning when you use $f3->call, the "whatever" DI lib you use, is called to resolve dependencies. How to use the DI you choose, you have to read in their docs.. PHP-DI, Dice, auryn, just to name a few.

@eazuka
Copy link

eazuka commented Feb 13, 2019

I played with it using Dice and it's pretty easy to setup:

class A {
	protected $b;

	function __construct(B $b) {
		$this->b = $b;
	}
	function get() {
		return $this->b->data();
	}
}

class B {
	protected $data = 123;

	function data() {
		return $this->data;
	}
}


$dice = new \Dice\Dice;
$f3->set('CONTAINER',function($class) use ($dice) {
	return $dice->create($class);
});

echo $f3->call('A->get'); // 123

@ikkez @nanawel please could any of you give further example of how Dice rules (addRules) fit into using this f3 CONTAINER option, using @ikkez dice example above.

Also is $f3->set('A', $f3->call('A')); proper?

then $f3->A->get(); doable with F3 and the DI container?

@ikkez
Copy link
Member

ikkez commented Feb 14, 2019

@eazuka just use the Dice rules as described in its manual.

Also is $f3->set('A', $f3->call('A')); proper?

No. What should it do? $f3->call is for calling a function or class method. If you just want to instantiate A and save it to the hive, it's $f3->set('A', new A());.

@Falkon1313
Copy link

Falkon1313 commented Sep 6, 2021

Valid points. It will not break BC, because CONTAINER would be a new way to instantiate objects and I don't know if anyone is actually using the $f3 and $args from the routing class constructor

I have been using those arguments in my controllers.

So for anyone else reading this and trying to figure it out, the documentation example doesn't show it, but if you use a callable to wrap your container, then you get free backwards compatibility!

Ex:

$f3->set('CONTAINER', function ($class, $args) use ($dice) {
    return $dice->create($class, $args);
});

Note that second argument - $args is available there (as of 3.7 at least), and they're the same that F3 would pass if you weren't using a container.

Now your controllers that were written as

public function __construct(\Base $f3, array $params = [], string $controllerCallback = '')

will still receive the same arguments that they would without using a container - the framework, the route parameters with tokens resolved, and the actual callback string.

This is because grab() does the following:

				elseif (is_callable($container))
					$parts[1]=call_user_func($container,$parts[1],$args);

Note: it only works if you use a callable for the container. Using a PSR-11 or Prefab-based container directly will not pass the $args. You can wrap it in a callable and do what you need to to get the args setup though, depending on your container library.

For new projects, it's probably better to let the container itself resolve arguments. But if you do need backward compatibility with existing code, it's very easy to get that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants