UPDATE en PHP

Actualizar registros existentes en MySQL desde PHP es una operación que ejecutarás frecuentemente en cualquier aplicación web, desde actualizar perfiles de usuario hasta modificar inventarios y precios. PDO facilita esta tarea con sentencias preparadas y control de transacciones. En este artículo aprenderás a ejecutar actualizaciones parametrizadas, verificar si se modificaron datos y construir consultas dinámicas de forma segura.

Requisitos previos

Necesitas una conexión PDO configurada y la tabla productos con datos de prueba. Consulta los artículos anteriores si necesitas configurar el entorno.

Código completo

Este ejemplo actualiza el precio y stock de un producto:

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=tienda;charset=utf8mb4',
    'root', 'tu_contraseña',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
 
$stmt = $pdo->prepare('UPDATE productos SET precio = ?, stock = ? WHERE id = ?');
$stmt->execute([13499.99, 30, 1]);
 
echo "Filas afectadas: " . $stmt->rowCount() . "\n";

Salida esperada:

Filas afectadas: 1

Explicación paso a paso

El método rowCount() en un UPDATE devuelve el número de filas que realmente cambiaron. Si actualizas un campo con el mismo valor que ya tenía, rowCount() devolverá 0 porque ninguna fila cambió realmente. Esto es diferente de la cantidad de filas que coinciden con el WHERE.

Si necesitas saber cuántas filas coincidieron con la condición (independientemente de si cambiaron), puedes configurar la opción PDO::MYSQL_ATTR_FOUND_ROWS:

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=tienda;charset=utf8mb4',
    'root', 'tu_contraseña',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::MYSQL_ATTR_FOUND_ROWS => true  // rowCount devuelve filas encontradas
    ]
);

Verificar si la actualización tuvo efecto

<?php
function actualizarPrecio(PDO $pdo, int $productoId, float $nuevoPrecio): bool {
    $stmt = $pdo->prepare('UPDATE productos SET precio = ? WHERE id = ?');
    $stmt->execute([$nuevoPrecio, $productoId]);
 
    if ($stmt->rowCount() === 0) {
        echo "No se encontró el producto o el precio no cambió\n";
        return false;
    }
 
    echo "Precio del producto {$productoId} actualizado a \${$nuevoPrecio}\n";
    return true;
}
 
actualizarPrecio($pdo, 1, 12499.99);
actualizarPrecio($pdo, 999, 100.00);

Salida esperada:

Precio del producto 1 actualizado a $12499.99
No se encontró el producto o el precio no cambió

Actualización con parámetros nombrados

Los parámetros nombrados hacen el código más legible cuando hay muchos campos:

<?php
$stmt = $pdo->prepare(
    'UPDATE productos SET nombre = :nombre, precio = :precio, stock = :stock WHERE id = :id'
);
 
$stmt->execute([
    ':nombre' => 'Laptop HP Pavilion 15 (2024)',
    ':precio' => 14999.99,
    ':stock' => 20,
    ':id' => 1
]);
 
echo "Producto actualizado. Filas afectadas: " . $stmt->rowCount() . "\n";

Actualización dinámica

Cuando el usuario puede modificar solo algunos campos, necesitas construir la consulta dinámicamente:

<?php
function actualizarProducto(PDO $pdo, int $id, array $campos): array {
    $permitidos = ['nombre', 'categoria', 'precio', 'stock', 'activo'];
    $sets = [];
    $params = [];
 
    foreach ($campos as $campo => $valor) {
        if (in_array($campo, $permitidos, true)) {
            $sets[] = "{$campo} = :{$campo}";
            $params[":{$campo}"] = $valor;
        }
    }
 
    if (empty($sets)) {
        return ['exito' => false, 'mensaje' => 'No se proporcionaron campos válidos'];
    }
 
    $params[':id'] = $id;
    $sql = 'UPDATE productos SET ' . implode(', ', $sets) . ' WHERE id = :id';
 
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
 
    return [
        'exito' => true,
        'modificado' => $stmt->rowCount() > 0
    ];
}
 
// Uso: actualizar solo precio y stock
$resultado = actualizarProducto($pdo, 1, ['precio' => 11999.99, 'stock' => 50]);
var_export($resultado);

Salida esperada:

array ( 'exito' => true, 'modificado' => true, )

Actualización con transacciones

Para operaciones que involucran múltiples actualizaciones relacionadas:

