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