1

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...

5
  • 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?
    – 4uk4
    Commented Jul 12, 2021 at 11:20
  • 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?
    – dickwan
    Commented Jul 12, 2021 at 11:33
  • 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.
    – avpaderno
    Commented Jul 12, 2021 at 15:28
  • 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.
    – dickwan
    Commented Jul 12, 2021 at 17:10
  • 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.
    – avpaderno
    Commented Jul 13, 2021 at 10:22

1 Answer 1

4

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.

6
  • 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.
    – dickwan
    Commented Jul 18, 2021 at 17:46
  • 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.
    – Jaypan
    Commented Jul 18, 2021 at 18:04
  • @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.
    – avpaderno
    Commented Jul 18, 2021 at 18:07
  • 2
    @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() 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.
    – avpaderno
    Commented Jul 18, 2021 at 18:13
  • @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
    – dickwan
    Commented Jul 19, 2021 at 17:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.