Skip to content

Sistema de Tipos de Migraciones - Documentación Técnica

Módulo: Migrations Componente: Migration Type Classification System Versión: 3.14.0 Fecha: 2026-02-11


Descripción

Sistema de categorización de migraciones de base de datos que permite clasificar las ~300 migraciones del proyecto según su propósito: cambios estructurales (BASE), operaciones de datos (TRANSACCIONAL), o configuración del sistema (CONFIG).

Esta categorización habilita:

  • Ejecución ordenada en scripts de deployment (BASE → TRANS → CONFIG)
  • Rollback selectivo por categoría de migración
  • Validación automática de tipos
  • Mejor organización y documentación del proyecto
  • Control de portabilidad de datos entre schemas del sistema multi-tenant

Portabilidad de Datos entre Schemas

Los tipos de migración determinan si los datos de una tabla pueden moverse entre diferentes schemas:

TipoPortabilidadJustificación
BASE 🏗️PORTABLESTablas maestras/catálogos compartidos (productos, clientes, provincias) - pueden centralizarse o distribuirse según necesidad
TRANSACCIONAL 🔄NO PORTABLESTransacciones comerciales con trazabilidad fiscal/contable ligadas a ubicación específica (facturas, movimientos)
CONFIG ⚙️NO PORTABLESConfiguraciones particulares de cada schema (numeradores, permisos, parámetros específicos por ubicación)

Implicancia práctica: El sistema de consolidación de schemas (ZZSchemaDataConsolidation) solo puede mover tablas BASE entre niveles jerárquicos. Las tablas TRANSACCIONAL y CONFIG permanecen en su schema de origen.

Características principales:

  • Enum MigrationType con 3 tipos (BASE, TRANSACCIONAL, CONFIG)
  • Método abstracto getMigrationType() en ConfigurableMigration (obliga definición)
  • API pública MigrationTypeDetector para scripts externos
  • Validador recursivo MigrationTypeValidator
  • Clasificador automático con heurísticas (MigrationTypeClassifier)
  • Integración completa en CLI con flags --validate-types y --migration-type

ADRs (Architecture Decision Records)

ADR-001: Enum PHP 8 sobre Constantes

Contexto: Necesitamos categorizar migraciones con tipos limitados y seguros.

Decisión: Usar enum MigrationType: string de PHP 8.1+ en lugar de constantes de clase.

Razones:

  1. Type safety: El sistema de tipos de PHP garantiza valores válidos en compile-time
  2. IDE support: Autocomplete y refactoring automático
  3. Métodos útiles: getDescription(), getEmoji(), getFullDescription()
  4. Pattern matching: match() expression con exhaustiveness checking

Implementación:

php
enum MigrationType: string
{
    case BASE = 'base';
    case TRANSACCIONAL = 'transaccional';
    case CONFIG = 'config';
}

Trade-offs:

  • ➕ Seguridad en tipos garantizada por PHP
  • ➕ Código más limpio y expresivo
  • ➖ Requiere PHP 8.1+ (ya requerido en el proyecto)

ADR-002: Método Abstracto sobre Atributo PHP

Contexto: Podemos usar método abstracto getMigrationType() o atributo #[MigrationTypeAttribute] para definir tipos.

Decisión: Método abstracto como mecanismo principal, atributo como fallback opcional.

Razones:

  1. Mandatory: El método abstracto obliga a todas las migraciones a definir tipo
  2. Compile-time check: PHP valida que todas las clases implementen el método
  3. Runtime flexibility: Permite lógica condicional si fuera necesario
  4. Backward compatibility: Se puede detectar tipo desde atributo como fallback

Prioridad de detección (en MigrationTypeDetector):

  1. Método getMigrationType() (prioridad alta)
  2. Atributo #[MigrationTypeAttribute] (fallback)

Trade-offs:

  • ➕ Imposible olvidar definir tipo (error en ejecución si se intenta extender ConfigurableMigration)
  • ➕ Flexibilidad para futuras necesidades (ej: tipo dinámico basado en condición)
  • ➖ Más verbose que un atributo (1 método vs 1 línea)

ADR-003: Clasificador Automático con Heurísticas

Contexto: Tenemos ~300 migraciones legacy sin tipo definido.

Decisión: MigrationTypeClassifier con análisis estático del código fuente usando regex patterns.

Razones:

  1. Escalabilidad: Clasificar 300 archivos en minutos vs días manual
  2. Consistencia: Heurísticas uniformes para todas las migraciones
  3. Confianza medible: Score de confianza (high/medium/low) para revisión manual
  4. Automatización completa: --scan → --apply sin intervención

Heurísticas implementadas:

  • Path/nombre con "seed" o "config" → CONFIG (score +10)
  • Operaciones DDL (CREATE/ALTER/DROP TABLE) → BASE (score +3 por match)
  • Operaciones DML (INSERT/UPDATE/DELETE) → TRANSACCIONAL (score +2 por match)
  • Métodos Phinx estructura (addIndex, addForeignKey) → BASE (score +3 por método)
  • Docblock @migration-type → Tipo explícito (score +20)

Trade-offs:

  • ➕ Ahorro de tiempo masivo (horas vs semanas)
  • ➕ Clasificación revisable antes de aplicar (dry-run mode)
  • ➖ Falsos positivos posibles en migraciones complejas (mitigado con confianza baja)

Estadísticas finales (aplicado en v3.14.0):

  • 117 migraciones BASE (95.1%)
  • 6 migraciones TRANSACCIONAL (4.9%)
  • 0 migraciones CONFIG (0%)
  • Total: 123 migraciones clasificadas

ADR-004: Integración en Logger con Métricas por Tipo

Contexto: El sistema de logging existente registra migraciones sin categorización.

