yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.24k stars 6.91k forks source link

Make Component like composite-pattern #503

Closed Ragazzo closed 10 years ago

Ragazzo commented 11 years ago

It would be great if we have ability to set components on some basic class extended from Component, so in this way it will be smth. like composite pattern, also @samdark do you remeber this suggestions from russian php forum? Any other thoughts of core-developers? In Yii1 i was missing this functionality, that i can't do smth. like this for example:

class MyComponent extends CComponent
{
}

$a = new MyComponent();
$a->setComponent($b);
$a->setComponents($config); //configure

I think this functionality can be cleared from Module and set to the Component class.

Ragazzo commented 10 years ago

@samdark absolutely not. it was only about public property components in object config and not that custom creating, see no benefits of such custom. because how will you determine simple strings from components? This feature only for making features from Module to Component.

Ragazzo commented 10 years ago

@samdark

$rocket = Yii::createObject(
    'class' => 'Rocket',
    'components' => [
         'engine' => [
             'class' => 'app\rocket\gasoline\Engine',
             'fuel' => 'Gas',
         ],
    ],
);

$roket->engine->start();
//or in your case
$roket->fly();
samdark commented 10 years ago

That's much more interesting. If I still want a rocket to fly w/o starting engine directly I need the class itself to be:

class Rocket extends Component
{
  public function fly()
  {
    return $this->getComponent('engine')->work();
  }
}

Pros:

  1. Much less code.
  2. Lots of stuff can be handled via config.
  3. Extremely flexible. Useful for CMS and other flexible systems.

Cons:

  1. No IDE autocomplete (solvable via phpdoc easily).
  2. ???

Since I can't find any significant cons I think it should be done.

creocoder commented 10 years ago

@samdark Probably make sence.

Ragazzo commented 10 years ago

is it time to celebrate then? yes, no?

a_382922f1

samdark commented 10 years ago

So the only question left is the form of it i.e. should it be component method or a separate container as @qiangxue already mentioned.

creocoder commented 10 years ago

@samdark

Cons:

  1. No IDE autocomplete (solvable via phpdoc easily).
  2. ???

About 2 a little:

class Rocket extends Component
{
  public function fly()
  {
    return $this->getComponent('engine')->work();
  }
}

$rocket = new Rocket();
$rocket->setComponent('bomb');
$rocket->fly();

Rockets created with another approaches does not have such troubles. I do not say that feature is bad. I just say that move it to Component is bad idea. Need some another ideas about implementation. For example for Module its really make sence to have ability set any components. For Rocket no. Maybe we need trait, maybe interface, do not know yet.

samdark commented 10 years ago

@creocoder I don't get it. You can enforce interface if you want via checking if getComponent returns what you need and throwing exception otherwise.

creocoder commented 10 years ago

@samdark

I don't get it

Will try to explain. Rockets created using:

class Rocket extends Component
{
  public function setEngine($engine) {
     ...
  }

  public function fly()
  {
    ...
  }
}

Simplier and reliable.

samdark commented 10 years ago

That's what I did in https://github.com/yiisoft/yii2/issues/503#issuecomment-32597577 exactly.

qiangxue commented 10 years ago

Proposal of DI container interface: https://gist.github.com/qiangxue/0af159376ecaf2e78e4a

Based on this interface, will implement a ContainerTrait which will be used by Module (and thus Application) to turn a module/application into a DI container. Will also provide a Container class out-of-box so that you can use it alone. If you prefer to turn your existing class into a container, just use the trait.

The container supports both constructor injection and setter injection. It also serves as service locator. Other injections will not be supported as they are not commonly used.

Please help review the interface design. Thanks!

Ragazzo commented 10 years ago

Ok, can we change ! for not shared, and make shared without this mark? Because it is common that ! is not and as for shared i thouth it is for not shared components, so i find it confusing. Overall i am not sure that we need such big implementation (good to have but not must), because Yii::$app by itslef with setComponents/getComponents acts like a ServiceLocator, and usually users will be using it rather that di trait.

However can you clarify you example with DI in constructor? So, user writed interface or class typehint, and what else he need to do? where we should write:

use yii\di\Container;
use yii\di\Instance;

 $container = new Container;
 $container->components = [
     'db' => [
         'class' => 'yii\db\Connection',
         'dsn' => '...',
     ],
     'UserFinderInterface' => [
         'class' => 'UserFinder',
         'db' => Instance::of('db', $container),
     ],
 ];

and should we? i thought it will be resolved automatically, no?

yupe commented 10 years ago

