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

Switching to non-static getServices method returning callables #20

Merged
merged 3 commits into from May 13, 2016

Conversation

moufmouf
Copy link
Contributor

Hey,

Following discussion on #5 and #18, here is a proposal for a new version of the ServiceProvider interface with:

  • a non static getServices method
  • a getServices method that returns an array of callables

I'm having a hard time wondering if we should not rename the getServices method (as it actually returns an array of factories).

Maybe to something like getFactories or serviceFactories?

Also, I've added a fair amount of text. Not sure everything belongs to README.md. Comments are welcome!

@weierophinney
Copy link

The only other change I might recommend is allowing factories to be any of:

  • PHP callable
  • Fully qualified class name resolving to a functor (class defining __invoke())

This latter has some interesting benefits in terms of caching the results of service providers:

  • it's neither an object instance nor a closure out-of-the-box, allowing easy serialization.
  • Containers can either choose to create the instances up front, or instantiate the factory only when the service is retrieved. If they do the latter, memory usage is minimized if the service is never retrieved.

@mnapoli
Copy link
Member

mnapoli commented Apr 19, 2016

@weierophinney if we allow that then it opens the door to things like:

  • what happens if the class has arguments in its constructor?
  • why is that exception to callable allowed and not another exception X or Y? (e.g. ['Foo', 'bar'] instead of [new Foo, 'bar'])
  • not being able to type-hint callable(minor)

Also this PR is quite long to review already, maybe it would be better to have one change per PR?

I'm having a hard time wondering if we should not rename the getServices method

@moufmouf I think this should be a separate PR too.


And if the service provider is only a provider of a callable array, then why make service providers actual arrays? Do we have benefits to having a class? Composition of service providers maybe?

@weierophinney
Copy link

weierophinney commented Apr 19, 2016

  • what happens if the class has arguments in its constructor?

Disallow it within the specification. I.e., specifying a functor implies a constructor-less class, or a constructor that allows no arguments.

Essentially, if a functor class has constructor arguments, you'd need to instantiate it when returning the list of factories:

return [
    Foo::class => new FooFactory($argument, $argument2),
];
  • why is that exception to callable allowed and not another exception X or Y?

For the reasons I specified:

  • serialization of service-provider results.
  • optimizations by containers (instances do not need to be created unless the service is retrieved).

This may seem like a micro-optimization, but we discovered quite the opposite with Zend Framework. You cannot serialize closures easily or in a performant fashion (even super-closure notes in its own README that "In general, however, serializing closures should probably be avoided."). Without the option of providing a functor class name, you're left with static methods as the only serializable option for factories returned by a service-provider.

Sure, these could wrap the functor:

public static function createFoo($container, callable $getPrevious = null)
{
    return (new FooFactory())($container, $getPrevious);
}

but this then leads to two function calls instead of one.

Containers can store the string class name, and instantiate it only when required:

if (! is_callable($this->factories[$name])) {
    $factory = new $this->factories[$name];
    if (! is_callable($factory)) {
        throw new InvalidFactoryException();
    }
    $this->factories[$name] = $factory;
    return $factory($this, $getPrevious);
}

$factory = $this->factories[$name];
return $factory($this, $getPrevious);

We observed 10-15% improvements when using this approach, over an approach that required always providing a callable, and measurable memory reduction as well.

@mnapoli
Copy link
Member

mnapoli commented Apr 20, 2016

That's really interesting. However:

Without the option of providing a functor class name, you're left with static methods as the only serializable option for factories returned by a service-provider.

  1. Then why aren't static methods not a good approach?

  2. And if any callable is allowed then what guarantees that modules will use functors? If only 10% of modules use that solution (because somehow they read ZF's best practices) then the optimization is only possible for those 10%, that's not really much.

    That's what I think are the advantage of static methods today (vs callables): we know they are as optimized as possible, and they are compatible with compiled containers too.

  3. Also it seems to me that another solution in the same branch as functors is possible: allowing [MyServiceProvider::class, 'method'] instead of [new MyServiceProvider, 'method']. The first form is not a proper callable (method() is not static) but allows instantiating MyServiceProvider only when needed (like functors). However it has the following benefits over functors:

  • no need to write one class per container entry (the class can have several methods)
  • less class instantiation by the container compared to functors (because more than 1 method per class) - i.e. the class can be reused by the container, but still be instatiated lazily
  • the class can be itself the service provider, allowing for something more simple (less classes overall, less objects in memory, simpler design, etc.)

And I think with that idea we are circling back to static methods, or at least to methods on the service provider which may or may not be static…

So to me, if we want to optimize for performances here are the best options:

  • static methods on the service provider only (current status)
  • functors only (so we can serialize them, etc.)
  • non-static methods on the service provider only
  • ?

@moufmouf
Copy link
Contributor Author

Hew @weierophinney

Thanks a lot for the great feedback.

If I understand correctly your suggestion, by adding fully qualified class name resolving to a functor as a possible return type (in addition to callables), you allow for an optimization, as the getServices() method does not have to create the functor instance when it is called (we only need to resolve it when the actual service is called).

So:

return [
    Foo::class => FooFactory::class,
];

is faster than:

return [
    Foo::class => new FooFactory(),
];

(if I'm correct, we save the autoloading of class FooFactory + the instantiation)

Now, I'm really wondering if we might actually save a fair amount of performance.

Assuming you have an application with 50 service providers, each containing about 5 services. That's about 250 services. Let's assume all these services are declared as functor. In a "naive" implementation of service providers (like the one in Simplex), this means having 50 calls to getServices(), and 250 instantiation of functors. For these naive implementations, your proposal makes a lot of sense (we can save 250 instantiation of functors by replacing those with FQCN).

But the truth is that if I want to optimize things, I'd prefer completely saving the call to getServices().
This is what I'm doing in Yaco and the Symfony bundle using this utility class. Basically, using this "service provider registry", I'm able to instantiate service providers and call the getServices() method only when a related service is fetched by the container.

So rather than calling the getServices() method 50 times, I'm only calling it as many times as needed to fetch the services I need.

Now, your optimization still makes sense. If a service provider contains 5 services, and if I fetch only one, I'm creating 5 functors instances for nothing (see example below);

return [
    Foo::class => new FooFactory(),
    Foo2::class => new Foo2Factory(),
    Foo3::class => new Foo3Factory(),
    Foo4::class => new Foo4Factory(),
    Foo5::class => new Foo5Factory(),
];

If I only make a call to $container->get(Foo::class), I indeed created 4 additional factories for nothing.

In practice however, I feel that there is going to be a tight coupling between the entries of a same service provider. I mean: if I fetch a service from one provider, there is a high likelihood that I'll fetch other services from the same service provider.

Here is a sample: a Doctrine DBAL service provider: https://github.com/xhuberty/dbal-universal-module/blob/dbal-service-provider-with-parameter-definition/src/DbalServiceProvider.php
If I fetch a DBAL connection, the container will automatically fetch the DBAL driver associated. There is a strong correlation between the services of a same service provider.

So in practice, I'm not sure that allowing functors FQCN as a possible return type would change much for performance. And this comes at the price of simplicity (having an array of callables as the return type of getServices() is dead easy).

However, that doesn't mean we cannot find ways to optimize this.

Here are a few ideas that pop out of my head:

  • we could serialize the functor instances and cache them (not sure that de-serialization is good for performance though...)
  • or we could provide a CacheableFunctor interface that instructs the compiler that this functor can be cached. For instance:
class MyServiceLocator implements ServiceLocator {
    public function getServices() {
        return [
            "my_alias" => new Alias('my_service');
        ];
    }
}

interface CacheableFunctor {
    public function getConstructorArguments() : array;
}

class Alias implements CacheableFunctor {
    private $alias;

    public function __construct(string $alias) {
        $this->alias = $alias;
    }

    // Returns the list of constructor arguments we can use to create back this object
    public function getConstructorArguments() {
        return [ $this->alias ];
    }

    public function __invoke(ContainerInterface $container) {
        return $container->get($this->alias);
    }
}

Containers interested in performance could "cache" the constructor arguments and reuse those to create the functor. Actually, thinking about it, it would maybe be easier to cache the whole functor (for instance using APCu).

Also, another idea I like very much is the idea to have functors the implement interfaces that semantically mean something. Using my alias example above:

interface AliasInterface {
    public function getAlias() : string;
}

class Alias implements AliasInterface {
    private $alias;

    public function __construct(string $alias) {
        $this->alias = $alias;
    }

    // Returns the list of constructor arguments we can use to create back this object
    public function getAlias() {
        return $this->alias;
    }

    public function __invoke(ContainerInterface $container) {
        return $container->get($this->alias);
    }
}

Container aware of the AliasInterface interface could "understand" that this is an alias, and perform the aliasing at compile/cache time rather than on every single request. This is for me the solution that offers the most optimisation gains.

As noted by @mnapoli however, we need to agree on a common set of interfaces for those functors and that might be quite hard to agree on (definition-interop might be a good starting point).

The important part: all this can be done later, or in another project. container-interop/service-provider should definitely become a PSR. To reach that goal, we should keep it dead simple.

Then, in another project, we could work on add-ons to service-provider (i.e. any interface that could be added to a functor to improve performance). These add-ons would be 100% optional (as they are only here to improve performance).

Another example that came to my mind:

What about a:

interface LazyInterface {
    public function isLazyService(): bool;
}

class LazyService implements LazyInterface {
    private $callable;

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

    public function isLazyService() {
        return true;
    }

    public function __invoke(ContainerInterface $container, $previous) {
        return $this->callable($container, $previous);
    }
}

The LazyInterface instructs any compatible container that the service should be proxied and lazily loaded. In this scenario, it is the role of the container to create to proxy if it detects a LazyInterface. (note: I'm not sure the LazyInterface is a good idea, this really crossed my mind recently and I wanted to share this with you)

Does it make sense?

@weierophinney
Copy link

weierophinney commented Apr 20, 2016

  1. Then why aren't static methods not a good approach?

I never said they weren't a good approach. (The only negative thing I said about static methods was that using them as wrappers to instantiation and invocation of a functor is not optimal.)

  1. And if any callable is allowed then what guarantees that modules will use functors? [ ... ] That's what I think are the advantage of static methods today (vs callables): we know they are as optimized as possible, and they are compatible with compiled containers too.

You're extrapolating based on a false assumption, that functors will only define the __invoke() method.

One advantage of functors is that they allow us to extract methods in order to reduce complexity.

As an example, zend-mvc-i18n defines a TranslatorFactory. The functor method is 4 lines long. One of those lines calls another method in the class, which has paths for invoking other methods. This approach makes the code easier to understand and easier to test.

Yes, this can be done with static methods as well. I personally find these far harder to test, the code harder to read, and much harder to extend (LSB has to be planned for, and visibility can strongly affect it).

  1. Also it seems to me that another solution in the same branch as functors is possible: allowing [MyServiceProvider::class, 'method'] instead of [new MyServiceProvider, 'method']. The first form is not a proper callable (method() is not static) but allows instantiating MyServiceProvider only when needed (like functors).

If you're willing to consider that, why not also FQCN functors? It poses the same complexity (if not more) for consuming containers. (Particularly your assertion that they could optimize by caching the first instance. Introspection of callables formed this way requires a fair bit of logic; we did this in Zend\Stdlib\CallbackHandler, and found it was more difficult for end-users to determine what the expected behaviors were.)

And I think with that idea we are circling back to static methods, or at least to methods on the service provider which may or may not be static…

And I'm tending to agree, particularly with the write-up @moufmouf created with this PR, with regards to "minimal way to fetch the service". In many cases, if a container is available, a developer could simply invoke the method with the container to get the instance required, which is powerful.

That said, I still have two reservations regarding the current syntax:

  • It's not immediately obvious when you first encounter a service provider that the factory refers to a method in the class.
  • Requiring that the method be part of the class limits factory re-use.

Regarding this latter point, consider classes that can be directly instantiated without any arguments. In zend-servicemanager, we provide an InvokableFactory that can be used for these; adapted to service-provider, it would look something like this:

Foo::class => [InvokableFactory::class, 'createService']

The current spec does not allow for this, in two different ways.

First, it limits to factories in the class itself. This means that you would need to wrap the above:

public static function getServices()
{
    return [
        Foo::class => 'createService',
    ];
}

public static function createService(ContainerInterface $container, callable $getPrevious = null)
{
    return InvokableFactory::createService($container, $getPrevious);
}

Again, as noted in my previous discussions around functors, this is suboptimal, as it doubles the number of function calls required in order to create an instance.

The more subtle issue, though, is that the service name is not passed to the factory. That means that the above factory method cannot be re-used for multiple services. As an example:

public static function getServices()
{
    return [
        Foo::class => 'createService',
        Bar::class => 'createService',
    ];
}

public static function createService(ContainerInterface $container, callable $getPrevious = null)
{
    // What will this create?
    return InvokableFactory::createService($container, $getPrevious);
}

In zend-servicemanager, we are currently passing the requested service name to the factory, which allows re-use. If multiple services have the same instantiation pattern, we can define one factory, but map it to multiple services that return different instances. This is powerful for re-use.

It also slightly complicates the signature, and modifies that "minimal" use case. This would likely result in one of the following factory signatures:

function (ContainerInterface $container, $name = null, callable $getPrevious = null);

function (ContainerInterface $container, callable $getPrevious = null, $name = null);

Considering that the callable for $getPrevious is not passed if only one factory is present for a given service, the first signature is likely more correct. The name can be omitted in many cases, but would generally be the name passed to $container->get(); for the minimal use case, it can be omitted unless the factory requires it.

tl;dr: I am buying into the arguments for using static methods as the preferred and/or only way of defining factories, but would like to be able to have re-usable factories, which would (a) potentially expand what's allowed as a factory (i.e., allow any static method, using [$class, $method] notation), and (b) require a signature change to factory invocation.

@mnapoli
Copy link
Member

mnapoli commented Apr 20, 2016

That's all really interesting.

If you're willing to consider that, why not also FQCN functors?

Those where the benefits I listed of using methods over functors:

  • no need to write one class per container entry […]
  • less class instantiation by the container compared to functors […]
  • the class can be itself the service provider, allowing for something more simple […]

But I take your point though:

It poses the same complexity (if not more) for consuming containers

I'm not opposed to functors, it just looks heavier to implement (you have to write one class per service your module defines, which can be a bit of a burden).

but would like to be able to have re-usable factories

As of right now, static or not static, we could achieve that by passing the requested entry name. Then you can extend parent classes or use traits that contain reusable factories (as methods).

That's not really ideal though (reuse through inheritance). But is it a problem? I'm just suggesting the idea, I don't have a strong opinion right now.

Else, if we want to avoid non-cacheable/serializable/compilable stuff like closures, we could allow [string, string] (i.e. ['class', 'method']). That way the method can be on any class. Whether or not the methods should be static or not (in which case containers must instantiate the class) is a minor issue I guess. And that allows to put the method either on the service provider or either on another class (which means reuse of code).

And with that you could do what you suggested:

Foo::class => [InvokableFactory::class, 'createService']

You could even be compatible with invokable classes:

Foo::class => [InvokableFactory::class, '__invoke']

Anyway what I'm arguing for is to restrict what we allow to:

  • one thing (for simplicity both for module writers and container implementations)
  • that is serializable/cacheable/compilable (for maximum compatibility and easy optimizations)

I'm fine if it's not static methods :)

@weierophinney
Copy link

I'm not opposed to functors, it just looks heavier to implement (you have to write one class per service your module defines, which can be a bit of a burden).

Not necessarily; as I noted elsewhere in my comment, you can, in zend-servicemanager, re-use these factories for multiple services. We even cache the factory instances, so that retrieval of services later can re-use them. (Pimple 3 has support for re-usable factories, with caching of the factory on first use, too.)

That said, we can stop arguing around functors. I think the suggestion that we limit to [string, string] callables is solid, and addresses the re-use factor, when combined with allowing passing of the requested service name to the factory, which, if I read your comment correctly, you approve of.

One note:

You could even be compatible with invokable classes:

Foo::class => [InvokableFactory::class, '__invoke']

The above wouldn't work, as __invoke() has to be an instance method when declared.

Anyway what I'm arguing for is to restrict what we allow to:

  • one thing (for simplicity both for module writers and container implementations)
  • that is serializable/cacheable/compilable (for maximum compatibility and easy optimizations)

Agreed, as noted.

To summarize the changes needed to the current specification:

  • Factories MUST be of the form [string, string], where the two arguments are a class name and a method, respectively, representing a callable static method.

  • Factories would use a signature compatible with:

    function (ContainerInterface $container, string $name = null, callable $getPrevious = null) : mixed

@moufmouf
Copy link
Contributor Author

Anyway what I'm arguing for is to restrict what we allow to:

  • one thing (for simplicity both for module writers and container implementations)
  • that is serializable/cacheable/compilable (for maximum compatibility and easy optimizations)

I'd really argue in the opposite direction.

First, about simplicity:

For container implementations, simplicity is not an issue. Yes, supporting callable is harder than supporting a static method, but I've tried it. It took me 1/2 day on Yaco, and another day on Symfony. So it's definitely possible to write.

Furthermore, simplicity is really important for people implementing the ServiceProvider (there is going to be a lot more developers writing ServiceProviders than developers writing containers). For them, having the ability to use any kind of callable is probably more important. In particular, I'm a big fan of functor instances (if we can pass some parameters to the constructor), like in new Alias('myAlias'). This is readable, this is clear, this can be optimized (see my previous post)

Also, having the ability to pass an argument in the constructor reduces the need for a third $serviceName argument, as you could write: new InvokableFactory('serviceName')

Regarding optimizations:

I don't think we should impose optimizations in the standard. As @weierophinney noted, this can end-up in a counter-productive code.

You cannot force a developer to write optimized code. Some people will write service providers that suck. The important part is that we should communicate on the best practices to respect to optimize service providers. Those best practices could eventually change as containers get more clever.

Finally, I feel that static methods are a dead end regarding performances. We think we are improving performance but we are actually doing the opposite.

@webmozart first spotted this when looking at the interface. He asked us: do we have the equivalent to Symfony tags (i.e. can we create easily a list of services?)

Our answer was that we could "extend" a service that would actually be an array of services. Each service provide could add a service to this list of services.

This is a fairly common use-case. We might want to create a list of log handlers, a list of routers, a list of controllers, a list of actions, a list of twig helpers, a list of commands for a Symfony console, etc...

A typical service provider would look like this:

class MyControllerServiceProvider extends ServiceProvider {
    public function getServices() {
        return [
            MyController::class => [ self::class, 'getMyController' ],
            "controllers" => [ self::class, 'addMyController' ]
        ];
    }

    public static function getMyController() {
        return new MyController();
    }

    // This very generic piece of code will be repeated ad-nauseam in every service provider that must add a service to an array.
    public static function addMyController(ContainerInterface $container, callable $getPrevious = null) {
        // Let's check if a previous value exists. If yes, let's resove it.
        $previous = ($getPrevious === null) ? [] : $getPrevious();

        $previous[] = $container->get(MyController::class);
        return $previous;
    }
}

Now, let's assume we have 20 controllers in the array controllers (added by various service providers).

When my MVC framework will fetch the array controllers, the construction of this array will require:

  • 20 static method calls
  • 20 tests ($getPrevious === null)
  • 19 creation of the $getPrevious() closure
  • 19 executions of the $getPrevious() closure
  • 20 calls to $container->get('xxx') (this is normal and not avoidable)
  • 20 push to the $previous array

This is the most minimal thing we can do to instantiate this controllers array (assuming we are dealing with a cleverly optimized container)

Let's compare this to what could happen with a functor implementing a specially crafted interface

A typical service provider would look like this:

class MyControllerServiceProvider extends ServiceProvider {
    public function getServices() {
        return [
            MyController::class => [ self::class, 'getMyController' ],
            "controllers" => new AddToArray(MyController::class)
        ];
    }

    public static function getMyController() {
        return new MyController();
    }
}

of course, there is an additional AddToArray class:

interface AddToArrayInterface {
    public function getAddedService();
}

class AddToArray implements AddToArrayInterface {
    private $serviceName;

    public function __construct(string $serviceName) {
        $this->serviceName = $serviceName;
    }

    public function getAddedService() {
        return $this->serviceName;
    }

    public function __invoke(ContainerInterface $container, callable $getPrevious = null) {
        // Let's check if a previous value exists. If yes, let's resove it.
        $previous = ($getPrevious === null) ? [] : $getPrevious();

        $previous[] = $container->get($this->serviceName);
        return $previous;
    }
}

Performance-wise:

For a non optimized container:

When my MVC framework will fetch the array controllers, the construction of this array will require:

  • 20 instantiation of service providers
  • 20 calls to getServices method
  • 20 instantiations of the AddToArray class
  • 20 calls to __invoke
  • 20 tests ($getPrevious === null)
  • 19 creation of the $getPrevious() closure
  • 19 executions of the $getPrevious() closure
  • 20 calls to $container->get('xxx') (this is normal and not avoidable)
  • 20 push to the $previous array

So yes, this clearly requires more work than with the static methods.

But let's have a look at the optimizations we can apply.

For an optimized container:

A clever cached/compiled container could:

  • scan all service factories
  • detect all services that are only instances of AddToArrayInterface
  • from the AddToArrayInterface, fetch the service name to add to the array and cache an array of service names (or compile directly the array)

In the end, compiled code could look like this:

$controllers = [
    $container->get('MyController'),
    $container->get('MyOtherController'),
    $container->get('YetAnotherController'),
    ...
];

And this is as optimized as you can ever get:

  • 20 calls to $container->get('xxx') (and that's it!)

You cannot do any better, and this is way way faster than what you can get with static methods.

I know there are a lot of assumptions here (like the fact that we can agree on a common AddToArrayInterface). I know there are risks to split the standard if 2 frameworks decide to go with 2 different set of interfaces. But I'd prefer working on these issues rather than going the "static method" way which seems to be a dead end in terms of optimization (unless someone can come up with an optimization idea that works for static methods).

... plus there are added benefits: service providers are easier to read, there is less duplicated code...

I also know this is harder to implement for container developers, but to me, this is clearly a non-issue because:

  • container can decide not to care for those special interfaces. In this case, they are very very easy to write
  • if containers are performance oriented, we really don't care how hard they are to write. I mean, the whole notion of a compiled container is already a giant hack around a performance problem :)

Finally, there may be completely different reasons to prefer having callables to public static methods. Apart for all the reasons raised by @dragoonis and @Giuseppe-Mazzapica in #5, one issue that comes to my mind was raised by @pmjones. In a mail, he said:

Among other things, the static methods on the service providers are just begging to be used as service locators outside of container code.

I don't want to speak instead of Paul but if I'm understanding is thoughts correctly, I'm sure he would prefer something like:

class MyControllerServiceProvider extends ServiceProvider {
    public function getServices() {
        return [
            MyController::class => function getMyController() {
                return new MyController();
            }
        ];
    }
}

because in the code above, a beginner developer cannot say: "Hey, look, it's easy to get an instance of the controller, just use MyControllerServiceProvider::getMyController()".

@weierophinney @mnapoli I know you are reluctant to this idea. I was too when we first started working on service providers. @stof convinced me to give it a try and see how it goes. After testing it, I'm convinced it has far more potential than going "static methods only". I'd really like to invite you to give it a try, just like I did, and see how it feels. I'm not saying it's flawless, but I think it has a greater potential in the end.

Anyway, I'm keen to have your thoughts on this!

@mnapoli
Copy link
Member

mnapoli commented Apr 22, 2016

@moufmouf this is starting to convince me. And I have exactly these kind of objects in PHP-DI so it would be really easy to integrate. I see the following issues:

  • definition-interop was complicated, with many interfaces, and I'd rather avoid going down that road again. What would be the minimal list of interfaces we would need to offer (e.g. Alias, AddToArray, …)?
  • do we even need those objects to be interfaces? With definition-interop you'd have to pull assembly or some other package implementing the interfaces just to be able to write a module, and there was no big differences between implementations. Such value objects could be standard implementations?
  • allowing callables means allowing at least 5 different types of values (that containers will have to support, and that aren't especially useful for users?). If we need functors, then let's allow only that? Or let's allow functor|Closure (for simplicity's sake)? Then it's easier to encourage using functors when the only alternative are closures?

And just because I have to:

Among other things, the static methods on the service providers are just begging to be used as service locators outside of container code.

I disagree a lot with the fact that it's a problem. Many people said the same about ContainerInterface for example.

@stof
Copy link

stof commented Apr 22, 2016

allowing callables means allowing at least 5 different types of values (that containers will have to support, and that aren't especially useful for users?). If we need functors, then let's allow only that? Or let's allow functor|Closure (for simplicity's sake)? Then it's easier to encourage using functors when the only alternative are closures?

Allowing callables means allowing the callable typehint. Restricting it would introduce restrictions without making it easier for container implementors

do we even need those objects to be interfaces? With definition-interop you'd have to pull assembly or some other package implementing the interfaces just to be able to write a module, and there was no big differences between implementations. Such value objects could be standard implementations?

totally agreed about not needing an interface. These should be simple value objects. And if a container does not support one of them, it will just treat it as a callable thanks to these value objects being invokable, which is the beauty of this proposal. The PSR standard does not even need to be the one shipping these value objects.

@moufmouf
Copy link
Contributor Author

moufmouf commented Apr 22, 2016

  • definition-interop was complicated, with many interfaces, and I'd rather avoid going down that road again. What would be the minimal list of interfaces we would need to offer (e.g. Alias, AddToArray, …)?

I completely agree that the definition-interop scope is way to complete for what we need in practice. So far, I see 3 really common use cases: Alias, AddToArray and Parameter (to insert directly a scalar value in a container without going through the method call)

  • do we even need those objects to be interfaces? With definition-interop you'd have to pull assembly or some other package implementing the interfaces just to be able to write a module, and there was no big differences between implementations. Such value objects could be standard implementations?

What about offering both? Containers should rely on the interface, in case someone wants to write an alternative implementation, and the base package could still offer a default implementation, next to the interface.

Note that I'm not opposed to the idea of only providing an implementation and no interface. This has a nice benefit: one of the shortcomings of implementing funktors with interface is that implementations must be "coherent". For instance, if the AddToArray::__invoke() function adds a service and if AddToArray::getServiceName() returns the name of another service, we are screwed. By providing the one and only implementation, we can ensure both method return matching results.

So I have really no strong opinion on this.

  • allowing callables means allowing at least 5 different types of values (that containers will have to support, and that aren't especially useful for users?). If we need functors, then let's allow only that? Or let's allow functor|Closure (for simplicity's sake)? Then it's easier to encourage using functors when the only alternative are closures?

In my experience, implementing closures in containers is the most difficult task. A solution with only functor|Closure would suit me fine, but in my experience, once you have support for closures, the other types come for free (tell me if this is not the case in PHP-DI). Also, public static functions will still be the fastest when you don't have a functor, so we might not want to get rid of it.

@gmazzap
Copy link

gmazzap commented Apr 22, 2016

Few unordered points:

  • If the standard will enforce to return an array of callable, and suggests to return array of static methods is very fine for me. In many cases I will use just plain functions... easy and performant.
  • Provide functors as implemented classes is ok for me. But I think it would be better to also provide the interfaces and rely on them. Even because if this will ever become a PSR, not sure there classes will be allowed. The issue @moufmouf raised about providing interface could be avoided by removing getAddedService() from the interface, it's not necessary imo (providers never call it). By providing the interface, who really wants to have that getter can write a decorator or extend the shipped class.
  • Regarding the naming of getServices I already said in different other comments (and used in my experimental implementation) the name serviceFactories(). But agree this is a separate topic.

@mnapoli
Copy link
Member

mnapoli commented Apr 22, 2016

👍 thanks for both of your answers.

Regarding allowing callable VS funktor VS functor|Closure:

Allowing callables means allowing the callable typehint.

Of course but containers care about the implementation of such callables. Closures and objects are a pain to deal with for serialized/compiled/cached containers.

Let's summarize:

Classic container Compiled/cached container
optimizable entry (Alias, Parameter…) ++ ++
string: static method on the service provider ++ ++
[string, string]: static method on any object ++ ++
string: method on the service provider ++ +
[object, string]: method on any object ++ +
object: invokable object ++
string: class name of invokable object ++ +
Closure ++
  • : not doable or not optimized at all
  • +: doable but not as optimized as possible (usually requires instantiating an object)
  • ++ doable and optimized

Do you agree with that table?

Then we can go from there and see if we want to eliminate stuff that is incompatible with some containers?

@mnapoli
Copy link
Member

mnapoli commented Apr 22, 2016

@Giuseppe-Mazzapica getAddedService() is the whole point of the object: compiled containers can discard the object and compile the entry into raw PHP code because it knows what __invoke() will do.

@gmazzap
Copy link

gmazzap commented Apr 22, 2016

@mnapoli not an expert of compiled containers, but can't the name of service retrieved by getServices definition?

@mnapoli
Copy link
Member

mnapoli commented Apr 22, 2016

Adding a service to an array means for the current entry (so we know the entry name) which is an array we add a new item to the array which is another service (which is why we need to know the service name).

In pseudo-code:

[
    'event_subscribers' => [
        $container->get('foo')
    ]
]

So we need to know that we are adding foo to the array.

@moufmouf
Copy link
Contributor Author

@mnapoli I do agree with your table (although I would not put "++" everywhere for classic container since they have worse performance than compiled/cached containers).

However, I think your table is not showing the relationship between the options.

If you think about it:

If your container supports optimisable entries (Alias, Parameter...), you have to also support invokable objects. Indeed, let's admit in the future there is a v1.1 of service-provider that adds a new type of object (like AddToPriorityQueue($serviceName, $priority)). Your container might not support it out of the box. So it must support being able to fallback to "invokable objects".

Once you have support for "invokable objects" in your container, you are only one step away from having support to closures (they are really the same kind of beast).

So optimisable entries =(implies support for)=> invokable objects =(almost equivalent to)=> closures.

@gmazzap
Copy link

gmazzap commented Apr 22, 2016

I was referring to this code posted by @moufmouf

class MyControllerServiceProvider extends ServiceProvider {

    public function getServices() {
        return [
            MyController::class => [ self::class, 'getMyController' ],
            "controllers" => new AddToArray(MyController::class)
        ];
    }

    public static function getMyController() {
        return new MyController();
    }
}

and I thought that both "controllers" and MyController::class service names can be retrieved analysing getServices() code, if the Container is compiled statically. On runtime the names are available as keys of the services array, so Containers that caches / compile on runtime, have access to names without looking at AddToArray instances state.

But as I said, I'm not an expert on the field, and is possible i'm saying something terribly wrong.

@gmazzap
Copy link

gmazzap commented Apr 22, 2016

I think I understand the point... when you compile statically you use reflections (I guess) which can't give you the service name...

@moufmouf
Copy link
Contributor Author

@Giuseppe-Mazzapica I'm not sure I understand your thoughts but when you say:

and I thought that both "controllers" and MyController::class service names can be retrieved analysing getServices() code

we definitely don't want to do a statistical analysis of getServices() code (I've been trying this and it is hard as hell and can fail in many many different ways). So we are not going to do some magic with nikic/PHP-Parser, it's never going to work (if that's what you were implying, I'm not sure).

What I'm suggesting here is that compiled/cached containers could search all callables implementing the AddToArray instance. From those services, we can build (using the getAddedService method) the list of all service names supposed to be added in this array. This list can be cached.

@shochdoerfer
Copy link

Is there any reason not to rely on the Delegate lookup feature?

class MyControllerServiceProvider implements \Interop\Container\ContainerInterface
{
    protected $container;

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

    public function getServices()
    {
        return [
            'MyService1',
            'MyService2'
        ];
    }

    public function has($id)
    {
        // ...
    }

    public function get($id)
    {
        // ...
    }
}

@moufmouf
Copy link
Contributor Author

moufmouf commented Apr 24, 2016

Hey Stephan,

Actually, we've been thinking about this. I think that was also suggested by @pmjones, if I recall correctly. The shortcoming with this approach is the way we can deal with "extended" services (i.e. how we do to extend a service declared in another container/service-provider).

Suddenly, that means we should change the get signature to get($id, callable $getPrevious = null), which is a bit weird, and harder to implement for existing containers (most containers don't have this "extended" service notion)

Also, in the current proposal, a service-provider creates the instances but does not need to ensure their unicity (this is the role of the container).

That being said, when we first started thinking about service providers, I tried making them extend ContainerInterface. As I said, I failed to add the "extended service" feature in a clean way. If you think you can come up with an idea that would both extend ContainerInterface and allow for extended services easily, I'm all ears!

@shochdoerfer
Copy link

I think we somehow need a solution for passing the parent container (the one used by the application) to the service providers because the instances configured by the service providers should be able to access to "global" dependency scope. Only that way I am able to pass instances (e.g. a configured Doctrine instance) to one of the classes built by the service provider. Relying on a ContainerInterface instance might be sufficient, given the parent container is aware of the delegate lookup feature. I thought that as the only reason we came up with idea of the delegate lookup :) - Somehow the "configuration code" needs to be merged with the configuration of the application. Returning concrete, configured instances does not seems to solve the problem (for me).

@moufmouf
Copy link
Contributor Author

Hey Stephan,

In this PR, the expected format for a factory method is:

function(ContainerInterface $container, callable $getPrevious = null) {
    // Some code that returns the service/entry
}

This might not be clear, but I always assumed that $container is the "top-most" container. What I mean it that if there are several containers with a root CompositeContainer, then the container passed to the factory method should be the root container.

Does it solve your problem? Should we maybe open a new PR to make it clear that $container is the root container?

@shochdoerfer
Copy link

To be fair it is not easy following the discussion here, the code plus what is is happening in gitter chat. I was just referring to the code samples I saw above and was not sure what you guys where thinking ;)

@moufmouf
Copy link
Contributor Author

Hey @mnapoli ,

Hope the holidays were cool.
I'd like to move forward and create a bunch of service providers for well known packages and I'm a bit stuck because of this pending PR.
Do you think we can move forward with this? We have working implementations in Symfony / Yaco / Simplex and PHP-DI which covers the whole range of container types out there (compiled / cached and runtime containers). Do you see anything holding back this PR? Any comments about the wording or other stuff?

@mnapoli
Copy link
Member

mnapoli commented May 10, 2016

@moufmouf I'm confused though, I think what came up in the latest discussions on Gitter is that allowing any kind of callable will be too complex. Functors are a solution but:

  • allowing all kinds of callables will create issues
  • modules have to use classes (AliasDefinition, etc.) else it's not optimizable: that means by default the standard isn't good for optimum performances. But maybe that's a smaller issue, the first one is much bigger IMO

In any case I feel this PR changes too many things at once. There's one detail that we can merge directly: removing static from getServices(). The text changes are a bit too much at once too, there are unrelated changes and it's hard to discuss all that given we are already discussing many things at once. I think we need to get the opinion of @Ocramius and @weierophinney on the "what getServices() returns" and whether to allow any kind of callables or not.

Quotes from the Gitter discussion I'm re-reading:

Argument against [$obj, 'method'] or 'method' that could be a static or non-static method on the current service provider:

One consistent comment I've seen in the README and the various threads is that we want to make the specification friendly to compiling operations. If we allow instances for the 'class' argument, that goes away. If we allow 'method' to be an instance method, we lose the "minimum viable invocation" example, and add complexity for consuming containers (which now need to determine what are instance methods and what are static).

https://gitter.im/container-interop/definition-interop?at=5717ba7a3307b26736e326ef

Argument against factories as functors:

Since factories are mostly just functions, I don't see a real need for making them objects anyway

https://gitter.im/container-interop/definition-interop?at=5717bb2e98c544f1396cf3ca

I liked the idea of callables, but there's so many potential issues with those, particularly with regards to performance and caching; allowing only static methods as callables makes sense.

https://gitter.im/container-interop/definition-interop?at=5717b979599a529856d9983b

@moufmouf
Copy link
Contributor Author

Hey Matthieu,

In any case I feel this PR changes too many things at once. There's one detail that we can merge directly: removing static from getServices().

I can definitely split this PR in 2. However, I have a hard time figuring out the interest of having a getServices() method that is non static if all factories are still static. I mean, it makes no sense. Either we go non static all the way, or full static. I feel ok splitting the PR in 2 for review purpose, but I would feel bad at stopping in the middle of the bridge. In my opinion, both PR must be merged or none.

One consistent comment I've seen in the README and the various threads is that we want to make the specification friendly to compiling operations. If we allow instances for the 'class' argument, that goes away. If we allow 'method' to be an instance method, we lose the "minimum viable invocation" example, and add complexity for consuming containers (which now need to determine what are instance methods and what are static).

We speak a lot about complexity, but that's really hardly complex. I wanted to test that complexity, so I wrote a bunch of tests. It's really not that hard to keep some code optimized.

In the worst case scenario, it takes ~10 lines of code to handle. So the argument of complexity does not stand the tests.

Since factories are mostly just functions, I don't see a real need for making them objects anyway

Regarding this statement, see my argument above: #20 (comment)
This is really about being able to optimize further the arrays, aliases and parameters.

I liked the idea of callables, but there's so many potential issues with those, particularly with regards to performance and caching; allowing only static methods as callables makes sense.

Regarding caching, this is a non-issue. See my test in PHP-DI: if the factory is a public static method, it is easily cachable. If it is not, we can still cache the index of the service provider and the name of the service (as shown here: https://github.com/moufmouf/PHP-DI/blob/callables/src/DI/Definition/InteropDefinition.php). This is a great way to not put the service provider in the cache (obviously bad), and still get some caching benefits.

Regarding performances: Of course, with non static factories, we need to instantiate the service provider and call the getServices method, but that's up to the service provider developer to write optimized code (i.e. with static methods). We cannot enforce that, only promote best practices like these: http://moufmouf.github.io/service-providers-best-practices/package_developers.html (see last paragraphs for performance best practices). If we enforce static methods, we loose the ability to use funktors like AddToList to extend lists, and that's actually bad for performance (once more, see: #20 (comment)). So really, the argument regarding performance goes both ways.

So, shall I first split the PR in 2 to make it more readable?

@mnapoli
Copy link
Member

mnapoli commented May 13, 2016

OK I want this to move forward too. Let's merge it and switch to allowing callable, and we'll see how it goes.

For the record I disagree with a few things you said, e.g. what I meant about simplicity wasn't about the implementation, but rather about the standard itself (users, module developers understanding it, etc.). The less options we have, the simpler it is. Now users have plenty of options and we have to feed them best practices/a manual: we wouldn't have to if it was really "simple".

And yes in PHP-DI I can cache static calls, but the rest I can't, so it doesn't matter if users don't use that. I don't call "caching the entry key" as caching, it's just not cacheable like static calls, but I can live with that if users follow "best practices". Anyway let's try it out.

I'll be opening further pull requests for discussions afterwards, e.g. one to remove completely the interface and fall back to callable[]. I don't see the point of having an interface and classes except for discoverability (through autoloading), but that could be up to all modules, or we could also use Puli (i.e. plenty of solutions).


By contrast, cached/compiled container will avoid calling `getServices` on each call by caching the results or dumping a PHP file containing the container. Therefore, service provider writers should not rely on the `getServices` method being always called before the factories.

A good approximation is to say that service providers SHOULD be immutable. Once initialized, the state of the service provider should not change.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all that section can be removed, the README is already super long I'd rather keep it as simple as we can. In the end the information here is:

service provider writers should not rely on the getServices method being always called before the factories.

And I don't think it's crucial, if it affected anyone it means there are doing something really wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll remove both "Best practices > Playing nice with all containers" and "Best practices > Performance considerations". I completely agree they don't belong this README. They should be put in a "best practices guide" or whatever we call that.

@moufmouf
Copy link
Contributor Author

Done! Took your comments into account.

I completely agree we needed to shorten the README. Most of its content needs to be put in a best practice guide and we can discuss that later anyway.

If you have no further comment, we can merge this and go for the next round of PR/issues :)

@mnapoli
Copy link
Member

mnapoli commented May 13, 2016

Let's go!

@mnapoli mnapoli merged commit 75a7105 into master May 13, 2016
@mnapoli mnapoli deleted the callables branch May 13, 2016 17:08
@moufmouf
Copy link
Contributor Author

@mnapoli Is it fine with you if we tag a v0.3.0 based on this?

@mnapoli
Copy link
Member

mnapoli commented May 16, 2016

@moufmouf Yes go ahead.

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

Successfully merging this pull request may close these issues.

None yet

6 participants