Decisión: Actualizar Logger.php con métodos específicos que incluyen tipo de migración.

Razones:

  1. Trazabilidad: Logs estructurados permiten filtrar por tipo en producción
  2. Métricas: Dashboards pueden mostrar duración por tipo de migración
  3. Debugging: Identificar rápidamente qué tipo de migración causó un error
  4. Auditoría: Cumplimiento con políticas que requieren separación de DDL/DML

Nuevos métodos:

php
Logger::logMigrationStartWithType(string $name, string $schema, MigrationType $type)
Logger::logMigrationMetricsByType(array $stats) // ['base' => X, 'transaccional' => Y, 'config' => Z]

Trade-offs:

  • ➕ Mejor observabilidad en producción
  • ➕ Compatibilidad con herramientas de monitoreo (DataDog, New Relic)
  • ➖ Logs más grandes (campo adicional por migración)

ADR-005: CLI Flags para Ejecución Selectiva

Contexto: Deployment scripts necesitan ejecutar migraciones en orden específico.

Decisión: Flags --migration-type=TYPE y --validate-types en migrate-db-command.php.

Razones:

  1. Deployment control: Scripts pueden ejecutar BASE → TRANSACCIONAL → CONFIG en stages separados
  2. Validation gate: deployment scripts puede validar tipos antes de merge (--validate-types)
  3. Debugging: Desarrolladores pueden ejecutar solo migraciones BASE para probar estructura
  4. Rollback selectivo: Rollback solo de migraciones de datos sin afectar estructura

Ejemplos de uso:

bash
# Validación (deployment scripts)
php migrate-db-command.php --validate-types

# Deployment ordenado (staging/production)
php migrate-db-command.php --migrate --migration-type=base
php migrate-db-command.php --migrate --migration-type=transaccional
php migrate-db-command.php --migrate --migration-type=config

Trade-offs:

  • ➕ Control fino para DevOps
  • ➕ Menor riesgo en deployments (ejecutar estructura antes que datos)
  • ➖ Comandos más largos (mitigado con scripts de deployment)

Arquitectura

Componentes del Sistema

MigrationType (Enum)
    ↓ define 3 casos
    ├─ BASE: Cambios estructurales
    ├─ TRANSACCIONAL: Operaciones de datos
    └─ CONFIG: Configuración

ConfigurableMigration (Abstract Class)
    ↓ método abstracto obligatorio
    getMigrationType(): MigrationType

MigrationTypeDetector (API Pública)
    ├─ getTypeFromInstance(object): MigrationType|null
    ├─ getTypeFromAttribute(object|string): MigrationType|null
    ├─ filterByType(array, MigrationType): array
    ├─ validate(object): array
    └─ getStatistics(array): array

MigrationTypeValidator (Validador Recursivo)
    ├─ validateAll(): array
    ├─ scanMigrationFiles(): array
    └─ extractClassName(string): string|null

MigrationTypeClassifier (Clasificador Automático)
    ├─ scanAll(): array
    ├─ classifyFile(string): array
    ├─ generatePatch(array, string): void
    └─ applyPatch(string, bool): array

migrate-db-command.php (CLI)
    ├─ --validate-types (Validar todos)
    └─ --migration-type=<tipo> (Filtrar ejecución)

Logger (Logging)
    ├─ logMigrationStartWithType()
    └─ logMigrationMetricsByType()

Flujo de Detección de Tipo

ConfigurableMigration instance

MigrationTypeDetector::getTypeFromInstance()

    ├─ Prioridad 1: Método getMigrationType()
    │     ├─ Reflection API para acceder a método (puede ser protected)
    │     ├─ Invocar método y obtener MigrationType
    │     └─ Retornar si existe

    ├─ Prioridad 2: Atributo #[MigrationTypeAttribute]
    │     ├─ Reflection API para obtener atributos de clase
    │     ├─ Instanciar atributo si existe
    │     └─ Retornar tipo del atributo

    └─ Retornar null si no se encuentra tipo

Flujo de Clasificación Automática

MigrationTypeClassifier::scanAll()

    ├─ Escanear recursivamente migrations/
    ├─ Excluir CustomMigration, ConfigurableMigration

    Para cada archivo:
        ├─ classifyFile(file)
        │     ├─ Analizar path (seeds/ → CONFIG)
        │     ├─ Regex: DDL patterns → BASE
        │     ├─ Regex: DML patterns → TRANSACCIONAL
        │     ├─ Regex: Phinx methods → BASE
        │     ├─ Docblock: @migration-type → Explícito
        │     ├─ Calcular scores por tipo
        │     └─ Determinar ganador + confianza

    Retornar array con clasificaciones

generatePatch(classifications, output.json)

    Guardar JSON con:
        ├─ file path
        ├─ suggested_type
        ├─ confidence (high/medium/low)
        └─ reasons (explicaciones)

applyPatch(patch.json, dry-run=false)

    Para cada migración:
        ├─ Verificar archivo existe
        ├─ Verificar ya tiene getMigrationType()
        │     └─ Skip si ya tiene
        ├─ Agregar use App\MigrationType;
        ├─ Inyectar método getMigrationType() después de getDefaultLevels()
        └─ Guardar archivo modificado

Tipos de Migraciones

BASE 🏗️ - Migraciones de Estructura (PORTABLES)

Propósito: Cambios en la estructura de la base de datos (esquema DDL).

Portabilidad: ✅ PORTABLES - Las tablas BASE contienen datos maestros/catálogos compartidos que PUEDEN moverse entre schemas sin perder su función. Son tablas base del negocio (productos, clientes compartidos, provincias, etc.)

