Esse projeto é bem mais antigo do que os que costumo documentar aqui, e é exatamente por isso que ele vale um post: foi o teste técnico que eu fiz em 2021 para uma vaga de desenvolvedor PHP, e mostra bem o nível de decisão que eu tomava naquela época sem framework, só PHP orientado a objetos, PDO e a disciplina de separar responsabilidades em pastas, mesmo sem ter as abstrações que uso hoje.
O enunciado do teste pedia um CRUD. O que construí foi um cadastro de clientes com uma tabela de dívidas vinculada a cada um deles um cliente pode ter várias dívidas, cada uma com descrição, valor e vencimento tudo listado em uma tela com DataTables, com os formulários abrindo em modal do Bootstrap e confirmações via SweetAlert2.
A estrutura de pastas já separava responsabilidades
Não existia framework, mas já existia a preocupação de não misturar tudo em um arquivo só. O composer.json declarava autoload PSR-4 para dois namespaces, e o projeto se organizava em quatro pastas com papéis bem definidos:
Pasta
Papel
Classes/
Entidades e infraestrutura: Cliente, Divida, a conexão DataBase (Singleton com PDO) e o mini-roteador Url
Helpers/
Funções utilitárias sem estado: máscaras, validação de e-mail e CNPJ, formatação de data, sanitização de string
modulos/
As "telas" — fragmentos de view (HTML + PHP) que o index.php inclui de acordo com a URL
operacoes/
Endpoints chamados via AJAX, que recebem o POST do formulário, instanciam a entidade certa e devolvem JSON
Roteamento artesanal, na unha
Não havia Router de framework: o próprio index.php lia os segmentos da URL através da classe Url, que fatiava a REQUEST_URI manualmente, e incluía o arquivo correspondente dentro de modulos/ caindo em uma página 404 própria quando o arquivo não existia. Rústico para os padrões de hoje, mas resolvia o problema sem depender de nada externo.
$modulo = Url::getURL( 0 );
if( $modulo == null )
$modulo = "home";
if( file_exists( "modulos/" . $modulo . ".php" ) )
require "modulos/" . $modulo . ".php";
else
require "modulos/404.php";
As entidades: cada uma com seu próprio save/update/delete
Cliente e Divida usam __get/__set mágicos para aceitar qualquer propriedade dinamicamente, e cada uma implementa seus próprios métodos save(), update(), delete(), show() e fetchAll() com SQL manual, parâmetros amarrados por posição (bindParam(1, ...), bindParam(2, ...)) e soft delete por uma coluna deleted com timestamp em vez de exclusão de verdade:
public function delete($id){
/**Aplicação de técnica de soft delete*/
$db = Database::conexao();
$sql = "UPDATE ".$this->table." SET deleted = NOW() WHERE id=?";
$deleted = $db->prepare($sql);
$deleted->bindParam(1, $id);
$deleted->execute();
return ($deleted->rowCount() > 0);
}
É o mesmo save/update/delete reescrito quase idêntico em Cliente e em Divida só muda o nome da tabela e os campos. Não havia um BaseRepository para absorver essa repetição, porque a ideia de abstrair uma camada de dados genérica ainda não fazia parte do meu vocabulário técnico naquele momento.
A "API" interna: um switch de opções numéricas
Cada arquivo em operacoes/ recebe o POST do formulário e decide o que fazer através de um número inteiro chamado opcao 1 para criar, 2 para atualizar, 3 para buscar um registro, 4 para excluir tudo dentro de um único switch, com a sanitização e as máscaras (CPF/CNPJ, telefone, CEP) aplicadas linha a linha via os Helpers antes de instanciar a entidade:
case 1:
$cliente = new Cliente();
$cliente->nome = $_POST["nome"];
$cliente->cpfcnpj = Common::Replace('/[^0-9]/i', '', $_POST["cpfcnpj"]);
$cliente->email = Common::checkEmail($_POST["email"]);
// ...
if($cliente->save()){
echo json_encode(["data" => 1, "msg" => "Salvo com sucesso !!"]);
}
break;
Funciona, e para 2021 resolvia bem o problema proposto no teste. Mas a validação fica misturada com a montagem da entidade, o contrato da "API" é um número mágico em vez de um verbo HTTP e uma rota, e não existe uma camada de serviço separada do que hoje eu chamaria de controller.
O que essa comparação me mostra hoje
Olhando de agora, dá pra apontar exatamente onde eu chegaria diferente: uma classe base para as entidades absorver o CRUD repetido, validação centralizada em Request/Form Object em vez de dentro do switch, rotas de verdade em vez de um número de opção, e um roteador que não depende de fatiar a REQUEST_URI na mão. É basicamente o caminho que segui depois hoje penso automaticamente em Controller, Service e Repository como camadas separadas, cada uma com uma base abstrata reaproveitável entre entidades, exatamente o padrão que documentei no post sobre a API de candidatos e vagas.
Mas nada disso desmerece o que o projeto já acertava para o momento: separação clara entre entidade, helper e view, prepared statements em vez de concatenar SQL direto (nada de injeção óbvia), soft delete pensado desde o início, e autoload PSR-4 via Composer mesmo num projeto sem framework nenhum. É um bom retrato de onde eu estava, e serve como referência de quanto o raciocínio de arquitetura evoluiu desde então.
Repositório
Código completo em github.com/paulodm145/CRUD-PHP-ORIENTADO-A-OBJETOS.