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.
{"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.
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, '
- 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();
}
}
}
};
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()]);
}
?>
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
]);
?>