Casos de uso:

  • Crear nuevas tablas (CREATE TABLE)
  • Modificar tablas existentes (ALTER TABLE ADD COLUMN, ALTER TABLE DROP COLUMN)
  • Agregar índices para performance (CREATE INDEX)
  • Definir foreign keys (ADD FOREIGN KEY)
  • Agregar constraints (ADD CONSTRAINT)
  • Crear secuencias (CREATE SEQUENCE)
  • Modificar tipos de columnas (ALTER COLUMN TYPE)

Ejemplos de tablas portables:

  • producto - Catálogo de productos compartido
  • ordcon - Clientes compartidos entre sucursales
  • provincia - Datos maestros geográficos
  • aliva - Alícuotas IVA compartidas
  • rubro - Rubros de productos

Ejemplos del proyecto:

  1. Crear tabla nueva (migrations/tenancy/20240101000000_create_producto_table.php):
php
final class CreateProductoTable extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'producto';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA, self::LEVEL_SUCURSAL];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::BASE; // 🏗️ Estructura
    }

    public function change(): void
    {
        $this->initSearchPath();
        $this->table('producto')
            ->addColumn('codigo', 'string', ['limit' => 50])
            ->addColumn('nombre', 'string', ['limit' => 200])
            ->addColumn('precio', 'decimal', ['precision' => 10, 'scale' => 2])
            ->addIndex(['codigo'], ['unique' => true])
            ->create();
    }
}
  1. Agregar índice (migrations/tenancy/20240201000000_add_index_producto_nombre.php):
php
final class AddIndexProductoNombre extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'producto';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA, self::LEVEL_SUCURSAL];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::BASE; // 🏗️ Índice de performance
    }

    public function change(): void
    {
        $this->initSearchPath();
        $this->table('producto')
            ->addIndex(['nombre'], ['name' => 'idx_producto_nombre'])
            ->update();
    }
}
  1. Agregar foreign key (migrations/tenancy/20240301000000_add_fk_factura_cliente.php):
php
final class AddFkFacturaCliente extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'factura';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_SUCURSAL, self::LEVEL_CAJA];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::BASE; // 🏗️ Relación estructural
    }

    public function change(): void
    {
        $this->initSearchPath();
        $this->table('factura')
            ->addForeignKey('cliente_id', 'cliente', 'id', [
                'delete' => 'RESTRICT',
                'update' => 'CASCADE'
            ])
            ->update();
    }
}

Cuándo elegir BASE:

  • ✅ Si tu migración usa CREATE, ALTER, DROP sobre tablas
  • ✅ Si agregas índices, constraints, o foreign keys
  • ✅ Si modificas tipos de columnas o estructura de tabla
  • ✅ Si creas secuencias, views, o funciones PostgreSQL

TRANSACCIONAL 🔄 - Migraciones de Datos (NO PORTABLES)

Propósito: Operaciones masivas de inserción, actualización o eliminación de datos (DML).

Portabilidad: ❌ NO PORTABLES - Las tablas TRANSACCIONAL contienen transacciones comerciales que NO se pueden mover entre schemas porque están ligadas específicamente a una ubicación (sucursal/caja) y tienen trazabilidad fiscal/contable.

Casos de uso:

  • Migrar datos entre tablas (INSERT INTO ... SELECT FROM)
  • Actualizar masivamente registros (UPDATE ... SET WHERE)
  • Transformar datos legacy a nuevo formato
  • Eliminar datos obsoletos (DELETE FROM WHERE)
  • Migrar datos entre schemas (multi-tenant)
  • Corregir datos inconsistentes

Ejemplos de tablas NO portables:

  • factura - Facturas por punto de venta específico
  • movimi - Movimientos de tesorería por ubicación
  • itefac - Ítems de factura vinculados a transacción específica
  • recfac - Recibos de factura con trazabilidad contable
  • mov_sto - Movimientos de stock con origen/destino específico

Ejemplos del proyecto:

  1. Migrar datos entre tablas (migrations/tenancy/20240401000000_migrate_cliente_legacy_to_new.php):
php
final class MigrateClienteLegacyToNew extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'cliente_nuevo';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::TRANSACCIONAL; // 🔄 Migración de datos
    }

    public function up(): void
    {
        $this->execute("
            INSERT INTO cliente_nuevo (codigo, razon_social, cuit, activo)
            SELECT
                codigo_legacy,
                nombre,
                REGEXP_REPLACE(cuit, '[^0-9]', '', 'g') as cuit_limpio,
                CASE WHEN estado = 'A' THEN true ELSE false END
            FROM cliente_legacy
            WHERE deleted_at IS NULL
        ");
    }

    public function down(): void
    {
        $this->execute("DELETE FROM cliente_nuevo WHERE migrated = true");
    }
}
  1. Actualizar datos masivamente (migrations/tenancy/20240501000000_update_producto_precio_iva.php):
php
final class UpdateProductoPrecioIva extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'producto';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::TRANSACCIONAL; // 🔄 Actualización masiva
    }

    public function up(): void
    {
        $this->execute("
            UPDATE producto
            SET precio_con_iva = ROUND(precio * 1.21, 2)
            WHERE precio_con_iva IS NULL
        ");
    }
}
  1. Eliminar datos obsoletos (migrations/tenancy/20240601000000_delete_factura_cancelled_old.php):
php
final class DeleteFacturaCancelledOld extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'factura';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_SUCURSAL];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::TRANSACCIONAL; // 🔄 Eliminación de datos
    }

    public function up(): void
    {
        $this->execute("
            DELETE FROM factura
            WHERE estado = 'CANCELADA'
              AND fecha < '2020-01-01'
              AND deleted_at IS NULL
        ");
    }
}

Cuándo elegir TRANSACCIONAL:

  • ✅ Si tu migración usa INSERT, UPDATE, DELETE masivos
  • ✅ Si migra datos de tablas legacy a nuevas tablas
  • ✅ Si transforma datos existentes (formateo, limpieza)
  • ✅ Si corrige inconsistencias en datos históricos