With this feature can we inject dependency into Controller through constructor for example ? Will framework core use that component ?

qiangxue commented 10 years ago

@Ragazzo As I wrote, Module (and thus Application) will use this DI trait, which will replace their current implementation of getComponents(), etc. The design of DI trait is made such that all existing syntax of components is unchanged. Only change is getComponent() is renamed to get(). The example I gave is to show how to use DI container alone. You may replace it with Yii::$app. When you configure components in app config, you are essentially configuring the DI container.

@yupe Yes.

There is some drawback by using DI container. It is mainly related with the performance degradation caused by constructor injection because the container needs to use reflection to detect dependencies. That means, the instantiation of every application component will be slightly slower.

Ragazzo commented 10 years ago

I think we should not resolve constructor dependecies with DI - container. Can you point docs from what you were reading and implementing current solution? I got breif look into L4 IoC and their container, however there is nothing special in it. But as for me resolving constructor deps. with type hint is not needed.

Ragazzo commented 10 years ago

also, @qiangxue am i correct to understand that not shared components will be created each time ? how do you plan to create them since they can be not yii\base\Component ? Will your container have ability for binding closures ?

qiangxue commented 10 years ago

Constructor dependency is the most used DI. See the writing from the original author of DI: http://martinfowler.com/articles/injection.html L4 implements that too.

There is no requirement regarding the base class of an object to be handled by this DI implementation.

Yes, it supports closures (there's a simple example.)

Ragazzo commented 10 years ago

yes, i know that L4 supports constructor DI. Not sure if we should do this, because it will force users to dump everything in DI container, for example https://github.com/yiisoft/yii2/blob/master/framework/db/Connection.php#L392 users will be putting some db.connection in DI container and then writing typehint everywhere, i think this is not right, since your container will be like huge number of components and nothing more.

Can you show here example with closure? https://gist.github.com/qiangxue/0af159376ecaf2e78e4a

Also should Yii::$app->container component be created? or how do you plan to use it ? more specic examples will be great to understand. For example in L4, it is simple App::bind(), in Yii what it should be?

qiangxue commented 10 years ago

Yes, I agree with you. I'm also not sure whether we should support constructor injection. The original author of DI compares fairly all kinds of DI. It's a good read.

https://gist.github.com/qiangxue/0af159376ecaf2e78e4a#file-containerinterface-php-L157

No, there's no Yii::$app->container, because Yii::$app itself is a container.

Ragazzo commented 10 years ago

As for me, IoC solution through DI that is implemented in simple manual injection into constructor and setters are fine, autoresolving is not just feeling right, and if needed they can simply get it from container by itself. Overall it is fine by me, since the only thing i wanted was ability to set components on every other component. Can you also show how can we register components (i mean any components, not Yii Component class) with get / set methods from config? for example for Yii components we can simply specify components property and setComponents setter will be called, how then specify correctly things for get / set properties? Or do you have plan/idea of simply specifying them somewhere in ioc_di.php config file, that will be included (require_once) after application instance is created and user can bind there everything he need?

qiangxue commented 10 years ago

When you are configuring components in app config, you are configuring the DI container (Yii::$app is the global DI container). There's nothing new here except the new syntax introduced as described in the following. The syntax is compatible with the current one with new extensions. https://gist.github.com/qiangxue/0af159376ecaf2e78e4a#file-containerinterface-php-L143

Ragazzo commented 10 years ago

no, when i set components i set Yii components (they will be created with Yii::createObject()), how can set through config not yii components, but for example some custom classes and other things with set and get methods? as i said, should we have separated config for this, it will be simple plain file, where user will register everything that is needed ? Or maybe do we need something like L4 service providers ? not sure about the last one though )

UPD sorry, my misunderstanding, you are right, however can you show how components will be created? with Yii::createObject or what method? How then handle other not yii objects ?

qiangxue commented 10 years ago

As I said components is the one you use to configure "application components" and classes. The latter is the new thing introduced. An "application component" is essentially a service or component in the DI terminology. For this reason, Application is indeed a service locator. There's no need to separate config file or whatever.

Ragazzo commented 10 years ago

so, all services should be just put in components config setting ? also how to resolve creating not yii components? i think user can use different classes and other things, how can we check if component should be created with Yii::createComponent or by some other thing ?

qiangxue commented 10 years ago

There's no Yii::createComponent().

The "application components" are simply named shared objects, while the syntax classname and !classname declare anonymous shared and non-shared objects.

Ragazzo commented 10 years ago

