3

We want to add some logic when Magento saves shipping information during checkout, so we have tried using a before plugin for saveAddressInformation method

This would be method definition

/**
 * Save address information.
 *
 * @param int $cartId
 * @param ShippingInformationInterface $addressInformation
 * @return PaymentDetailsInterface
 * @throws InputException
 * @throws NoSuchEntityException
 * @throws StateException
 */
public function saveAddressInformation(
    $cartId,
    ShippingInformationInterface $addressInformation
): PaymentDetailsInterface {

vendor/magento/module-checkout/Model/ShippingInformationManagement.php

So, our plugin looks like this

public function beforeSaveAddressInformation(
    \Magento\Checkout\Model\ShippingInformationManagement $subject,
    $cartId,
    \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation
) {

Question would be, which data should we return during our custom code, if some validation fails? We've tried return $subject; but that throws this error, which I am not sure to understand

CRITICAL: TypeError: Argument 2 passed to Magento\Checkout\Model\ShippingInformationManagement::saveAddressInformation() must implement interface Magento\Checkout\Api\Data\ShippingInformationInterface, instance of Magento\Checkout\Model\PaymentDetailsFactory given, called in .../vendor/magento/framework/Interception/Interceptor.php on line 58 and defined in .../vendor/magento/module-checkout/Model/ShippingInformationManagement.php:158

UPDATE

I have read in devdocs we should return an array including all arguments (which we are not doing by returning only $subject) but if so what I'd expect would be having some error of null given for second parameter, and not that instance of Magento\Checkout\Model\PaymentDetailsFactory

Not sure if I understand what's happening here. Any tips?

2 Answers 2

3

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

2
2

You can try below code:

<?php

namespace Vendor\Module\Plugin;

use Magento\Checkout\Api\Data\PaymentDetailsInterface;
use Magento\Checkout\Api\Data\ShippingInformationInterface;
use Magento\Checkout\Model\ShippingInformationManagement;

class SaveAddress
{
    /**
     * @param ShippingInformationManagement $subject
     * @param int $cartId
     * @param ShippingInformationInterface $addressInformation
     * @return array
     */

    public function beforeSaveAddressInformation(ShippingInformationManagement $subject, $cartId, ShippingInformationInterface $addressInformation)
    {
        // need to add custom logic
        return [$cartId, $addressInformation];
    }
}

Note: Just update class name and namespace

2
  • 1
    Yeah, I'll try... could you add an explanation of the error I am having, so I will understand it better Oct 25, 2022 at 6:45
  • 1
    I think main issue was missing cartId parameter. number of parameter should be same in beforepluign and original class.
    – Pawan
    Oct 25, 2022 at 7:58

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.