Plugin vs Preference en Magento 2

Plugin vs Preference en Magento 2

Si queremos sobreescribir alguna funcionalidad de Magento 2, lo primero que se nos viene a la cabeza es, ¿uso un “plugin” o un “preference”?

Para entenderlo mejor, voy a explicar para qué sirve cada una de las opciones. (Cogeré como ejemplo un módulo que sea JLNarvaez_OverrideModule)

Preference

Un “preference” nos permitirá sobreescribir una clase al completo. Es decir, nuestra clase heredará de la clase que queramos sobreescribir, y prevalecerá la nuestra a la hora de ejecutarse la misma.

¿Cómo declarar un “preference”?

Tendremos que indicarlo en el archivo <module>/etc/di.xml de la siguiente manera:

app/code/JLNarvaez/OverrideModule/etc/di.xml
<config  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Declaración del preference -->
    <preference for="Magento\Checkout\Controller\Onepage\Success" type="JLNarvaez\OverrideModule\Controller\Onepage\Success" />
</config>

En el atributo for indicaremos la clase que queremos sobreescribir y en el atributo type nuestra clase que la sobreescribirá.

Después de esto, crearemos nuestra clase, que hemos indicado en el atributo type en la ruta que hemos indicado anteriormente. En este caso será:

app/code/JLNarvaez/OverrideModule/Controller/Onepage/Success.php
<?php
namespace JLNarvaez\OverrideModule\Controller\Onepage;

/**
 * Onepage checkout success controller custom class
 */
class Success extends \Magento\Checkout\Controller\Onepage\Success
{
    ...
}

Es importante que nuestra clase herede de la clase que vayamos a sobreescribir. En este caso heredaremos de \Magento\Checkout\Controller\Onepage\Success.

En este caso, voy a implementar mi propio método execute().

Este es el método original:

public function execute()
{
    $session = $this->getOnepage()->getCheckout();
    if (!$this->_objectManager->get(\Magento\Checkout\Model\Session\SuccessValidator::class)->isValid()) {
        return $this->resultRedirectFactory->create()->setPath('checkout/cart');
    }
    $session->clearQuote();
    //@todo: Refactor it to match CQRS
    $resultPage = $this->resultPageFactory->create();
    $this->_eventManager->dispatch(
        'checkout_onepage_controller_success_action',
        [
            'order_ids' => [$session->getLastOrderId()],
            'order' => $session->getLastRealOrder()
        ]
    );
    return $resultPage;
}

Por ejemplo, voy a hacer que cuando haga la validación de session (en la segunda línea de la función), si no es correcta, en vez de llevar al carrito, nos lleve a la página principal de nuestra tienda. Este es el resultado de nuestra clase, con el método sobreescrito:

<?php
namespace JLNarvaez\OverrideModule\Controller\Onepage;

/**
 * Onepage checkout success controller custom class
 */
class Success extends \Magento\Checkout\Controller\Onepage\Success
{
    public function execute()
    {
        $session = $this->getOnepage()->getCheckout();
        if (!$this->_objectManager->get(\Magento\Checkout\Model\Session\SuccessValidator::class)->isValid()) {
            return $this->resultRedirectFactory->create()->setPath('/'); // He actualizado esta línea
        }
        $session->clearQuote();
        //@todo: Refactor it to match CQRS
        $resultPage = $this->resultPageFactory->create();
        $this->_eventManager->dispatch(
            'checkout_onepage_controller_success_action',
            [
                'order_ids' => [$session->getLastOrderId()],
                'order' => $session->getLastRealOrder()
            ]
        );
        return $resultPage;
    }
}

Con esto ya tendríamos nuestra clase sobreescrita. Solo nos quedaría ejecutar el comando bin/magento setup:di:compile, ya que cada vez que hagamos cambio en el fichero di.xml tendremos que ejecutar este comando.

Plugin

Un “plugin” (o también conocido como “interceptor”) nos permitirá interceptar una función de una clase, haciendo que un código se ejecute “antes” (before), “después” (after) o “antes y después” (around).

¿Cómo declarar un “plugin”?

También tendremos que indicarlo en el archivo <module>/etc/di.xml de la siguiente manera:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <!-- Declaración del plugin -->
    <type name="Magento\Checkout\Controller\Onepage\Success">
        <plugin name="plugin_personalizado_success" type="JLNarvaez\OverrideModule\Plugin\SuccessPlugin" sortOrder="10" disabled="false"  />
    </type>
</config>