<?php
function transferirStock(PDO $pdo, int $origenId, int $destinoId, int $cantidad): bool {
    $pdo->beginTransaction();
 
    try {
        // Verificar stock disponible
        $stmt = $pdo->prepare('SELECT stock FROM productos WHERE id = ? FOR UPDATE');
        $stmt->execute([$origenId]);
        $origen = $stmt->fetch();
 
        if (!$origen || $origen['stock'] < $cantidad) {
            $pdo->rollBack();
            echo "Stock insuficiente\n";
            return false;
        }
 
        // Descontar del origen
        $stmt = $pdo->prepare('UPDATE productos SET stock = stock - ? WHERE id = ?');
        $stmt->execute([$cantidad, $origenId]);
 
        // Agregar al destino
        $stmt = $pdo->prepare('UPDATE productos SET stock = stock + ? WHERE id = ?');
        $stmt->execute([$cantidad, $destinoId]);
 
        $pdo->commit();
        echo "{$cantidad} unidades transferidas del producto {$origenId} al {$destinoId}\n";
        return true;
 
    } catch (PDOException $e) {
        $pdo->rollBack();
        echo "Error en la transferencia: {$e->getMessage()}\n";
        return false;
    }
}

Caso práctico

Veamos una función completa de actualización de perfil con auditoría:

<?php
function actualizarPerfil(PDO $pdo, int $userId, array $cambios): array {
    $pdo->beginTransaction();
 
    try {
        // Obtener valores actuales
        $stmt = $pdo->prepare('SELECT nombre, email, telefono FROM usuarios WHERE id = ?');
        $stmt->execute([$userId]);
        $actual = $stmt->fetch();
 
        if (!$actual) {
            $pdo->rollBack();
            return ['exito' => false, 'mensaje' => 'Usuario no encontrado'];
        }
 
        // Filtrar solo campos que cambiaron
        $permitidos = ['nombre', 'email', 'telefono', 'direccion'];
        $updates = [];
        $params = [];
 
        foreach ($cambios as $campo => $valor) {
            if (in_array($campo, $permitidos, true) && ($actual[$campo] ?? null) !== $valor) {
                $updates[] = "{$campo} = :{$campo}";
                $params[":{$campo}"] = $valor;
            }
        }
 
        if (empty($updates)) {
            $pdo->rollBack();
            return ['exito' => true, 'mensaje' => 'No hay cambios que aplicar'];
        }
 
        $params[':id'] = $userId;
        $sql = 'UPDATE usuarios SET ' . implode(', ', $updates) . ', fecha_actualizacion = NOW() WHERE id = :id';
 
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
 
        // Registrar auditoría
        $stmt = $pdo->prepare(
            'INSERT INTO audit_log (tabla, registro_id, accion, detalles, fecha) VALUES (?, ?, ?, ?, NOW())'
        );
        $stmt->execute(['usuarios', $userId, 'UPDATE', json_encode(['antes' => $actual, 'despues' => $cambios])]);
 
        $pdo->commit();
        return ['exito' => true, 'campos_modificados' => count($updates)];
 
    } catch (PDOException $e) {
        $pdo->rollBack();
        return ['exito' => false, 'mensaje' => 'Error al actualizar: ' . $e->getMessage()];
    }
}

Manejo de errores

Los errores comunes al actualizar registros en PHP:

<?php
function actualizarSeguro(PDO $pdo, int $id, array $datos): array {
    try {
        $stmt = $pdo->prepare('UPDATE productos SET nombre = ?, precio = ? WHERE id = ?');
        $stmt->execute([$datos['nombre'], $datos['precio'], $id]);
 
        if ($stmt->rowCount() === 0) {
            return ['error' => 'NOT_FOUND', 'mensaje' => 'Producto no encontrado o sin cambios'];
        }
 
        return ['exito' => true];
 
    } catch (PDOException $e) {
        $codigo = $e->errorInfo[1];
 
        switch ($codigo) {
            case 1062:
                return ['error' => 'DUPLICADO', 'mensaje' => 'Ya existe un producto con ese nombre'];
            case 1406:
                return ['error' => 'LONGITUD', 'mensaje' => 'El nombre excede el límite de caracteres'];
            case 1205:
                return ['error' => 'TIMEOUT', 'mensaje' => 'El registro está bloqueado por otro proceso'];
            case 1213:
                return ['error' => 'DEADLOCK', 'mensaje' => 'Interbloqueo detectado. Intenta de nuevo'];
            default:
                return ['error' => 'INTERNO', 'mensaje' => "Error [{$codigo}]: {$e->getMessage()}"];
        }
    }
}

Ahora que conoces las técnicas para actualizar registros en PHP, en el siguiente artículo aprenderás a eliminar datos de MySQL.

Escrito por Eduardo Lázaro