CONFIG ⚙️ - Migraciones de Configuración (NO PORTABLES)

Propósito: Datos de configuración del sistema, seeds, o datos de referencia.

Portabilidad: ❌ NO PORTABLES - Las tablas CONFIG contienen configuraciones específicas por schema/empresa que NO se pueden mover porque los valores configurados son particulares de cada ubicación (numeradores, parámetros, permisos específicos).

Casos de uso:

  • Cargar datos iniciales requeridos (bootstrap data)
  • Insertar datos de referencia (catálogos, listas)
  • Configuraciones por defecto del sistema
  • Permisos y roles iniciales
  • Datos maestros (provincias, monedas, tipos de comprobante)

Ejemplos de tablas NO portables:

  • data_config - Configuraciones específicas por sucursal
  • permisos - Permisos configurados por empresa
  • numeradores - Numeración de comprobantes por ubicación
  • grupos - Grupos de usuarios configurados por empresa

Ejemplos del proyecto:

  1. Seed de provincias (migrations/seeds/argentina_provincias.php):
php
final class ArgentinaProvincias extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'provincia';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::CONFIG; // ⚙️ Datos de referencia
    }

    public function up(): void
    {
        $provincias = [
            ['codigo' => 'BA', 'nombre' => 'Buenos Aires'],
            ['codigo' => 'CABA', 'nombre' => 'Ciudad Autónoma de Buenos Aires'],
            ['codigo' => 'CO', 'nombre' => 'Córdoba'],
            // ... resto de provincias
        ];

        $this->table('provincia')->insert($provincias)->saveData();
    }
}
  1. Seed de permisos (migrations/seeds/permisos_default.php):
php
final class PermisosDefault extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'permiso';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::CONFIG; // ⚙️ Configuración inicial
    }

    public function up(): void
    {
        $permisos = [
            ['modulo' => 'ventas', 'accion' => 'crear_factura', 'descripcion' => 'Crear facturas'],
            ['modulo' => 'ventas', 'accion' => 'anular_factura', 'descripcion' => 'Anular facturas'],
            ['modulo' => 'compras', 'accion' => 'aprobar_orden', 'descripcion' => 'Aprobar órdenes de compra'],
            // ... resto de permisos
        ];

        $this->table('permiso')->insert($permisos)->saveData();
    }
}
  1. Configuración del sistema (migrations/seeds/data_config_initial.php):
php
final class DataConfigInitial extends ConfigurableMigration
{
    protected function getTableName(): string
    {
        return 'data_config';
    }

    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA];
    }

    protected function getMigrationType(): MigrationType
    {
        return MigrationType::CONFIG; // ⚙️ Configuración
    }

    public function up(): void
    {
        $configs = [
            ['clave' => 'moneda.default', 'valor' => 'ARS', 'help' => 'Moneda por defecto del sistema'],
            ['clave' => 'factura.numeracion.inicio', 'valor' => '1', 'help' => 'Número inicial de facturación'],
            ['clave' => 'sistema.iva.default', 'valor' => '21', 'help' => 'IVA por defecto en %'],
        ];

        $this->table('data_config')->insert($configs)->saveData();
    }
}

Cuándo elegir CONFIG:

  • ✅ Si tu archivo está en directorio seeds/
  • ✅ Si nombre incluye "seed", "config", "inicial", "setup"
  • ✅ Si carga datos maestros o de referencia
  • ✅ Si configura valores por defecto del sistema

Decision Tree: ¿Qué Tipo Elegir?

┌─ ¿Tu migración crea/modifica ESTRUCTURA de tablas?

├─ SÍ → ¿Usa CREATE/ALTER/DROP TABLE/INDEX/CONSTRAINT?
│      │
│      └─ SÍ → MigrationType::BASE 🏗️

├─ NO → ¿Tu migración INSERT/UPDATE/DELETE DATOS masivamente?
│       │
│       ├─ SÍ → ¿Son datos de referencia o configuración?
│       │       │
│       │       ├─ SÍ → MigrationType::CONFIG ⚙️
│       │       │
│       │       └─ NO → MigrationType::TRANSACCIONAL 🔄
│       │
│       └─ NO → Por defecto → MigrationType::BASE 🏗️

Casos especiales:

SituaciónTipo CorrectoJustificaciónPortable
Migración crea tabla Y carga datos inicialesBASELa estructura es lo crítico; datos pueden ser seed aparte
Migración solo carga catálogo (provincias, monedas)CONFIGDatos de referencia sin lógica de negocio
Migración corrige CUIT mal formateadosTRANSACCIONALTransformación de datos existentes
Migración agrega columna nueva con valor defaultBASECambio estructural (default es parte del schema)
Migración migra datos legacy de tabla vieja a nuevaTRANSACCIONALOperación de datos masiva
Tabla de productos/clientes compartidos entre sucursalesBASECatálogo maestro que puede centralizarse
Tabla de facturas por punto de ventaTRANSACCIONALTransacción con trazabilidad fiscal específica
Tabla de configuración de numeradores por sucursalCONFIGParámetros específicos de ubicación

Implementación para Desarrolladores

Crear Nueva Migración con Tipo

Template completo:

php
<?php

declare(strict_types=1);

use App\migrations\ConfigurableMigration;
use App\MigrationType;

/**
 * [Descripción de la migración]
 *
 * Nivel: EMPRESA | SUCURSAL | CAJA
 * Tipo: BASE | TRANSACCIONAL | CONFIG
 *
 * @since 3.14.0
 */
final class MiNuevaMigracion extends ConfigurableMigration
{
    /**
     * Nombre de la tabla principal afectada.
     */
    protected function getTableName(): string
    {
        return 'mi_tabla'; // MANDATORY
    }

