Skip to content

Dependency injecting callable objects #1487

Closed
@danslo

Description

@danslo

Imagine having a third party library:

class MyLibrary {
    protected $callableClass;

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

    public function run() {
        call_user_func($this->callableClass);
    }
}

Now also imagine we define our own callable object:

class CallableClass {
    public function __invoke() {
        // We are now callable.
        echo 'Hello world';
    }
}

This works fine when injecting dependencies manually:

$lib = new MyLibrary(new CallableClass());
$lib->run();

However, this doesn't work when we use the Magento2 DI system:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <type name="MyLibrary">
        <arguments>
            <argument name="callableClass" xsi:type="object">CallableClass</argument>
        </arguments>
    </type>
</config>

Instead of an instance of CallableClass, Magento 2 will attempt to inject an array:

array (size=1)
      'instance' => string 'CallableClass' (length=13)

which obviously results in something bad:

Recoverable Error: Argument 1 passed to MyLibrary::__construct() must be callable, array given

I never gave you an array, I gave you an object!

I managed to fix this by checking for ReflectionParameter::isCallable in Magento\Framework\Code\Reader::getConstructor:

+++ lib/internal/Magento/Framework/Code/Reader/ClassReader.php
@@ -8,6 +8,23 @@ namespace Magento\Framework\Code\Reader;
 class ClassReader implements ClassReaderInterface
 {
     /**
+     * Determines type from reflection parameter.
+     *
+     * @param \ReflectionParameter $parameter
+     * @return string
+     */
+    protected function getParameterType(\ReflectionParameter $parameter)
+    {
+        $type = null;
+        if ($parameter->getClass() !== null) {
+            $type = $parameter->getClass()->getName();
+        } elseif ($parameter->isCallable()) {
+            $type = 'callable';
+        }
+        return $type;
+    }
+
+    /**
      * Read class constructor signature
      *
      * @param string $className
@@ -26,7 +43,7 @@ class ClassReader implements ClassReaderInterface
                 try {
                     $result[] = [
                         $parameter->getName(),
-                        $parameter->getClass() !== null ? $parameter->getClass()->getName() : null,
+                        $this->getParameterType($parameter),
                         !$parameter->isOptional(),
                         $parameter->isOptional()
                             ? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null)

I am however having difficulties finding the appropriate place to create tests for this. Adding a unit test for this method will be easy, but the DI mechanism itself must be tested as well.

I also haven't exactly found why that object is being wrapped in an array. I assume this is default behavior for things it doesn't know (like callable)?

Input here would be much appreciated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fixed in 2.4.xThe issue has been fixed in 2.4-develop branchIssue: Format is not validGate 1 Failed. Automatic verification of issue format is failed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions