INSERT en PHP

Insertar datos en MySQL desde PHP es una operación fundamental que realizarás en prácticamente cualquier aplicación web. PDO facilita esta tarea mediante sentencias preparadas que protegen contra inyecciones SQL de forma transparente. En este artículo aprenderás a insertar registros individuales, obtener el ID generado, realizar inserciones masivas eficientes y usar transacciones para operaciones complejas.

Requisitos previos

Necesitas una conexión PDO configurada y la tabla productos. Si no la tienes, consulta el artículo de conexión para la configuración.

USE tienda;
 
CREATE TABLE IF NOT EXISTS productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    categoria VARCHAR(50) NOT NULL,
    precio DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    activo TINYINT(1) DEFAULT 1,
    fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP
);

Código completo

Este ejemplo inserta un producto y muestra el ID generado:

<?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(
    'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (?, ?, ?, ?)'
);
$stmt->execute(['Tablet Samsung Galaxy Tab S9', 'Tablets', 8999.00, 35]);
 
echo "Producto insertado exitosamente\n";
echo "ID generado: " . $pdo->lastInsertId() . "\n";
echo "Filas afectadas: " . $stmt->rowCount() . "\n";

Salida esperada:

Producto insertado exitosamente
ID generado: 9
Filas afectadas: 1

Explicación paso a paso

El flujo para insertar datos con PDO consiste en preparar la sentencia con prepare(), ejecutarla con execute() pasando los valores, y luego consultar lastInsertId() para obtener el ID AUTO_INCREMENT generado. El método rowCount() del statement devuelve el número de filas afectadas.

PDO ofrece dos formas de pasar parámetros. Con placeholders posicionales (?), los valores se pasan como array en el orden correcto. Con placeholders nombrados (:nombre), los valores se pasan como array asociativo:

Insertar con parámetros nombrados

<?php
$stmt = $pdo->prepare(
    'INSERT INTO productos (nombre, categoria, precio, stock)
     VALUES (:nombre, :categoria, :precio, :stock)'
);
 
$stmt->execute([
    ':nombre' => 'Cargador USB-C 65W',
    ':categoria' => 'Accesorios',
    ':precio' => 599.00,
    ':stock' => 300
]);
 
echo "ID del nuevo producto: " . $pdo->lastInsertId() . "\n";

Insertar con bindValue y bindParam

Para mayor control sobre los tipos de datos, puedes usar bindValue() o bindParam():

<?php
$stmt = $pdo->prepare(
    'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (:nombre, :categoria, :precio, :stock)'
);
 
$stmt->bindValue(':nombre', 'Hub USB-C 7 en 1', PDO::PARAM_STR);
$stmt->bindValue(':categoria', 'Accesorios', PDO::PARAM_STR);
$stmt->bindValue(':precio', 899.00);
$stmt->bindValue(':stock', 150, PDO::PARAM_INT);
 
$stmt->execute();
echo "Producto insertado con ID: " . $pdo->lastInsertId() . "\n";

La diferencia entre bindValue() y bindParam() es que bindValue() asigna el valor en el momento de la llamada, mientras que bindParam() enlaza una referencia a la variable, por lo que el valor se lee en el momento de execute().

Inserción masiva con transacciones

Cuando necesitas insertar muchos registros, envolverlos en una transacción mejora drásticamente el rendimiento porque MySQL hace un solo flush a disco en lugar de uno por cada INSERT:

<?php
$productos = [
    ['Impresora HP LaserJet', 'Impresoras', 3499.00, 20],
    ['Cable HDMI 2.1 3m', 'Accesorios', 349.00, 500],
    ['Mousepad XXL Gaming', 'Accesorios', 449.00, 200],
    ['Soporte Monitor Ajustable', 'Accesorios', 1299.00, 75],
    ['Adaptador DisplayPort a HDMI', 'Accesorios', 249.00, 400]
];
 
$stmt = $pdo->prepare(
    'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (?, ?, ?, ?)'
);
 
$pdo->beginTransaction();
 
try {
    $insertados = 0;
    foreach ($productos as $producto) {
        $stmt->execute($producto);
        $insertados++;
    }
 
    $pdo->commit();
    echo "Registros insertados: {$insertados}\n";
 
} catch (PDOException $e) {
    $pdo->rollBack();
    echo "Error: {$e->getMessage()}. Cambios revertidos.\n";
}

Salida esperada:

Registros insertados: 5

Caso práctico

Veamos cómo implementar un formulario de registro de productos con validación:

<?php
function crearProducto(PDO $pdo, array $datos): array {
    // Validar campos requeridos
    $requeridos = ['nombre', 'categoria', 'precio'];
    foreach ($requeridos as $campo) {
        if (empty($datos[$campo])) {
            return ['exito' => false, 'mensaje' => "El campo '{$campo}' es obligatorio"];
        }
    }
 
    // Validar precio
    if (!is_numeric($datos['precio']) || $datos['precio'] <= 0) {
        return ['exito' => false, 'mensaje' => 'El precio debe ser un número positivo'];
    }
 
    try {
        $stmt = $pdo->prepare(
            'INSERT INTO productos (nombre, categoria, precio, stock)
             VALUES (:nombre, :categoria, :precio, :stock)'
        );
 
        $stmt->execute([
            ':nombre' => trim($datos['nombre']),
            ':categoria' => trim($datos['categoria']),
            ':precio' => (float) $datos['precio'],
            ':stock' => (int) ($datos['stock'] ?? 0)
        ]);
 
        $id = (int) $pdo->lastInsertId();
 
        return [
            'exito' => true,
            'id' => $id,
            'mensaje' => "Producto creado exitosamente con ID {$id}"
        ];
 
    } catch (PDOException $e) {
        if ($e->errorInfo[1] === 1062) {
            return ['exito' => false, 'mensaje' => 'Ya existe un producto con ese nombre'];
        }
        return ['exito' => false, 'mensaje' => 'Error al crear el producto'];
    }
}
 
// Simulando datos de un formulario
$resultado = crearProducto($pdo, [
    'nombre' => 'Monitor LG UltraWide 34"',
    'categoria' => 'Monitores',
    'precio' => 8499.00,
    'stock' => 15
]);
 
echo $resultado['mensaje'] . "\n";

Salida esperada:

Producto creado exitosamente con ID 14

Para inserciones más complejas que involucran múltiples tablas, usa transacciones:

<?php
function registrarPedido(PDO $pdo, int $clienteId, array $items): array {
    $pdo->beginTransaction();
 
    try {
        // Insertar pedido
        $stmt = $pdo->prepare(
            'INSERT INTO pedidos (cliente_id, fecha, estado) VALUES (?, NOW(), ?)'
        );
        $stmt->execute([$clienteId, 'pendiente']);
        $pedidoId = (int) $pdo->lastInsertId();
 
        // Insertar líneas del pedido
        $stmtDetalle = $pdo->prepare(
            'INSERT INTO detalle_pedido (pedido_id, producto_id, cantidad, precio_unitario) VALUES (?, ?, ?, ?)'
        );
        $stmtStock = $pdo->prepare(
            'UPDATE productos SET stock = stock - ? WHERE id = ? AND stock >= ?'
        );
 
        foreach ($items as $item) {
            $stmtDetalle->execute([$pedidoId, $item['producto_id'], $item['cantidad'], $item['precio']]);
 
            $stmtStock->execute([$item['cantidad'], $item['producto_id'], $item['cantidad']]);
            if ($stmtStock->rowCount() === 0) {
                throw new Exception("Stock insuficiente para producto {$item['producto_id']}");
            }
        }
 
        $pdo->commit();
        return ['exito' => true, 'pedido_id' => $pedidoId];
 
    } catch (Exception $e) {
        $pdo->rollBack();
        return ['exito' => false, 'mensaje' => $e->getMessage()];
    }
}

Manejo de errores

Los errores más comunes al insertar datos en PHP:

<?php
function insertarSeguro(PDO $pdo, string $nombre, string $categoria, float $precio, int $stock): array {
    try {
        $stmt = $pdo->prepare(
            'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (?, ?, ?, ?)'
        );
        $stmt->execute([$nombre, $categoria, $precio, $stock]);
 
        return ['exito' => true, 'id' => (int) $pdo->lastInsertId()];
 
    } catch (PDOException $e) {
        $codigo = $e->errorInfo[1];
 
        switch ($codigo) {
            case 1062:
                return ['exito' => false, 'mensaje' => 'Ya existe un producto con ese nombre'];
            case 1048:
                return ['exito' => false, 'mensaje' => 'Faltan campos obligatorios'];
            case 1406:
                return ['exito' => false, 'mensaje' => 'Uno de los valores excede la longitud permitida'];
            case 1452:
                return ['exito' => false, 'mensaje' => 'Referencia a tabla externa no válida'];
            default:
                return ['exito' => false, 'mensaje' => "Error [{$codigo}]: {$e->getMessage()}"];
        }
    }
}

Ahora que sabes cómo insertar datos, en el siguiente artículo aprenderás a actualizar registros existentes en MySQL desde PHP.

Escrito por Eduardo Lázaro