edit 8. CRUD Avançado - UPDATE

A operação UPDATE apresenta desafios únicos que não existem no CREATE. Você precisa verificar permissões (usuário pode editar este recurso?), implementar partial updates (atualizar apenas campos enviados) e prevenir race conditions (dois usuários editando simultaneamente).

Em aplicações profissionais, updates devem suportar versionamento otimista (detectar conflitos com updated_at), manter audit logs (histórico de alterações) e validar transições de estado (ex: não permitir mudar de "published" para "draft" se há comentários).

Nesta seção, você aprenderá a construir um endpoint UPDATE robusto com validação de propriedade, partial updates dinâmicos, versionamento otimista e webhooks para notificar sistemas externos sobre mudanças.

warning
⚠️ Segurança Crítica: NUNCA permita atualizar qualquer campo enviado pelo usuário. Atacantes podem enviar {"role": "admin"} e elevar privilégios. Sempre use whitelist de campos permitidos.

compare Full Update vs Partial Update

update PUT - Full Update

PUT /api/posts/123
{
  "title": "Novo título",
  "content": "Novo conteúdo",
  "category_id": 5,
  "status": "published"
}

Substitui todos os campos. Campos não enviados são zerados/removidos. Raramente usado em APIs modernas.

edit_note PATCH - Partial Update

PATCH /api/posts/123
{
  "title": "Novo título"
}

Atualiza apenas campos enviados. Outros permanecem intactos. Recomendado para APIs RESTful.

recommend
✅ Melhor Prática: Use PATCH para updates. Permite que o frontend envie apenas campos modificados, reduzindo tráfego e simplificando validação.

code Endpoint UPDATE com Validação de Propriedade