i understand by when set is called, then instance should be created, and in docs to interface there is a notice about Yii::createObject (sorry, i mixed it with Yii1 similar method), and still how you will resolve this situation ? https://gist.github.com/qiangxue/0af159376ecaf2e78e4a#file-containerinterface-php-L184

qiangxue commented 10 years ago

Nope, an object is not created until get() is called.

What exactly is your question? It seems we are looping around. You may view Yii::createObject() as the new operator except that the former will respect Yii::$objectConfig.

Ragazzo commented 10 years ago

my question is: how object will be created if i register it like this:


class MyCustomObject
{
}

`components` => [
      'customObject' => 'MyCustomObject',
],

it also can be different classes from other fw and etc. If they will be created with Yii::createObject that means that some array will be passed to __construct() but objects may not expect such behavior, that is what i am talking about. Maybe we will need services init. based on env, such idea was spoken long time ago too - main thing is that service should know how it should be init. and created, this is implemented with service init. layer (read it as class), that will create object and init it as needed.

qiangxue commented 10 years ago

Yes, if you give a config array for an object, it will be passed to the constructor. We may do it better by checking if Object is the base class. If not, the config array will be applied after the object is created.

Maybe we will need services init.

More details?

Ragazzo commented 10 years ago

yes, i have some thoughts but i think that this can be something overcomplicated and i discarted it. So if i will need custom object creating i always can use callback function for this purpose and put all such things in separated file like /config/ioc.php and in it:

Yii::$app->setComponent('custom', function ($container) {
      $custom = MyCustomObject();
      $custom->setSomething('some-value');
      ....
      return $custom;
});

so if this is a valid use-case then we should provide docs for this as an example i think ?

Also we may consider to implement something like L4 service providers - thing to group similar providers in class, because some init. may be needed for them, based on env. You can look like in L4 it is done, however it is simple class-wrapper as you can see, nothing special.

Such situation can be used when we for example may use different classes and components from other frameworks, and if we use several of such components, we may need some good way to pre-init them on some env. conditions, with that said it is better to organize them into some structure - service provider.

qiangxue commented 10 years ago

components is a massive way of configuring the container. You can use set() to configure individual class. So nothing is special here. Your example of using anonymous function is already supported.

L4 service provider is just a way to run a piece of code from a class during bootstrap time. We already have Application::extensions which does similar thing.

Ragazzo commented 10 years ago

nope, extenstions are not the same as providers for services in L4. Overall it can be something like this:


class CustomProvider extends ServiceProvider
{

    public function init()
    {
         $this->registerServices();
    }

    public function registerFirstService()
    {
        ....
    }

    public function registerSecondService()
    {
        ....
    }

}

by this we can plug in and configure services as needed, without puttin them in plain file as i described above, this makes services configuring flexible.

Also as for anonymous functions, i know they are supported, i just asked if my example with plain file is correct. If so then maybe we should add later to docs this example.

qiangxue commented 10 years ago

All features are done. Reopened for documentation purpose.

Ragazzo commented 10 years ago

ArrayAccess done ?

qiangxue commented 10 years ago

Nope as it doesn't make sense

On Friday, March 21, 2014, Mark notifications@github.com wrote:

ArrayAccess done ?

— Reply to this email directly or view it on GitHubhttps://github.com/yiisoft/yii2/issues/503#issuecomment-38249635 .

Ragazzo commented 10 years ago

hm, afaik in sf2 you can act with container as array? no?

qiangxue commented 10 years ago

Yes but it still doesn't make sense

On Friday, March 21, 2014, Mark notifications@github.com wrote:

hm, afaik in sf2 you can act with container as array? no?

— Reply to this email directly or view it on GitHubhttps://github.com/yiisoft/yii2/issues/503#issuecomment-38268606 .

Ragazzo commented 10 years ago

why it does not make sense?

qiangxue commented 10 years ago

Why array access?

On Friday, March 21, 2014, Mark notifications@github.com wrote:

why it does not make sense?

— Reply to this email directly or view it on GitHubhttps://github.com/yiisoft/yii2/issues/503#issuecomment-38269007 .

Ragazzo commented 10 years ago

Useful for setting components, same issues as for sf2 overall .

qiangxue commented 10 years ago

Arrayaccess should not be abused. It should be mainly used by classes that represent data collections. Otherwise we would say any classes should implement this as it is convenient to set properties. I don't think sf2 is making a good choice here (other fws are not doing this.) Anyway, let's not spend more time trying to convince each other. We can always implement this in future if there are strong requests for this feature.

Ragazzo commented 10 years ago

Ok )