    /**
     * Niveles de schema donde se ejecuta esta migración.
     */
    protected function getDefaultLevels(): array
    {
        return [self::LEVEL_EMPRESA, self::LEVEL_SUCURSAL]; // MANDATORY
    }

    /**
     * Tipo de migración: BASE, TRANSACCIONAL, o CONFIG.
     */
    protected function getMigrationType(): MigrationType
    {
        return MigrationType::BASE; // MANDATORY - Elegir según Decision Tree
    }

    /**
     * Ejecutar migración (up).
     */
    public function change(): void
    {
        $this->initSearchPath(); // REQUIRED para multi-tenancy

        // Tu lógica de migración aquí
        $this->table('mi_tabla')
            ->addColumn('nueva_columna', 'string', ['limit' => 100])
            ->update();
    }

    /**
     * Verificar si debe ejecutarse (opcional).
     */
    public function shouldExecute(): bool
    {
        return $this->shouldRunOnLevel() && $this->haveTable('mi_tabla');
    }
}

Checklist para Nuevas Migraciones

  • [ ] Extender ConfigurableMigration (no CustomMigration)
  • [ ] Implementar getTableName() con nombre de tabla principal
  • [ ] Implementar getDefaultLevels() con niveles de schema
  • [ ] Implementar getMigrationType() consultando Decision Tree
  • [ ] Agregar docblock con descripción, nivel, y tipo
  • [ ] Ejecutar --validate-types localmente
  • [ ] Probar en ambiente de desarrollo con --dry-run

API para Procesos Externos

MigrationTypeDetector

API pública para scripts de deployment, deployment scripts, y herramientas externas.

getTypeFromInstance(object): MigrationType|null

Detecta el tipo desde una instancia de migración.

php
use App\MigrationTypeDetector;

// Instanciar migración
$migration = new CreateProductoTable();

// Detectar tipo
$type = MigrationTypeDetector::getTypeFromInstance($migration);

if ($type === null) {
    throw new Exception("Migración sin tipo definido");
}

echo $type->value; // "base"
echo $type->getEmoji(); // "🏗️"
echo $type->getDescription(); // "Migración de estructura..."

filterByType(array, MigrationType): array

Filtra array de migraciones por tipo específico.

php
use App\MigrationTypeDetector;
use App\MigrationType;

// Array de instancias de migraciones
$allMigrations = [
    new CreateProductoTable(),
    new MigrateClienteLegacy(),
    new SeedProvincias(),
];

// Filtrar solo migraciones BASE
$baseMigrations = MigrationTypeDetector::filterByType(
    $allMigrations,
    MigrationType::BASE
);

// Ejecutar solo BASE
foreach ($baseMigrations as $migration) {
    $migration->up();
}

validate(object): array

Valida que una migración tenga tipo definido.

php
use App\MigrationTypeDetector;

$migration = new MiMigracion();

$result = MigrationTypeDetector::validate($migration);

if (!$result['valid']) {
    echo "❌ Errores:\n";
    foreach ($result['errors'] as $error) {
        echo "  - $error\n";
    }
} else {
    echo "✅ Migración válida: {$result['type']->value}\n";
}

Formato de retorno:

php
[
    'valid' => bool,           // true si tiene tipo definido
    'errors' => string[],      // Array de mensajes de error
    'type' => MigrationType|null  // Tipo detectado o null
]

getStatistics(array): array

Obtiene estadísticas de tipos de un array de migraciones.

php
use App\MigrationTypeDetector;

$allMigrations = [...]; // Array de instancias

$stats = MigrationTypeDetector::getStatistics($allMigrations);

echo "📊 Estadísticas:\n";
echo "  BASE: {$stats['base']}\n";
echo "  TRANSACCIONAL: {$stats['transaccional']}\n";
echo "  CONFIG: {$stats['config']}\n";
echo "  Sin tipo: {$stats['without_type']}\n";

Formato de retorno:

php
[
    'base' => int,
    'transaccional' => int,
    'config' => int,
    'without_type' => int
]

MigrationTypeValidator

Validador recursivo para escanear directorios y validar tipos.

validateAll(): array

Escanea recursivamente el directorio de migraciones y valida tipos.

php
use App\MigrationTypeValidator;

$validator = new MigrationTypeValidator(__DIR__ . '/migrations');
$result = $validator->validateAll();

if (!$result['valid']) {
    echo "❌ Errores de validación:\n";
    foreach ($result['errors'] as $file => $reason) {
        echo "  - $file: $reason\n";
    }
} else {
    echo "✅ Todas las {$result['total']} migraciones tienen tipo definido\n";
}

echo "\n📊 Estadísticas:\n";
echo "  BASE: {$result['stats']['base']}\n";
echo "  TRANSACCIONAL: {$result['stats']['transaccional']}\n";
echo "  CONFIG: {$result['stats']['config']}\n";

Formato de retorno:

php
[
    'valid' => bool,              // true si todas tienen tipo
    'total' => int,               // Total de archivos escaneados
    'errors' => [                 // Errores por archivo
        'path/file.php' => 'razón del error'
    ],
    'stats' => [                  // Estadísticas por tipo
        'base' => int,
        'transaccional' => int,
        'config' => int,
        'without_type' => int
    ]
]

CLI y Comandos

Flag: --validate-types

Propósito: Validar que todas las migraciones tengan tipo definido.

Uso en deployment scripts:

bash
php migrations/migrate-db-command.php --validate-types

# Exit code 0 si todas válidas, 1 si hay errores

Salida de ejemplo:

🔍 Validando tipos de migraciones...

✅ Todas las migraciones tienen tipo definido