prepare('SELECT * FROM posts WHERE id = ?');
    $stmt->execute([$postId]);
    $post = $stmt->fetch();
    
    if (!$post) {
        http_response_code(404);
        echo json_encode(['success' => false, 'message' => 'Post não encontrado']);
        exit;
    }
    
    // 3. VERIFICAR PERMISSÃO (dono ou admin)
    $stmt = $pdo->prepare('SELECT role FROM users WHERE id = ?');
    $stmt->execute([$userId]);
    $userRole = $stmt->fetchColumn();
    
    if ($post['user_id'] != $userId && $userRole !== 'admin') {
        http_response_code(403);
        echo json_encode(['success' => false, 'message' => 'Sem permissão para editar este post']);
        exit;
    }
    
    // 4. RECEBER DADOS
    $data = json_decode(file_get_contents('php://input'), true);
    
    // 5. WHITELIST DE CAMPOS PERMITIDOS
    $allowedFields = ['title', 'content', 'category_id', 'status', 'excerpt'];
    $updateFields = [];
    $params = [];
    
    foreach ($allowedFields as $field) {
        if (isset($data[$field])) {
            // Validar cada campo
            switch ($field) {
                case 'title':
                    $value = trim($data[$field]);
                    if (strlen($value) < 5 || strlen($value) > 200) {
                        throw new InvalidArgumentException('Título deve ter entre 5 e 200 caracteres');
                    }
                    $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
                    break;
                    
                case 'content':
                    $value = trim($data[$field]);
                    if (strlen($value) < 50) {
                        throw new InvalidArgumentException('Conteúdo deve ter no mínimo 50 caracteres');
                    }
                    $value = strip_tags($value, '


verified
✅ Recursos Implementados:
  • Verificação de propriedade (ownership)
  • Whitelist de campos (previne mass assignment)
  • Validação individual por tipo de campo
  • Versionamento otimista (conflict detection)
  • Audit log (histórico de mudanças)

history Versionamento Otimista - Prevenir Conflitos

schedule Cenário de Conflito

┌──────────────────────────────────────────────────────────────┐
│ 10:00 - Usuário A carrega post (updated_at: 2024-01-15 09:30)│
│ 10:01 - Usuário B carrega mesmo post                         │
│ 10:02 - Usuário A salva alterações                           │
│         ✅ Sucesso (updated_at: 2024-01-15 10:02)            │
│ 10:03 - Usuário B tenta salvar suas alterações              │
│         ❌ CONFLITO! updated_at enviado (09:30) != atual (10:02)│
│         → Retornar 409 Conflict com versão atual             │
└──────────────────────────────────────────────────────────────┘
// Frontend React - Detectar e resolver conflito
const handleUpdate = async (formData) => {
  try {
    await axios.patch(`/api/posts/${postId}`, {
      ...formData,
      updated_at: currentPost.updated_at // Enviar versão atual
    });
    
    toast.success('Post atualizado!');
  } catch (error) {
    if (error.response?.status === 409) {
      // Conflito detectado
      const confirmed = window.confirm(
        'Este post foi modificado por outro usuário. ' +
        'Deseja recarregar e perder suas alterações, ou sobrescrever?'
      );
      
      if (confirmed) {
        // Forçar update sem verificar versão
        await axios.patch(`/api/posts/${postId}`, formData);
      } else {
        // Recarregar versão atual
        fetchPost();
      }
    }
  }
};
info
📝 Alternativa: Para colaboração em tempo real (estilo Google Docs), use Operational Transformation ou CRDTs com WebSockets. Versionamento otimista é ideal para edições ocasionais.

batch_prediction Bulk Update - Atualizar Múltiplos Registros

 100) {
        throw new InvalidArgumentException('Máximo 100 registros por vez');
    }
    
    // Verificar permissão para TODOS os posts
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $stmt = $pdo->prepare("
        SELECT id FROM posts 
        WHERE id IN ($placeholders) AND (user_id = ? OR ? IN (SELECT id FROM users WHERE role = 'admin'))
    ");
    $stmt->execute([...$ids, $userId, $userId]);
    $allowedIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
    
    if (count($allowedIds) !== count($ids)) {
        http_response_code(403);
        echo json_encode(['success' => false, 'message' => 'Sem permissão para alguns posts']);
        exit;
    }
    
    // Construir UPDATE (apenas status, por segurança)
    if (!isset($updates['status']) || !in_array($updates['status'], ['draft', 'published', 'archived'])) {
        throw new InvalidArgumentException('Status inválido');
    }
    
    // Executar bulk update
    $stmt = $pdo->prepare("
        UPDATE posts 
        SET status = ?, updated_at = NOW() 
        WHERE id IN ($placeholders)
    ");
    $stmt->execute([$updates['status'], ...$ids]);
    
    $affected = $stmt->rowCount();
    
    echo json_encode([
        'success' => true,
        'message' => "$affected posts atualizados",
        'affected_rows' => $affected
    ]);
    
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
warning
⚠️ Limite de Escala: Bulk updates devem ter limite máximo (ex: 100 registros). Para operações maiores, use jobs assíncronos com fila (Laravel Queue, BullMQ, etc).

webhook Webhooks - Notificar Mudanças

prepare('SELECT url, secret FROM webhooks WHERE event = ? AND active = 1');
    $stmt->execute([$event]);
    $webhooks = $stmt->fetchAll();
    
    foreach ($webhooks as $webhook) {
        // Enviar requisição assíncrona (não bloquear resposta)
        $payload = json_encode([
            'event' => $event,
            'timestamp' => time(),
            'data' => $data
        ]);
        
        // Gerar assinatura HMAC
        $signature = hash_hmac('sha256', $payload, $webhook['secret']);
        
        // cURL assíncrono
        $ch = curl_init($webhook['url']);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                "X-Webhook-Signature: $signature"
            ],
            CURLOPT_TIMEOUT_MS => 1000, // 1 segundo
            CURLOPT_RETURNTRANSFER => true
        ]);
        
        // Executar em background (não esperar resposta)
        curl_exec($ch);
        curl_close($ch);
    }
}

// USO após update:
triggerWebhook('post.updated', [
    'post_id' => $postId,
    'updated_fields' => array_keys($data),
    'updated_by' => $userId
]);
?>
arrow_forward
🎯 Próxima Seção: Vamos ao CRUD Avançado - DELETE, aprendendo soft delete, hard delete, cascata e recuperação de registros deletados.