Con Magento2 cambiaron los scripts que usamos para modificar estructura de base de datos o para agregar, modificar y eliminar información (algo habíamos visto en el pasado aquí y aquí).

En Magento 2.3, con bombos y platillos, se dio lugar a Declarative Schema por un lado y a los Data Patch por el otro (y a los Schema Patch también). Para evitar problemas de compatibilidad y dolores de cabeza extra, durante la vida de la rama 2.3, tanto el viejo sistema de scripts como el nuevo siguen funcionando (la recomendación es abandonar tan rápido como sea posible los viejos scripts).

La idea del post no es explicar el data patch por completo. A diferencia de lo que sufrimos vivimos en el pasado, hoy hay documentación bastante clara (a pesar que a veces se contradice a si misma), por lo que me voy a concentrar en cómo revertir un Data Patch de forma correcta (y completa).

La mayoría de los Data Patch que vamos a encontrar serán más o menos así:

<?php

declare(strict_types=1);

namespace Vendor\ModuleName\Setup\Patch\Data;

use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

class PatchName implements DataPatchInterface
{
    /**
     * ModuleDataSetupInterface
     *
     * @var ModuleDataSetupInterface
     */
    private $moduleDataSetup;

    /**
     * @param ModuleDataSetupInterface $moduleDataSetup
     */
    public function __construct(
        ModuleDataSetupInterface $moduleDataSetup
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
    }

    /**
     * Este es el método que se ejecuta efectivamente.
     */
    public function apply()
    {
        $this->moduleDataSetup->getConnection()->startSetup();

        /**
         * Aquí estará el código que hará los cambios en la base de datos
         *
         */
        $this->moduleDataSetup->getConnection()->endSetup();
    }

    /**
     * Podemos aquí indicar un array de otros DataPatch que se necesiten antes.
     * Es muy útil para casos de migraciones y arreglos de datos.
     */
    public static function getDependencies()
    {
        return [];
    }

    /**
     * Un Alias para ser referenciado por otro Patch en un getDependencies.
     */
    public function getAliases()
    {
        return [];
    }
}

La mayoría de los patches tienen esta estructura, y no tiene necesariamente nada malo.

Cuando trabajamos prestando servicios de desarrollo o soporte de tiendas, las idas y vueltas en la base de datos son siempre progresivas, incrementales. Es decir, incluso si tengo que cambiar algo que había hecho antes, no lo deshago primero para luego hacer otra acción.

Distinto es el caso cuando trabajamos con extensiones. Nuestra presencia en la tienda se limitará a la vida del módulo dentro del proyecto y, en caso de ser desinstalado, deberíamos ser tan prolijos como sea posible (controlar el 100% de las variables es imposible).

Si el patch que mostré antes necesitara ser removido habría que hacer dos operaciones manuales:

  1. Revertir cualquier acción que se hubiera ejecutado dentro del método apply().
  2. Eliminar la fila de la tabla patch_list que contiene el nombre del patch (los pacthes que figuran en esa tabla ya no se vuelven a ejecutar).

La buena noticia es que podemos convertir a nuestro patch en un Revertable data patch. El cambio es mínimo, pero no abundan ejemplos.

Primero, vamos a importar la interfase Magento\Framework\Setup\Patch\PatchRevertableInterface y la vamos a implementar en la clase. Luego, vamos a incorporar el método revert().

<?php

declare(strict_types=1);

namespace Vendor\ModuleName\Setup\Patch\Data;

use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;

class PatchName implements DataPatchInterface, PatchRevertableInterface
{
    /**
     * ModuleDataSetupInterface
     *
     * @var ModuleDataSetupInterface
     */
    private $moduleDataSetup;

    /**
     * @param ModuleDataSetupInterface $moduleDataSetup
     */
    public function __construct(
        ModuleDataSetupInterface $moduleDataSetup
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
    }

    /**
     * Este es el método que se ejecuta efectivamente.
     */
    public function apply()
    {
        $this->moduleDataSetup->getConnection()->startSetup();

        /**
         * Aquí estará el código que hará los cambios en la base de datos
         */

        $this->moduleDataSetup->getConnection()->endSetup();
    }

    /**
     * Revert patch
     */
    public function revert()
    {
        $this->moduleDataSetup->getConnection()->startSetup();

        /**
         * Aquí estará el código que hará la operación inversa a lo hecho en apply()
         */

        $this->moduleDataSetup->getConnection()->endSetup();
    }

    /**
     * Podemos aquí indicar un array de otros DataPatch que se necesiten antes.
     * Es muy útil para casos de migraciones y arreglos de datos.
     */
    public static function getDependencies()
    {
        return [];
    }

    /**
     * Un Alias para ser referenciado por otro Patch en un getDependencies.
     */
    public function getAliases()
    {
        return [];
    }
}

Ahora, cuando desinstalemos el módulo ejecutando este comando en la consola:

bin/magento module:uninstall Vendor_ModuleName

O, en caso de no ser un módulo instalado vía Composer, usando:

bin/magento module:uninstall --non-composer Vendor_ModuleName

Se procederá a revertir todo lo que allí hayamos definido. A manera de ejemplo, el output sería:

bin/magento module:uninstall Vendor_ModuleName
You are about to remove code and/or database tables. Are you sure?[y/N] Y
Enabling maintenance mode
You are about to remove a module(s) that might have database data. Do you want to remove the data from database?[y/N] Y
You are removing data without a database backup.
Removing Vendor_ModuleName from module registry in database
Removing Vendor_ModuleName from module list in deployment configuration
Removing code from Magento codebase:
Cache cleared successfully.
Generated classes cleared successfully. Please run the 'setup:di:compile' command to generate classes.
Info: Some modules might require static view files to be cleared. To do this, run 'module:uninstall' with the --clear-static-content option to clear them.
Disabling maintenance mode

Si ahora revisan la tabla patch_list, el patch no figurará. Lo más importante, deberían de haberse ejecutado las operaciones que reviertan los datos introducidos por el patch.

Un ejemplo real lo pueden encontrar en el módulo Mugar_ArgentinaRegions, en el patch AddRegions.

La única restricción que tiene el revert es que no podemos desinstalar un único patch de nuestro módulo. Se desinstalarán todos los patches del módulo, por lo que el uso del getDependencies tiene mucho sentido.

No resulta entonces tan complicado que nuestros módulos limpien en la medida posible sus propios cambios. Be kind, rewind.