add_circle 6. CRUD Avançado - CREATE

A operação CREATE é onde sua aplicação recebe dados externos pela primeira vez. Este é o ponto mais crítico para segurança: você está permitindo que usuários (potencialmente maliciosos) insiram informações no seu banco de dados. Um formulário mal validado pode resultar em SQL Injection, XSS ou corrupção de dados.

Um endpoint CREATE profissional implementa três camadas de defesa: validação (garantir formato correto), sanitização (remover caracteres perigosos) e prepared statements (prevenir SQL Injection). Além disso, deve retornar erros informativos para o frontend sem expor detalhes técnicos que possam ajudar atacantes.

Nesta seção, você aprenderá a construir um endpoint CREATE completo para um sistema de posts/artigos, com validação de múltiplos campos, upload de imagens, slugs únicos e tratamento de erros adequado para ambientes de produção.

security
🔐 Regra de Ouro: "Never trust user input" (Nunca confie em dados do usuário). Mesmo que o frontend valide, SEMPRE valide no backend. Atacantes podem bypassar validações JavaScript facilmente.

shield As Três Camadas de Defesa

check_circle

1. Validação

Verificar formato, tipo e regras de negócio.
Ex: Email válido, número entre 1-100, campo obrigatório

cleaning_services

2. Sanitização

Remover/escapar caracteres perigosos.
Ex: Tags HTML, scripts maliciosos, caracteres especiais SQL

verified

3. Prepared Statements

Separar lógica SQL dos dados.
Ex: PDO::prepare() com placeholders (?)

code Endpoint CREATE Completo: Criar Post

 200) {
        throw new InvalidArgumentException('Título deve ter entre 5 e 200 caracteres');
    }
    
    $content = trim($data['content']);
    if (strlen($content) < 50) {
        throw new InvalidArgumentException('Conteúdo deve ter no mínimo 50 caracteres');
    }
    
    $categoryId = filter_var($data['category_id'], FILTER_VALIDATE_INT);
    if (!$categoryId) {
        throw new InvalidArgumentException('ID de categoria inválido');
    }
    
    // 4. SANITIZAÇÃO
    $title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
    // content pode ter HTML permitido - use biblioteca como HTML Purifier
    $content = strip_tags($content, '


check_circle
✅ Boas Práticas Aplicadas:
  • HTTP Status Code 201 (Created) em vez de 200
  • Validação completa antes de tocar no banco
  • Sanitização apropriada para cada tipo de dado
  • Slug único automático
  • Foreign key validation (categoria existe)

rule Classe Validator Reutilizável

errors[] = "$fieldName é obrigatório";
        }
        return $this;
    }
    
    /**
     * Valida comprimento mínimo
     */
    public function minLength($value, $min, $fieldName) {
        if (strlen($value) < $min) {
            $this->errors[] = "$fieldName deve ter no mínimo $min caracteres";
        }
        return $this;
    }
    
    /**
     * Valida comprimento máximo
     */
    public function maxLength($value, $max, $fieldName) {
        if (strlen($value) > $max) {
            $this->errors[] = "$fieldName deve ter no máximo $max caracteres";
        }
        return $this;
    }
    
    /**
     * Valida email
     */
    public function email($value, $fieldName) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[] = "$fieldName não é um email válido";
        }
        return $this;
    }
    
    /**
     * Valida URL
     */
    public function url($value, $fieldName) {
        if (!filter_var($value, FILTER_VALIDATE_URL)) {
            $this->errors[] = "$fieldName não é uma URL válida";
        }
        return $this;
    }
    
    /**
     * Valida número inteiro
     */
    public function integer($value, $fieldName) {
        if (!filter_var($value, FILTER_VALIDATE_INT)) {
            $this->errors[] = "$fieldName deve ser um número inteiro";
        }
        return $this;
    }
    
    /**
     * Valida intervalo numérico
     */
    public function between($value, $min, $max, $fieldName) {
        if ($value < $min || $value > $max) {
            $this->errors[] = "$fieldName deve estar entre $min e $max";
        }
        return $this;
    }
    
    /**
     * Valida regex customizado
     */
    public function pattern($value, $pattern, $fieldName, $message = null) {
        if (!preg_match($pattern, $value)) {
            $this->errors[] = $message ?: "$fieldName tem formato inválido";
        }
        return $this;
    }
    
    /**
     * Verifica se passou em todas as validações
     */
    public function isValid() {
        return empty($this->errors);
    }
    
    /**
     * Retorna array de erros
     */
    public function getErrors() {
        return $this->errors;
    }
}