En este caso en el atributo name del elemento <type> pondremos la clase de la cuál queramos crear un plugin. Dentro del elemento <type> tenemos un elemento <plugin> donde pondremos un atributo name que tendrá como valor un nombre único con el que identificaremos a nuestro plugin. En el atributo type pondremos la clase en la cual vamos a crear nuestro plugin.

También tenemos otros atributos como el sortOrder que nos servirá por si tenemos varios plugins que apunten a la misma clase, establecer un orden de ejecución de los mismos. También tendremos la posibilidad de establecer si este plugin lo queremos deshabilitar pasandole el atributo disabled con valor true. (Esto nos será útil por si queremos desactivar algún Plugin en concreto, sin necesidad de tener que borrar la declaración por si en algún futuro nos volviera a servir).

A la hora de crear el plugin, no tendremos que extender de ninguna clase y simplemente tendremos que crear nuestra clase con el método que queramos “sobreescribir”. En este caso queremos usar el método execute() para nuestro plugin. En este momento decidiremos si nuestra lógica se ejecutará antes del método, después o antes y después. Vamos a ver los tres casos:

beforeExecute()

Este método recibirá como primer parámetro la clase sobre la cual estamos haciendo el plugin (La clase del método original). Desde el segundo parámetro en adelante, serán los parámetros que tenga el método original. En este caso, el método execute original no recibe ningún parámetro.

<?php
namespace JLNarvaez\OverrideModule\Plugin\Onepage;

/**
 * Onepage checkout success controller plugin
 */
class SuccessPlugin
{
    public function beforeExecute(\Magento\Checkout\Controller\Onepage\Success $subject)
    {
        if ($this->getRequest()->getParam('redirect_home')) {
            return $this->resultRedirectFactory->create()->setPath('/');
        }
    }
}
afterExecute()

Este método recibirá como primer parámetro la clase sobre la cual estamos haciendo el plugin (La clase del método original). Como segundo parámetro recibirá el resultado de la función original. Desde el tercer parámetro en adelante, serán los parámetros que tenga el método original. En este caso, el método execute original no recibe ningún parámetro.

<?php
namespace JLNarvaez\OverrideModule\Plugin\Onepage;

/**
 * Onepage checkout success controller plugin
 */
class SuccessPlugin
{
    public function afterExecute(\Magento\Checkout\Controller\Onepage\Success $subject, $result)
    {
        
        if ($this->getRequest()->getParam('redirect_home')) {
            return $this->resultRedirectFactory->create()->setPath('/');
        }
        
        return $result;
    }
}
aroundExecute()

Este método recibirá como primer parámetro la clase sobre la cual estamos haciendo el plugin (La clase del método original). Como segundo parámetro recibirá un Callable con la función original (¡Es importante que llamemos a esta función para que se ejecute la función original!) Desde el tercer parámetro en adelante, serán los parámetros que tenga el método original. En este caso, el método execute original no recibe ningún parámetro.

<?php
namespace JLNarvaez\OverrideModule\Plugin\Onepage;

/**
 * Onepage checkout success controller plugin
 */
class SuccessPlugin
{
    public function aroundExecute(\Magento\Checkout\Controller\Onepage\Success $subject, $callable)
    {
        $result = $callable();
        
        if ($this->getRequest()->getParam('redirect_home')) {
            return $this->resultRedirectFactory->create()->setPath('/');
        }
        
        return $result;
    }
}

Tras crear nuestra clase, y declararlo en el di.xml, al igual que al hacer el preference, deberemos de ejecutar el comando bin/magento setup:di:compile.

¿Qué es preferible usar?

Evitaremos usar el preference en la medida de lo posible e intentaremos usar los plugins siempre que podamos. Pero, ¿Por qué?

En el caso de los plugin respetaremos las funciones originales y nuestro código no afectará al código original. Sin embargo, en el caso de los preference estamos machacando todo el código de la función. El problema de esto es que, por ejemplo, una actualización de Magento que cambie el código de este método, hará que nos perdamos estas nuevas cosas que se añadan, ya que el método de nuestra clase se ejecutará por encima del código original.

El problema de crear plugins es que tiene unas cuantas limitaciones. No se podrán crear plugins sobre lo siguiente:

  • Métodos no públicos.
  • Clases “final”
  • Métodos “final”
  • Métodos __construct y __destruct
  • Métodos estáticos
  • Virtual types

También se tendrá que dar el caso de que nuestra lógica sea imposible implementarla a través de un plugin, e implique cambios en varias líneas de código del método original.

Jose Luis
Jose Luis
Magento 2 Certified Developer