📊 Estadísticas:
  🏗️  BASE: 117
  🔄 TRANSACCIONAL: 6
  ⚙️  CONFIG: 0

Total: 123 migraciones

Salida con errores:

🔍 Validando tipos de migraciones...

❌ Errores de validación:

  - migrations/tenancy/20240101000000_create_producto.php:
    La migración CreateProducto no tiene método getMigrationType() definido

  - migrations/tenancy_p/20240201000000_migrate_cliente.php:
    La migración MigrateCliente no tiene método getMigrationType() definido

📊 Estadísticas:
  BASE: 115
  TRANSACCIONAL: 6
  CONFIG: 0
  Sin tipo: 2

Total: 123 migraciones

Flag: --migration-type=TYPE

Propósito: Ejecutar solo migraciones de un tipo específico.

Valores válidos: base, transaccional, config

Uso en deployment:

bash
# Deployment ordenado (recomendado)

# Paso 1: Ejecutar migraciones de estructura
php migrations/migrate-db-command.php --migrate --migration-type=base

# Paso 2: Ejecutar migraciones de datos
php migrations/migrate-db-command.php --migrate --migration-type=transaccional

# Paso 3: Ejecutar migraciones de configuración
php migrations/migrate-db-command.php --migrate --migration-type=config

Ventajas del deployment ordenado:

  1. Seguridad: Estructura lista antes de migrar datos
  2. Debugging: Fácil identificar en qué stage falló
  3. Rollback: Rollback selectivo por tipo
  4. Performance: Separar operaciones pesadas (migraciones de datos)

Ejemplo de script de deployment (deploy-migrations.sh):

bash
#!/bin/bash

set -e # Exit on error

echo "🚀 Deployment de migraciones - Sistema Bautista"
echo "=============================================="

# Paso 1: Validar tipos
echo ""
echo "🔍 Paso 1: Validando tipos de migraciones..."
php migrations/migrate-db-command.php --validate-types
if [ $? -ne 0 ]; then
    echo "❌ Validación falló. Abortando deployment."
    exit 1
fi
echo "✅ Validación exitosa"

# Paso 2: Ejecutar migraciones BASE
echo ""
echo "🏗️  Paso 2: Ejecutando migraciones BASE (estructura)..."
php migrations/migrate-db-command.php --migrate --migration-type=base --oficial
if [ $? -ne 0 ]; then
    echo "❌ Migraciones BASE fallaron. Abortando deployment."
    exit 1
fi
echo "✅ Migraciones BASE completadas"

# Paso 3: Ejecutar migraciones TRANSACCIONAL
echo ""
echo "🔄 Paso 3: Ejecutando migraciones TRANSACCIONAL (datos)..."
php migrations/migrate-db-command.php --migrate --migration-type=transaccional --oficial
if [ $? -ne 0 ]; then
    echo "❌ Migraciones TRANSACCIONAL fallaron. Abortando deployment."
    exit 1
fi
echo "✅ Migraciones TRANSACCIONAL completadas"

# Paso 4: Ejecutar migraciones CONFIG
echo ""
echo "⚙️  Paso 4: Ejecutando migraciones CONFIG (configuración)..."
php migrations/migrate-db-command.php --migrate --migration-type=config --oficial
if [ $? -ne 0 ]; then
    echo "❌ Migraciones CONFIG fallaron. Abortando deployment."
    exit 1
fi
echo "✅ Migraciones CONFIG completadas"

echo ""
echo "🎉 Deployment de migraciones completado exitosamente"

Hacer ejecutable:

bash
chmod +x deploy-migrations.sh

Clasificador Automático

MigrationTypeClassifier

Herramienta CLI para clasificar automáticamente migraciones legacy.

Heurísticas de Clasificación

El clasificador asigna scores basándose en patterns del código fuente:

| Pattern | Tipo Asignado | Score | Explicación | | ----------------------------------------------------------------------------- | ------------- | ------------- | ---------------------------------- | --- | ----------------------------- | | Path contiene /seeds/ | CONFIG | +10 | Directorio de seeds | | Nombre contiene "seed", "config", "inicial", "setup" | CONFIG | +10 | Nomenclatura sugiere configuración | | Contiene CREATE/ALTER/DROP TABLE/INDEX/CONSTRAINT | BASE | +3 por match | Operaciones DDL | | Contiene INSERT/UPDATE/DELETE masivos | TRANSACCIONAL | +2 por match | Operaciones DML | | Usa métodos Phinx: addIndex(), addForeignKey(), addColumn(), create() | BASE | +3 por método | API de estructura Phinx | | Docblock @migration-type base | transaccional | config | Explícito | +20 | Tipo declarado explícitamente |

Determinación de tipo ganador:

  1. Sumar scores por tipo
  2. Tipo con mayor score gana
  3. Si empate o score = 0, default a BASE
  4. Calcular confianza: (score_ganador / score_total) * 100

Niveles de confianza:

  • High (≥80%): Clasificación muy segura, revisar rápidamente
  • Medium (50-79%): Clasificación probable, revisar antes de aplicar
  • Low (<50%): Clasificación incierta, revisar manualmente

Comando: --scan

Propósito: Escanear migraciones y sugerir tipos con confianza.

bash
php MigrationTypeClassifier.php --scan

Salida de ejemplo:

📊 Clasificación de Migraciones
============================================================

🏗️ ✅ CreateProductoTable → base
     • Contiene 5 operaciones DDL (CREATE/ALTER/DROP)
     • Usa 3 métodos de Phinx para estructura (addIndex/addForeignKey/addColumn/create)

🔄 ⚠️  MigrateClienteLegacy → transaccional
     • Contiene 8 operaciones DML (INSERT/UPDATE/DELETE)
     • Usa execute() genérico - requiere revisión manual

