I am trying to answer your "Why" question. In order to understand it, you need to know how plugins actually works.
Every plugin we defines via di.xml
file will use the Interceptor trait Magento\Framework\Interception\Interceptor
internally. This Interceptor trait apply all before
and after
and around
plugins defined for a specific method of the subject class.
Below you see the Magento\Framework\Interception\Interceptor::__callPlugins()
method that actually will be called internally in order to subject the method for plugin modifications. I am just quoting the before
logic you find there.
protected function ___callPlugins($method, array $arguments, array $pluginInfo)
{
$subject = $this;
$type = $this->subjectType;
$pluginList = $this->pluginList;
$next = function (...$arguments) use (
$method,
&$pluginInfo,
$subject,
$type,
$pluginList,
&$next
) {
...
$result = null;
if (isset($currentPluginInfo[DefinitionInterface::LISTENER_BEFORE])) {
// Call 'before' listeners
foreach ($currentPluginInfo[DefinitionInterface::LISTENER_BEFORE] as $code) {
$pluginInstance = $pluginList->getPlugin($type, $code);
$pluginMethod = 'before' . $capMethod;
$beforeResult = $pluginInstance->$pluginMethod($this, ...array_values($arguments));
if ($beforeResult !== null) {
$arguments = (array)$beforeResult;
}
}
}
...
return $result;
};
$result = $next(...array_values($arguments));
$next = null;
return $result;
}
Magento Trivia:
Ideally, before plugins expected to return array of original arguments
of the method or a null
.
Now let's debug what happens in your case. You can see magento loops through all before plugins defined. These plugins can modify the arguments as per their need. If a plugin changes the argument, then the plugins in the pipeline will be feed with those changed arguments.
In your case, you return $subject
. This means $beforeResult
is now equals to $subject
. The magic happens in the next if condition.
if ($beforeResult !== null) {
$arguments = (array)$beforeResult;
}
if $beforeResult
is actually an array, then $arguments
will be same as the $beforeResult
. However, if $beforeResult
is an object, then (array)$beforeResult
will give you totally different output which will be an array of the object properties.
To understand this correctly, let's do this plainly with a test class. Consider following code:
class TestClass {
private $property1 = 'my property';
function __construct(string $arg1, string $arg2 = 'test') {}
}
print_r((array)(new TestClass('some')));
you will yield a result:
Array ( [TestClassproperty1] => my property )
This means you still get an array which will be the collection of properties of the object in the context.
So if we apply the same in your context, the $arguments
looks like:
$arguments = Array (
[ShippingInformationManagementPaymentMethodManagementInterface] =>
[ShippingInformationManagementPaymentDetailsFactory] =>
[ShippingInformationManagementCartTotalRepositoryInterface] =>
....
)
Due to this, the next before plugin in the pipeline will have an exact same method definition like yours will be called like below.
$beforeResult = $pluginInstance->$pluginMethod($this, ...array_values($arguments));
Here the plugin first argument is $this
. It is fine. Second argument would be PaymentMethodManagementInterface
which is not correct. But it wont throw error as there is no typecasting for $cartId
. The next argument would be PaymentDetailsFactory
and it finally fails due to the typecasting issue in the before plugin which is in the context that expects ShippingInformationInterface
there. Thus you get the above error.
Take away point
You can pass a null
value if you are not changing the arguments altogether in your plugin. If you altered any of the argument in the before plugin, then what you need to return is the array of original arguments of the method