Score:1

Return a Closure from a Factory

cn flag

I have services that depend on request information. I'm trying to create a Factory Factory which will have the Dependency Injection container returns a factory (an anonymous function) that will create the services I want. So basically somthing like this:

class FooFactoryFactory {
  public static function create(ContainerInterface $container): callable {
     return static function (Request $request) use ($container) {
       $raz = request->get('param_raz');
       return new Bar(
           $container->get(LoggerInterface::class),
           $raz,
       );
     };
  }

}

The issue I have is that the Closure is properly created, but de Drupal's Dependency Injection Container class tries to set a proprty to the Closure. This cannot work and ends in a fatal error:

Closure object cannot have properties

Here is the place where it occurs:

// Drupal\Component\DependencyInjection\Container::createService() - Line 283-293
if (isset($definition['properties'])) {
  if ($definition['properties'] instanceof \stdClass) {
    $definition['properties'] = $this->resolveServicesAndParameters($definition['properties']);
  }
  foreach ($definition['properties'] as $key => $value) {
     $service->{$key} = $value;  // <-- Will not work for Closure
  }
}
...

So the questions I have:

  • Is this by design? Meaning is it something thta is not wanted in term of coding practice?
  • The error can be avoided if I manage to make sure, that the service I want to create does not have properties it it's definition. I just don't know how to achieve this.

Thanks for the help on this matter.

EDIT: This is basically what I'm trying to achieve:


  closure.factory.service:
    class: Drupal\some_module\ClosureFactoryService
  closure.service:
    class: Closure
    factory: [ '@closure.factory.service', getCallable ]
class ClosureFactoryService {
  public function getCallable(): Closure {
    return static function (int $arg): string {
      return sprintf('val %d', $arg);
    };
  }
}

I get: Closure object cannot have properties in Drupal\Component\DependencyInjection\Container->createService() (line 288...

4uk4 avatar
cn flag
If your service depends on request information then inject `@request_stack` To understand your design, can you provide the entire code, specifically the Bar class and the service definition causing the error. Why are you using this specific factory method? Can you provide more context?
cn flag
I just edited the question to Have the orignal code as in `Drupal\Component\DependencyInjection\Container.php`. @4k4: that's pretty much it in the example. The services I want to need to have dependencies pulled form the container and depends on the container. Is it something that is not allowed to be implemented that way?
apaderno avatar
us flag
In Drupal, services are defined in a .services.yml file, which is what @4k4 asked to see. Also, a service defined in that file doesn't implement any `create()` method. The Dependency Injection used by Drupal, which is Symfony Dependency Injection, expects `create()` to return an instance of that class, not a closure.
cn flag
Hi @apaderno, thanks for the reply. I didn't mentioned the the services declaration in `*.yml` files because that is not where I'm having issuef. The challenge I have is to return an anonymous (factory) function from the following Drupal's class `Drupal\Component\DependencyInjection\Container.php`. I've posted the piece of code in that class that make it fails. The `Closure` is properly created in the mentioned Drupal class but the next lines makes the `Container.php` throw an error. I'll check the container implementation of Symfony.
apaderno avatar
us flag
That information is necessary; at least, it's necessary to understand if that class is for a service defined in the module's .services.yml file. In this way, the answer can be more useful.
Score:4
us flag

In Drupal, the class for a service defined in a module .services.yml file doesn't need to implement create(ContainerInterface $container). It's not even requested to implement a specific PHP interface.

See one of the services Drupal core implements, for example the path_alias.manager service.

path_alias.manager:
  class: Drupal\path_alias\AliasManager
  arguments:
   - '@path_alias.repository'
   - '@path_alias.whitelist'
   - '@language_manager'
   - '@cache.data'

The AliasManager class that implements that service doesn't implement any create() method; it just implement the constructor, with the parameters defined in the same order the service arguments are listed.

public function __construct($alias_repository, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
  $this->pathAliasRepository = $alias_repository;
  $this->languageManager = $language_manager;
  $this->whitelist = $whitelist;
  $this->cache = $cache;
}

The classes that implement create(ContainerInterface $container) and which implement ContainerInjectionInterface, for example the CronForm class, don't return a closure from create(ContainerInterface $container); they actually only return an instance of themselves. See CronForm::create().

public static function create(ContainerInterface $container) {
  return new static($container->get('config.factory'),
    $container->get('state'),
    $container->get('cron'),
    $container->get('date.formatter'),
    $container->get('module_handler')
  );
}

If you want to implement a factory service in Drupal, you should take the cache_factory service as example to write your code.

cache_factory:
  class: Drupal\Core\Cache\CacheFactory
  arguments:
    - '@settings'
    - '%cache_default_bin_backends%'
  calls:
    - [setContainer, ['@service_container']]

A service that uses that service as factory is, for example, the cache.render service.

cache.render:
  class: Drupal\Core\Cache\CacheBackendInterface
  tags:
    - { name: cache.bin }
  factory:
    - '@cache_factory'
    - get
  arguments:
    - render

The factory key defines which service is the factory service and which method is called for that factory service; the arguments key define the arguments passed to that method. In this case, it's telling Drupal to instantiate the cache.render service by instantiating the cache_factory service and calling get('render') on that object.

cn flag
Thanks for the detailed answer. I've edited my question to picture what I'm trying to achieve. I think it is not possible with the current Drupal Container. Or I'm massively doing something wrong.
Jaypan avatar
de flag
You haven't provided the use-case of what you are trying to do (or problem you're trying to fix/avoid), only the problems you are having in trying to implement it. I'm suspecting that you're taking your previous PHP experience, and trying to apply it to do something that is handled in a different manner using the Drupal framework. Maybe you could explain more about your use-case, rather than your implementation. Otherwise it's hard to say if you're doing it "wrong" (aka - in a non-Drupally manner) or not.
apaderno avatar
us flag
@Jaypan I take dickwan is simply used to how DI works with different libraries/frameworks. For example, in PHP-DI is perfectly fine to have a service factory implemented with a `Closure` object (or any callable object), but the same isn't valid in Drupal.
apaderno avatar
us flag
@dickwan As you found out, Drupal expects it can add properties to the object returned for a service. Since `Closure` objects cannot have properties, they cannot be used. What I described in the answer is the Drupal way to implement a service and a service factory. Keep in mind that instances of a class that implements [`__invoke()`](https://www.php.net/manual/en/language.oop5.magic.php#object.invoke) are callable objects, for PHP, and they can have properties, which is what Drupal expects. If `$callable` is one of those objects, `$result = $callable();` is perfectly valid code.
cn flag
@apaderno: Thanks. I was indeed a bit surprised that the approach I wanted to take was not working. I think I was looking for the reason behing this framwework specific behavior. So I take that Framework is behaving as expected and it's probably not possible to create a service without properties
apaderno avatar
us flag
@dickwan Drupal uses Symfony components, but it doesn't behave like Symfony in every case. Knowing that Drupal uses Symfony can help in understanding how Drupal works, but it still necessary to look at Drupal code, to understand Drupal completely.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.