// USO:
$validator = new Validator();
$validator
    ->required($data['title'], 'Título')
    ->minLength($data['title'], 5, 'Título')
    ->maxLength($data['title'], 200, 'Título');

$validator
    ->required($data['email'], 'Email')
    ->email($data['email'], 'Email');

if (!$validator->isValid()) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'errors' => $validator->getErrors()
    ]);
    exit;
}
?>

image Upload de Imagens com Validação

 $maxSize) {
        throw new InvalidArgumentException('Imagem muito grande. Máximo: 5MB');
    }
    
    // 3. VALIDAR TIPO MIME (não confiar na extensão!)
    $allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    
    if (!in_array($mimeType, $allowedTypes)) {
        throw new InvalidArgumentException('Tipo de arquivo não permitido');
    }
    
    // 4. VALIDAR DIMENSÕES
    $imageInfo = getimagesize($file['tmp_name']);
    if ($imageInfo[0] > 4000 || $imageInfo[1] > 4000) {
        throw new InvalidArgumentException('Dimensões muito grandes. Máximo: 4000x4000');
    }
    
    // 5. GERAR NOME ÚNICO
    $extension = match($mimeType) {
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/webp' => 'webp'
    };
    
    $filename = uniqid('img_' . $userId . '_', true) . '.' . $extension;
    $uploadDir = __DIR__ . '/../../uploads/images/';
    $uploadPath = $uploadDir . $filename;
    
    // 6. CRIAR DIRETÓRIO SE NÃO EXISTE
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
    }
    
    // 7. MOVER ARQUIVO
    if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
        throw new RuntimeException('Erro ao salvar arquivo');
    }
    
    // 8. REDIMENSIONAR (opcional, use biblioteca como Intervention Image)
    // resizeImage($uploadPath, 800, 800);
    
    // 9. SALVAR REFERÊNCIA NO BANCO
    $stmt = $pdo->prepare('
        INSERT INTO images (user_id, filename, mime_type, size, created_at) 
        VALUES (?, ?, ?, ?, NOW())
    ');
    $stmt->execute([$userId, $filename, $mimeType, $file['size']]);
    
    http_response_code(201);
    echo json_encode([
        'success' => true,
        'data' => [
            'id' => $pdo->lastInsertId(),
            'filename' => $filename,
            'url' => "/uploads/images/$filename"
        ]
    ]);
    
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
warning
⚠️ Segurança Crítica: NUNCA confie na extensão do arquivo ($_FILES['image']['name']). Atacantes podem renomear shell.php para image.jpg. Sempre valide o MIME type real com finfo_file().

integration_instructions Integração React (Frontend)

// frontend/src/services/postService.js
import axios from 'axios';

const API_URL = import.meta.env.VITE_API_URL;

export const postService = {
  async createPost(postData) {
    try {
      const { data } = await axios.post(`${API_URL}/posts/create`, postData, {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
      });
      return data;
    } catch (error) {
      throw error.response?.data || error;
    }
  },
  
  async uploadImage(file) {
    const formData = new FormData();
    formData.append('image', file);
    
    try {
      const { data } = await axios.post(`${API_URL}/uploads/image`, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
      });
      return data;
    } catch (error) {
      throw error.response?.data || error;
    }
  }
};

// USO NO COMPONENTE:
const handleSubmit = async (e) => {
  e.preventDefault();
  
  try {
    const result = await postService.createPost({
      title: formData.title,
      content: formData.content,
      category_id: formData.categoryId,
      status: 'published'
    });
    
    alert(`Post criado! ID: ${result.data.id}`);
    navigate(`/posts/${result.data.slug}`);
  } catch (error) {
    setError(error.message || 'Erro ao criar post');
  }
};
arrow_forward
🎯 Próxima Seção: Com o CREATE dominado, vamos ao CRUD Avançado - READ, aprendendo queries complexas, paginação, filtros e ordenação para listagens eficientes.