⚙️ ✅ SeedProvincias → config
     • Archivo en directorio seeds/ o nombre sugiere configuración
     • Tipo explícito en docblock: config

🏗️ ❓ AddColumnProductoPrecio → base
     • Sin indicadores claros - asignado BASE por defecto

============================================================
📈 Estadísticas:

Por tipo:
  🏗️  BASE: 87
  🔄 TRANSACCIONAL: 4
  ⚙️  CONFIG: 2

Por confianza:
  ✅ Alta: 75
  ⚠️  Media: 12
  ❓ Baja: 6

Total: 93 migraciones

Comando: --scan --output=FILE

Propósito: Generar archivo JSON con clasificaciones para revisión.

bash
php MigrationTypeClassifier.php --scan --output=classification.json

Formato del JSON generado:

json
{
  "generated_at": "2026-02-11 10:30:45",
  "total_files": 93,
  "migrations": [
    {
      "file": "/path/to/migrations/tenancy/20240101000000_create_producto_table.php",
      "class_name": "CreateProductoTable",
      "suggested_type": "base",
      "confidence": "high",
      "reasons": [
        "Contiene 5 operaciones DDL (CREATE/ALTER/DROP)",
        "Usa 3 métodos de Phinx para estructura (addIndex/addForeignKey/addColumn/create)",
        "Crea tabla usando Phinx table()->create()"
      ]
    },
    {
      "file": "/path/to/migrations/tenancy/20240201000000_migrate_cliente_legacy.php",
      "class_name": "MigrateClienteLegacy",
      "suggested_type": "transaccional",
      "confidence": "medium",
      "reasons": [
        "Contiene 8 operaciones DML (INSERT/UPDATE/DELETE)",
        "Usa execute() genérico - requiere revisión manual"
      ]
    }
  ]
}

Revisar clasificaciones:

bash
# Ver solo clasificaciones con confianza baja
jq '.migrations[] | select(.confidence == "low")' classification.json

# Ver solo migraciones transaccionales
jq '.migrations[] | select(.suggested_type == "transaccional")' classification.json

Comando: --apply=FILE --dry-run

Propósito: Preview de cambios antes de aplicar clasificación.

bash
php MigrationTypeClassifier.php --apply=classification.json --dry-run

Salida de ejemplo:

🔧 Aplicando clasificación (DRY RUN)...

Would update CreateProductoTable (base)
Would update MigrateClienteLegacy (transaccional)
Would update SeedProvincias (config)
Would update AddColumnProductoPrecio (base)

✅ Aplicadas: 93
⏭️  Omitidas: 0

Qué hace (sin --dry-run):

  1. Leer archivo JSON de clasificación
  2. Para cada migración:
    • Verificar archivo existe
    • Verificar no tiene ya getMigrationType()
    • Agregar use App\MigrationType; después del namespace
    • Inyectar método getMigrationType() después de getDefaultLevels()
    • Guardar archivo modificado

Código inyectado (ejemplo):

php
protected function getMigrationType(): MigrationType
{
    return MigrationType::BASE;
}

Comando: --apply=FILE

Propósito: Aplicar clasificación automática a los archivos.

bash
php MigrationTypeClassifier.php --apply=classification.json

Salida de ejemplo:

🔧 Aplicando clasificación...

✅ Aplicadas: 93
⏭️  Omitidas: 0

Post-aplicación:

  1. Ejecutar --validate-types para verificar
  2. Revisar cambios con git diff
  3. Corregir manualmente clasificaciones de confianza baja
  4. Commit cambios

Troubleshooting

Error: "No se pudo cargar la clase"

Síntoma:

Fatal error: Class 'App\MigrationType' not found

Causa: El autoloader de Composer no reconoce la clase MigrationType.

Solución:

bash
cd migrations
composer dump-autoload

Verificar carga:

php
// Test en CLI
php -r "require 'vendor/autoload.php'; use App\MigrationType; var_dump(MigrationType::BASE);"

Error: "Tipo no definido" en getMigrationType()

Síntoma:

PHP Fatal error: Class contains abstract method getMigrationType() and must therefore be declared abstract

Causa: Una clase extiende ConfigurableMigration pero no implementa getMigrationType().

Solución:

  1. Agregar método en la clase:
php
protected function getMigrationType(): MigrationType
{
    return MigrationType::BASE; // O TRANSACCIONAL, CONFIG
}
  1. Si no sabes qué tipo asignar:
    • Ejecutar clasificador: php MigrationTypeClassifier.php --scan
    • Revisar sugerencia del archivo
    • Aplicar manualmente basándose en Decision Tree

Error: Migraciones Duplicadas en --validate-types

Síntoma:

Migraciones duplicadas encontradas:
  - migrations/tenancy/20240101000000_create_producto.php
  - migrations/tenancy_p/20240101000000_create_producto.php

Causa: Migraciones para modo "oficial" y "prueba" están duplicadas (diseño intencional del sistema de multi-modo).

Solución: Esto NO es un error. El sistema multi-modo usa:

  • migrations/tenancy/ → Migraciones para BD oficial
  • migrations/tenancy_p/ → Wrappers que ejecutan misma lógica en BD de prueba

Configuración correcta:

Archivo en tenancy/:

php
// migrations/tenancy/20240101000000_create_producto.php
final class CreateProducto extends ConfigurableMigration
{
    protected function getMigrationType(): MigrationType
    {
        return MigrationType::BASE;
    }

    // ... lógica completa
}

Wrapper en tenancy_p/:

php
// migrations/tenancy_p/20240101000000_create_producto.php
<?php
require_once __DIR__ . '/../tenancy/20240101000000_create_producto.php';

Validación ignora wrappers: El validador detecta archivos que solo tienen require_once y los omite automáticamente.


