bmorais / database
The data layer is a persistent abstraction component of your database that PDO
Requires
- php: >=8.0
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
Requires (Dev)
- rector/rector: ^0.19.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v3.0.10
- v3.0.8
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.0.12
- v2.0.11
- v2.0.10
- v2.0.9
- v2.0.8
- v2.0.7
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.0.86
- v1.0.85
- v1.0.84
- v1.0.83
- v1.0.82
- v1.0.81
- v1.0.80
- v1.0.79
- v1.0.78
- v1.0.77
- v1.0.76
- v1.0.75
- v1.0.74
- v1.0.73
- v1.0.72
- v1.0.71
- v1.0.70
- v1.0.69
- v1.0.68
- v1.0.67
- v1.0.66
- v1.0.65
- v1.0.64
- v1.0.63
- v1.0.62
- v1.0.61
- v1.0.60
- v1.0.59
- v1.0.58
- v1.0.57
- v1.0.56
- v1.0.55
- v1.0.53
- v1.0.52
- v1.0.51
- v1.0.50
- v1.0.49
- 1.0.48
- 1.0.47
- 1.0.46
- 1.0.45
- 1.0.44
- 1.0.43
- 1.0.42
- 1.0.41
- 1.0.40
- 1.0.39
- 1.0.38
- 1.0.37
- 1.0.36
- 1.0.35
- 1.0.34
- 1.0.33
- 1.0.32
- 1.0.31
- v1.0.30
- v1.0.29
- 1.0.28
- 1.0.27
- 1.0.26
- 1.0.25
- 1.0.24
- 1.0.23
- 1.0.22
- 1.0.21
- 1.0.20
- v1.0.19
- v1.0.18
- v1.0.17
- v1.0.16
- v1.0.15
- v1.0.14
- v1.0.13
- v1.0.12
- v1.0.11
- v1.0.10
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-fix/builder-e-seguranca
- dev-extractModel
- dev-add-license-1
This package is auto-updated.
Last update: 2026-10-19 21:57:43 UTC
README
Database BMORAIS Code
A PDO-based persistence layer with prepared statements, CRUD, a fluent query builder, transactions and tools for large datasets.
O database é uma camada de persistência sobre PDO, sempre com prepared statements, para as rotinas comuns de cadastrar, ler, editar e remover dados. Inclui um query builder fluente, transações com rollback garantido e recursos para grandes volumes (inserção em lote e leitura linha a linha).
Sumário
- Destaques
- Instalação
- Configuração
- Criando um DAO
- Models
- CRUD
- SQL direto e leitura do resultado
- Query builder
- Transações
- Grandes volumes
- Segurança
- Erros, log e debug
- Conexões
- Exemplo completo
Destaques
- CRUD completo com poucas linhas, sempre com prepared statements
- Query builder fluente (joins, where/andWhere/orWhere, groupBy/having, orderBy, limit/offset)
- Mapeamento automático do resultado para models (
ModelAbstract) transaction()com rollback em qualquer erro e nova tentativa em deadlockinsertMany()(INSERT em lote) ecursor()(leitura linha a linha) para tabelas grandes- Conexão compartilhada entre DAOs, reconexão segura e suporte a ambiente de homologação
- Pronto para o Composer
Instalação
Requer PHP 8.0+ com as extensões pdo, json e mbstring.
composer require bmorais/database
ou no composer.json:
"bmorais/database": "^3.0"
Configuração
Defina a constante global CONFIG_DATA_LAYER antes de usar a biblioteca. Para outros bancos, veja o
manual de drivers do PDO.
const CONFIG_DATA_LAYER = [ "driver" => "mysql", "host" => "localhost", "port" => "3306", "dbname" => "database", "username" => "user", "passwd" => "", "options" => [ PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8MB4, lc_time_names = 'pt_BR'", PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::MYSQL_ATTR_FOUND_ROWS => true, PDO::ATTR_STRINGIFY_FETCHES => true, ], "homologation" => "homologacao", // sufixo do banco de homologação "homologation_active" => false, // opcional (veja abaixo) "directory_models" => "App\\Models\\", // namespace dos models "display_errors_details" => true, // guarda o SQL para getLogSQL() ];
| Chave | Descrição |
|---|---|
options |
Opções do PDO. Use UTF8MB4 (o UTF8 do MySQL não grava emoji). ERRMODE_EXCEPTION é obrigatório para o tratamento de erros. |
homologation |
Em homologação, esse sufixo é adicionado ao nome do banco (app → appHomologacao). |
homologation_active |
Opcional. Define explicitamente se o ambiente é de homologação. Sem ela, vale a variável de ambiente DB_HOMOLOGATION e, por último, a detecção por $_SERVER['SERVER_NAME']. |
directory_models |
Namespace usado para instanciar os models (setClassModel("UsuarioModel") → App\Models\UsuarioModel). |
display_errors_details |
Quando true, guarda a última consulta para getLogSQL(). |
Homologação em Apache: com
UseCanonicalName Off(padrão), oSERVER_NAMEvem do headerHostda requisição. Defina"homologation_active" => falseno config de produção para que umHostforjado não direcione a produção para o banco de homologação. Em Nginx, oSERVER_NAMEvem doserver_nameconfigurado.
Criando um DAO
Cada tabela tem um DAO que estende Crud (CRUD e SQL direto) ou CrudBuilder (tudo do Crud + query builder).
use BMorais\Database\CrudBuilder; class UsuarioDao extends CrudBuilder { public function __construct() { $this->setTableName("USUARIO", "U"); // tabela e alias opcional $this->setClassModel("UsuarioModel"); // model usado quando o retorno é em models // $this->setDatabase("outro_banco"); // opcional: banco diferente de CONFIG_DATA_LAYER["dbname"] } }
Os métodos de CRUD (select, insert*, update*, delete) são públicos. Os de execução e builder
(executeSQL, fetch*, selectBuilder, where, transaction, cursor...) são protegidos e são usados dentro do DAO.
Models
Os models estendem ModelAbstract e são preenchidos automaticamente pelo PDO ou a partir de arrays/JSON.
namespace App\Models; use BMorais\Database\ModelAbstract; class UsuarioModel extends ModelAbstract { protected ?int $CODUSUARIO = null; protected ?string $NOME = null; protected ?string $EMAIL = null; public function getNOME(): ?string { return $this->NOME; } }
$model = new UsuarioModel(["NOME" => "Ana", "EMAIL" => "ana@x.com"]); // ou fromMapToModel() / fromJsonToModel() $model->toArray(); // ["CODUSUARIO" => null, "NOME" => "Ana", "EMAIL" => "ana@x.com"] (usa os getters) $model->toObject(); // stdClass $model->toJson(); // {"CODUSUARIO":null,"NOME":"Ana","EMAIL":"ana@x.com"}
Propriedades tipadas sem valor (public int $id;) são lidas como null na serialização.
CRUD
// SELECT: select(campos, complemento SQL, valores, retornarModel) $usuarios = $this->select("CODUSUARIO, NOME", "WHERE ATIVO = ? ORDER BY NOME", [1]); // stdClass[] $models = $this->select("*", "WHERE CODUSUARIO = ?", [$id], true); // UsuarioModel[] $comJoin = $this->select("U.NOME, P.TELEFONE", "LEFT JOIN PERFIL P ON P.CODUSUARIO = U.CODUSUARIO WHERE U.CODUSUARIO = ?", [$id]); // INSERT $this->insert("NOME, EMAIL", [$nome, $email]); $id = $this->lastInsertId(); $this->insertArray(["NOME" => $nome, "EMAIL" => $email]); // valores null são ignorados $this->insertObject($model->toObject()); // propriedades públicas do objeto $this->insertModel($model); // lê também propriedades protected/private // UPDATE: valores das colunas + valores do where $this->update("EMAIL", [$email, $id], "CODUSUARIO = ?"); $this->updateArray(["NOME" => $nome], "CODUSUARIO = ?", [$id]); // null é ignorado $this->updateObject($model->toObject(), "CODUSUARIO = ?", [$id]); $this->updateModel($model, "CODUSUARIO = ?", [$id], ["EMAIL"]); // grava NULL nas colunas listadas // DELETE $this->delete([$id], "CODUSUARIO = ?");
Não passe
$_POSTdireto parainsertArray/updateArray: monte o array só com as colunas permitidas.update()/delete()sem WHERE afetam a tabela inteira (e registram um aviso no log de erros do PHP).
SQL direto e leitura do resultado
$this->executeSQL("SELECT * FROM USUARIO WHERE ATIVO = ?", [1]); $lista = $this->fetchArrayObj();
| Método | Retorno |
|---|---|
fetchArrayObj() |
lista de stdClass |
fetchArrayAssoc() |
lista de arrays associativos |
fetchArrayClass(null, "OutroModel") |
lista de models (classModel ou a classe informada) |
fetchOneObj() / fetchOneAssoc() / fetchOneClass() |
um registro ou null |
rowCount() |
linhas afetadas/retornadas pelo último comando |
Os fetch* também aceitam o PDOStatement retornado por executeSQL(). Parâmetros nomeados (:email) também
funcionam; não misture com ? na mesma consulta.
Query builder
Disponível em DAOs que estendem CrudBuilder. A ordem das chamadas é livre: o SQL é montado na ordem correta e os
parâmetros acompanham cada trecho. O estado é limpo a cada executeQuery().
$resultado = $this->selectBuilder("U.CODUSUARIO, U.NOME, P.DESCRICAO") ->innerJoin("PERFIL", "P", "P.CODPERFIL = U.CODPERFIL") // também leftJoin / rightJoin ->where("U.ATIVO = ?", [1]) ->andWhere("U.NOME LIKE ?", ["%{$nome}%"]) ->orWhere("U.EMAIL LIKE ?", ["%{$nome}%"]) ->groupBy("U.CODUSUARIO") ->having("COUNT(*) > 0") ->orderBy("U.NOME") ->addOrderBy("U.CODUSUARIO", "DESC") ->limit("20") ->offset("40") ->executeQuery() ->fetchArrayObj(); $this->insertBuilder("NOME, EMAIL", [$nome, $email])->executeQuery(); $this->updateBuilder("NOME", [$nome])->where("CODUSUARIO = ?", [$id])->executeQuery(); $this->deleteBuilder()->where("CODUSUARIO = ?", [$id])->executeQuery(); // SQL livre como trecho principal $this->query("SELECT ... UNION SELECT ...", [$a, $b])->executeQuery()->fetchArrayAssoc();
Para ordenação vinda do usuário, use uma whitelist de colunas e direção (veja listarOrdenado() no exemplo).
Transações
Forma recomendada: transaction() faz o commit ao final e o rollback em qualquer erro, relançando a exceção.
O segundo parâmetro repete o bloco em deadlock (1213) ou lock wait timeout (1205). Dentro de uma transação já
aberta, apenas participa dela.
$id = $this->transaction(function () use ($dados) { $this->insertArray($dados); $id = $this->lastInsertId(); $this->query("INSERT INTO HISTORICO (CODUSUARIO, ACAO) VALUES (?, ?)", [$id, "CRIADO"])->executeQuery(); return $id; }, 3);
Vários DAOs na mesma transação: passe a mesma conexão com setInstance().
$perfilDao = new PerfilDao(); $perfilDao->setInstance($this->getInstance()); $this->transaction(function () use ($perfilDao, $dados) { $this->insertArray($dados["usuario"]); $perfilDao->insertArray($dados["perfil"]); });
Forma manual: beginTransaction(), commitTransaction() e rollBackTransaction() ignoram chamadas repetidas.
try { $this->beginTransaction(); // ... $this->commitTransaction(); } catch (\Throwable $e) { // não use catch (Error): DatabaseException é Exception $this->rollBackTransaction(); throw $e; }
Grandes volumes
Inserção em lote: insertMany()
Um INSERT com várias linhas por lote, dentro de uma transação (tudo ou nada). Todas as linhas devem ter as mesmas
colunas; null é gravado como NULL. Respeita o limite de 65.535 placeholders do MySQL.
$total = $this->insertMany([ ["NOME" => "Ana", "EMAIL" => "ana@x.com"], ["NOME" => "Bia", "EMAIL" => "bia@x.com"], ], 1000); // linhas por lote
Em MySQL 8.4, 20.000 linhas: 1.565 ms com insertArray em laço contra 119 ms com insertMany (13x).
Leitura linha a linha: cursor()
Percorre o resultado sem carregar tudo em memória, ideal para exportações e relatórios. Usa uma conexão própria (sem buffer no MySQL), fechada ao final ou quando o laço é interrompido.
foreach ($this->cursor("SELECT * FROM PEDIDO WHERE DATA >= ?", [$inicio]) as $pedido) { fputcsv($arquivo, (array) $pedido, ",", "\"", ""); } foreach ($this->cursor("SELECT * FROM PEDIDO", null, true) as $model) { /* instâncias de classModel */ }
Em MySQL 8.4, 300.000 linhas: fetchArrayObj() usa 258 MB (estoura o memory_limit padrão de 128 MB);
cursor() usa 0,1 MB. Por ser outra conexão, o cursor não enxerga dados ainda não commitados da transação atual.
IN (...) seguro: placeholders()
$sql = "SELECT * FROM USUARIO WHERE CODUSUARIO IN (" . $this->placeholders($ids) . ") AND ATIVO = ?"; $this->executeSQL($sql, [...array_values($ids), 1]);
Lista vazia vira IN (NULL), que não retorna linhas (em vez de erro de sintaxe).
Segurança
- Todos os valores vão como parâmetros (
?ou:nome). Nunca concatene valores no SQL; para listas useplaceholders(). - Nomes de coluna em
insert*/update*/insertManysão validados (inclusive chaves de arrays). orderBy,addOrderBy,limiteoffsetrejeitam;,--,#e/* */. Mesmo assim, valores vindos da requisição devem passar por whitelist (colunas) ou(int)(limite/página).- A mensagem de falha de conexão não expõe usuário, host nem banco; o detalhe original fica em
getPrevious(). - Veja a nota sobre homologação em Apache na seção Configuração.
Erros, log e debug
Erros de banco lançam BMorais\Database\DatabaseException:
try { $this->select("*", "WHERE CODUSUARIO = ?", [$id]); } catch (DatabaseException $e) { $e->getMessage(); // mensagem (inclui a do driver, ex.: código 1062 em chave duplicada) $e->getQuery(); // SQL executado $e->getParameters(); // parâmetros $e->getPrevious(); // PDOException original (errorInfo) error_log(json_encode($e->getContextInfo())); // tudo acima, para log — não exiba ao usuário }
- Debug:
$debug = trueemselect/insert/update/deleteimprime o SQL sem executar;debug()no builder imprime a consulta em construção e encerra o script. Use só em desenvolvimento. - Log: com
display_errors_detailsativo,getLogSQL()devolve a última consulta com os valores no lugar dos parâmetros (valores acima de 64 KB aparecem como[N bytes]).
Conexões
- DAOs que apontam para o mesmo banco compartilham uma única conexão PDO.
- Se a conexão cair (MySQL 2006/2013), ela é refeita e o comando repetido apenas quando é seguro: fora de transação e, no 2013, só para leituras.
closeConnections()fecha as conexões em cache (útil em workers e processos longos); conexões com transação aberta são mantidas.
Exemplo completo
A pasta example tem um config de referência (config.php) e um DAO com todas as
formas de uso (ExampleDao.php): CRUD, builder, fetch, transações, grandes volumes,
debug e tratamento de erros.
Suporte
Se você descobrir algum problema relacionado à segurança, envie um e-mail para contato@bmorais.com em vez de usar o rastreador de issues.
Contribuindo
Contribuições são bem-vindas. Faça um fork, crie uma branch (git checkout -b feature/minha-feature), faça o commit e
abra um Pull Request. Siga Conventional Commits e
Semantic Versioning, e não altere assinaturas de métodos existentes: o pacote é usado por
vários projetos.
Créditos
- Bruno Morais (Developer)
- Athus Felipe (Developer)
Licença
The MIT License (MIT). Veja o arquivo de licença.