Error: Clasificador Sugiere Tipo Incorrecto

Síntoma:

🏗️ ❓ MigrateClienteData → base (LOW confidence)
     • Sin indicadores claros - asignado BASE por defecto

Causa: El clasificador no detectó patterns claros (DML oculto en stored procedures, lógica compleja).

Solución:

  1. Revisar manualmente el archivo:
bash
cat migrations/tenancy/20240101000000_migrate_cliente_data.php
  1. Corregir tipo basándose en contenido real:
php
protected function getMigrationType(): MigrationType
{
    return MigrationType::TRANSACCIONAL; // Correcto si migra datos
}
  1. Agregar docblock para futuras clasificaciones:
php
/**
 * Migra datos de cliente desde tabla legacy a nueva estructura.
 *
 * @migration-type transaccional
 */
final class MigrateClienteData extends ConfigurableMigration
  1. Regenerar patch con tipo corregido:
bash
php MigrationTypeClassifier.php --scan --output=classification-v2.json

Error: --migration-type No Ejecuta Ninguna Migración

Síntoma:

$ php migrate-db-command.php --migrate --migration-type=base

Ejecutando migraciones BASE...
0 migraciones ejecutadas

Causa posible 1: Las migraciones BASE ya fueron ejecutadas (están en phinxlog).

Verificar:

bash
php migrate-db-command.php --status

# Buscar migraciones con status "up"

Causa posible 2: Filtro de tipo está funcionando pero no hay migraciones pendientes de ese tipo.

Verificar estadísticas:

bash
php migrate-db-command.php --status | grep -c "down"

Solución: Ejecutar --status para ver estado real de migraciones por tipo.


Error: Pre-commit Hook No Se Ejecuta

Síntoma: El hook de validación no se ejecuta al hacer git commit.

Causa 1: Hook no tiene permisos de ejecución.

Solución:

bash
chmod +x .git/hooks/pre-commit

Causa 2: Hook está en directorio incorrecto.

Verificar:

bash
ls -la .git/hooks/pre-commit
# Debe mostrar: -rwxr-xr-x ... .git/hooks/pre-commit

Causa 3: Git config core.hooksPath apunta a otro directorio.

Verificar:

bash
git config core.hooksPath

# Si devuelve algo diferente a vacío o .git/hooks, resetear:
git config --unset core.hooksPath

Warning: "Clasificador No Detecta Tipo en Migración Legacy"

Síntoma:

⚠️  Warning: Migración antigua sin getMigrationType() detectada

Causa: Migraciones legacy que extienden CustomMigration directamente (sin ConfigurableMigration).

Solución a largo plazo: Refactorizar migraciones legacy:

  1. Identificar migraciones legacy:
bash
grep -r "extends CustomMigration" migrations/migrations/
  1. Refactorizar a ConfigurableMigration:
php
// ANTES
final class OldMigration extends CustomMigration
{
    // ...
}

// DESPUÉS
final class OldMigration extends ConfigurableMigration
{
    protected function getTableName(): string { return 'tabla'; }
    protected function getDefaultLevels(): array { return [self::LEVEL_EMPRESA]; }
    protected function getMigrationType(): MigrationType { return MigrationType::BASE; }
    // ...
}
  1. Probar con dry-run:
bash
php migrate-db-command.php --migrate --dry-run

Solución temporal: Si hay muchas migraciones legacy:

  1. Crear issue para refactorización gradual
  2. Priorizar migraciones más usadas
  3. Nuevas migraciones SIEMPRE usan ConfigurableMigration

Recursos Adicionales

Archivos Principales

ArchivoDescripciónLíneas
MigrationType.phpEnum con 3 tipos (BASE, TRANSACCIONAL, CONFIG)66
MigrationTypeAttribute.phpAtributo PHP 8 para metadata38
MigrationTypeDetector.phpAPI pública para detección160
MigrationTypeValidator.phpValidador recursivo187
MigrationTypeClassifier.phpClasificador automático con heurísticas414
ConfigurableMigration.phpClase base con método abstracto120
Logger.phpLogger con soporte de tipos350+
migrate-db-command.phpCLI con flags --validate-types y --migration-type800+

Ejemplos Reales

Ver migraciones del proyecto para ejemplos completos:

BASE (estructura):

  • migrations/tenancy/20231201000000_create_producto_table.php
  • migrations/tenancy/20231215000000_add_index_producto_codigo.php
  • migrations/tenancy/20240101000000_add_fk_factura_cliente.php

TRANSACCIONAL (datos):

  • migrations/tenancy/20240201000000_migrate_cliente_legacy.php
  • migrations/tenancy/20240301000000_update_producto_precio_iva.php

CONFIG (configuración):

  • migrations/seeds/argentina_provincias.php
  • migrations/seeds/permisos_default.php

Documentación Relacionada

  • Multi-Tenancy: docs/architecture/database/multi-tenant.md
  • Migraciones: bautista-backend/migrations/CLAUDE.md
  • ConfigurableMigration: migrations/migrations/ConfigurableMigration.php (docblocks)

Comandos Útiles

bash
# Ver todas las migraciones con tipo
grep -r "getMigrationType()" migrations/migrations/ | wc -l

# Listar migraciones por tipo
grep -r "MigrationType::BASE" migrations/migrations/
grep -r "MigrationType::TRANSACCIONAL" migrations/migrations/
grep -r "MigrationType::CONFIG" migrations/migrations/

# Validar todas
php migrations/migrate-db-command.php --validate-types

# Clasificar automáticamente
php migrations/MigrationTypeClassifier.php --scan

# Deployment ordenado
bash deployment/deploy-migrations.sh

Última actualización: 2026-02-11 Versión del sistema: 3.14.0 Autor: Sistema de Tipos de Migraciones - Bautista Backend