Outros idiomas:
- English documentation
- Русская документация
- Documentation française
- Deutsche Dokumentation
- Documentazione italiana
- 日本語ドキュメント
- Documentación en español
- 한국어 문서
- 简体中文文档
- 繁體中文文件
- Dokumentasi Bahasa Indonesia
- हिंदी दस्तावेज़
- التوثيق بالعربية
- Türkçe Dokümantasyon
- Tài liệu tiếng Việt
Você pode baixá-la como arquivo, cloná-la deste site ou instalá-la via composer (link para packagist.org):
composer require krugozor/database
krugozor/database é uma biblioteca de classes PHP >= 8.0 para trabalho simples, conveniente, rápido e seguro com banco de dados MySQL, usando a extensão PHP mysqli.
Por que é necessária uma classe personalizada para MySQL se o PHP já tem a abstração PDO e a extensão mysqli?
As principais desvantagens de todas as bibliotecas para trabalhar com banco de dados MySQL em PHP são:
- Verbosidade
- Para prevenir injeções SQL, os desenvolvedores têm dois caminhos:
- Usar declarações preparadas.
- Escapar manualmente os parâmetros que vão para o corpo da consulta SQL. Parâmetros de string são passados por mysqli_real_escape_string, e parâmetros numéricos esperados são convertidos para os tipos correspondentes —
intefloat.
- Ambas as abordagens têm desvantagens colossais:
- Declarações preparadas são terrivelmente verbosas. Usar "fora da caixa" a abstração PDO ou a extensão mysqli, sem agregar todos os métodos para obter dados do DBMS é simplesmente impossível — para obter um valor de uma tabela é necessário escrever no mínimo 5 linhas de código! E assim para cada consulta!
- Escapar manualmente os parâmetros que vão para o corpo da consulta SQL — nem mesmo é discutido. Um bom programador é um programador preguiçoso. Tudo deve ser maximamente automatizado.
- Para prevenir injeções SQL, os desenvolvedores têm dois caminhos:
- Impossibilidade de obter a consulta SQL para depuração
- Para entender por que uma consulta SQL não funciona no programa, é necessário depurá-la — encontrar um erro lógico ou sintático. Para encontrar o erro, é necessário "ver" a consulta SQL em si, sobre a qual o banco de dados "reclamou", com os parâmetros substituídos em seu corpo. Ou seja, ter SQL completo e formado. Se o desenvolvedor usa PDO com declarações preparadas, isso é... IMPOSSÍVEL! Não há mecanismos maximamente convenientes para isso PREVISTOS nas bibliotecas nativas. Resta apenas fazer contorcionismos ou olhar no log do banco de dados.
- Elimina a verbosidade — em vez de 3 ou mais linhas de código para executar uma consulta ao usar a biblioteca "nativa", você escreve apenas uma.
- Escapa todos os parâmetros que vão para o corpo da consulta, de acordo com o tipo de placeholder especificado — proteção confiável contra injeções SQL.
- Não substitui a funcionalidade do adaptador mysqli "nativo", mas simplesmente o complementa.
- Extensível. Em essência, a biblioteca fornece apenas um parser e a execução de consultas SQL com proteção garantida contra injeções SQL. Você pode herdar de qualquer classe da biblioteca e, usando tanto os mecanismos da biblioteca quanto os mecanismos de
mysqliemysqli_result, criar os métodos necessários para trabalhar.
A maioria dos wrappers para vários drivers de banco de dados são um amontoado de código inútil com arquitetura horrível. Seus autores, não compreendendo eles mesmos o propósito prático de seus wrappers, os transformam em construtores de consultas (sql builder), bibliotecas ActiveRecord e outras soluções ORM.
A biblioteca krugozor/database não é nada disso. É apenas uma ferramenta conveniente para trabalhar com SQL normal no contexto do DBMS MySQL — e nada mais!
Placeholders (marcadores) são marcadores tipados especiais que são escritos na string da consulta SQL no lugar de valores explícitos (parâmetros da consulta). Os valores em si são passados "depois", como argumentos subsequentes do método principal que executa a consulta SQL:
$result = $db->query(
"SELECT * FROM `users` WHERE `name` = '?s' AND `age` = ?i",
"D'Artagnan", 41
);Os parâmetros da consulta SQL que passaram pelo sistema de placeholders são processados por mecanismos especiais de escape, dependendo do tipo de placeholder. Ou seja, agora você não precisa mais colocar variáveis em funções de escape como mysqli_real_escape_string() ou convertê-las para tipo numérico, como era feito antes:
<?php
// Antes, antes de cada consulta ao DBMS fazíamos
// aproximadamente isso (e muitos ainda não fazem "isso"):
$id = (int) $_POST['id'];
$value = mysqli_real_escape_string($mysql, $_POST['value']);
$result = mysqli_query($mysql, "SELECT * FROM `t` WHERE `f1` = '$value' AND `f2` = $id");Agora escrever consultas ficou fácil, rápido e, o mais importante, a biblioteca krugozor/database previne completamente todas as possíveis injeções SQL.
Os tipos de placeholders e seus propósitos são descritos abaixo. Antes de se familiarizar com os tipos de placeholders, é necessário entender como funciona o mecanismo da biblioteca.
PHP é uma linguagem fracamente tipada e durante o desenvolvimento desta biblioteca surgiu um dilema ideológico. Imagine que temos uma tabela com a seguinte estrutura:
`name` varchar not null
`flag` tinyint not nulle a biblioteca DEVE (por algum motivo, possivelmente não dependente do desenvolvedor) executar a seguinte consulta:
$db->query(
"INSERT INTO `t` SET `name` = '?s', `flag` = ?i",
null, false
);Neste exemplo, há uma tentativa de escrever o valor null no campo de texto not null name,
e o tipo booleano false no campo numérico flag. O que fazer nesta situação?
- Quem é responsável pela validação dos parâmetros da consulta - o código cliente ou a biblioteca?
- É necessário neste caso interromper a execução do programa ou, talvez, aplicar algumas manipulações para que os dados sejam escritos no banco de dados?
- Podemos interpretar o valor
falsepara a colunatinyintcomo valor0enullcomo string vazia para a colunaname? - Como podemos simplificar ou padronizar esta problemática em nosso código?
Em vista das questões levantadas, foi decidido implementar nesta biblioteca dois modos de operação.
- Mysql::MODE_STRICT — modo estrito de correspondência entre tipo de placeholder e tipo de argumento.
No modo
Mysql::MODE_STRICTo tipo de argumento deve corresponder ao tipo de placeholder. Por exemplo, a tentativa de passar como argumento o valor55.5ou'55.5'para o placeholder de tipo inteiro?igerará uma exceção:
// estabelecemos o modo estrito
$db->setTypeMode(Mysql::MODE_STRICT);
// esta expressão não será executada, uma exceção será lançada:
// tentativa de especificar para o placeholder de tipo "integer" um valor de tipo "double" no template da consulta "SELECT ?i"
$db->query('SELECT ?i', 55.5);- Mysql::MODE_TRANSFORM — modo de conversão do argumento para o tipo de placeholder em caso de não correspondência entre tipo de placeholder e tipo de argumento. O modo
Mysql::MODE_TRANSFORMé definido por padrão e é um modo "tolerante" — em caso de não correspondência entre tipo de placeholder e tipo de argumento não gera uma exceção, mas tenta converter o argumento para o tipo de placeholder necessário através da própria linguagem PHP. A propósito, eu, como autor da biblioteca, sempre uso exatamente este modo, o modo estrito (Mysql::MODE_STRICT) nunca usei no trabalho real, mas talvez você precise dele.
São permitidas as seguintes conversões no modo Mysql::MODE_TRANSFORM:
- Para o tipo
int(placeholder?i) são convertidos- números de ponto flutuante, representados tanto no tipo
stringquanto no tipodouble boolTRUE é convertido emint(1), FALSE é convertido emint(0)nullé convertido emint(0)
- números de ponto flutuante, representados tanto no tipo
- Para o tipo
double(placeholder?d) são convertidos- números inteiros, representados tanto no tipo
stringquanto no tipoint boolTRUE é convertido emfloat(1), FALSE é convertido emfloat(0)nullé convertido emfloat(0)
- números inteiros, representados tanto no tipo
- Para o tipo
string(placeholder?s) são convertidosboolTRUE é convertido emstring(1) "1", FALSE é convertido emstring(1) "0". Este comportamento difere da conversão do tipoboolparaintem PHP, pois frequentemente, na prática, o tipo booleano é escrito em MySQL precisamente como número.- valor do tipo
numericé convertido em string de acordo com as regras de conversão do PHP nullé convertido emstring(0) ""
- Para o tipo
null(placeholder?n) são convertidos- quaisquer argumentos.
- Para arrays, objetos e recursos não são permitidas conversões.
$db->query(
'SELECT * FROM `users` WHERE `id` = ?i', 123
);Consulta SQL após conversão do template:
SELECT * FROM `users` WHERE `id` = 123ATENÇÃO! Se você opera com números que excedem PHP_INT_MAX, então:
- Opere-os exclusivamente como strings em seus programas.
- Não use este placeholder, use o placeholder de string
?s(veja abaixo). O fato é que números que excedemPHP_INT_MAX, o PHP os interpreta como números de ponto flutuante. O parser da biblioteca tentará converter o parâmetro para o tipoint, resultando em «o resultado será indefinido, pois float não tem precisão suficiente para retornar o resultado correto. Neste caso não será emitido nem aviso nem notice!» — php.net.
$db->query(
'SELECT * FROM `prices` WHERE `cost` IN (?d, ?d)',
12.56, '12.33'
);Consulta SQL após conversão do template:
SELECT * FROM `prices` WHERE `cost` IN (12.56, 12.33)ATENÇÃO! Se você usa a biblioteca para trabalhar com o tipo de dados double, defina a localidade apropriada para que o separador entre a parte inteira e fracionária seja igual tanto no nível PHP quanto no nível DBMS.
Os valores dos argumentos são escapados com o método mysqli::real_escape_string():
$db->query(
'SELECT "?s"',
"Vocês todos são tolos, e eu sou D'Artagnan!"
);Consulta SQL após conversão do template:
SELECT "Vocês todos são tolos, e eu sou D\'Artagnan!"Os valores dos argumentos são escapados com o método mysqli::real_escape_string() + escape de caracteres especiais usados no operador LIKE (% e _):
$db->query('SELECT "?S"', '% _');Consulta SQL após conversão do template:
SELECT "\% \_"Os valores de quaisquer argumentos são ignorados, os placeholders são substituídos pela string NULL na consulta SQL:
$db->query('SELECT ?n', 123);Consulta SQL após conversão do template:
SELECT NULL?A* — placeholder para conjunto associativo de array associativo, gera uma sequência de pares na forma chave = valor
onde o símbolo * é um dos placeholders:
i(placeholder para números inteiros)d(placeholder para números de ponto flutuante)s(placeholder para tipo string)
as regras de conversão e escape são as mesmas dos tipos escalares únicos descritos acima. Exemplo:
$db->query(
'INSERT INTO `test` SET ?Ai',
['first' => '123', 'second' => 456]
);Consulta SQL após conversão do template:
INSERT INTO `test` SET `first` = "123", `second` = "456"?a* — placeholder para conjunto de array simples (ou também associativo), gera uma sequência de valores
onde * é um dos tipos:
i(placeholder para números inteiros)d(placeholder para números de ponto flutuante)s(placeholder para tipo string)
as regras de conversão e escape são as mesmas dos tipos escalares únicos descritos acima. Exemplo:
$db->query(
'SELECT * FROM `test` WHERE `id` IN (?ai)',
[123, 456]
);Consulta SQL após conversão do template:
SELECT * FROM `test` WHERE `id` IN ("123", "456")?A[?n, ?s, ?i, ...] — placeholder para conjunto associativo com indicação explícita de tipo e número de argumentos, gera uma sequência de pares chave = valor
Exemplo:
$db->query(
'INSERT INTO `users` SET ?A[?i, "?s"]',
['age' => 41, 'name' => "D'Artagnan"]
);Consulta SQL após conversão do template:
INSERT INTO `users` SET `age` = 41,`name` = "D\'Artagnan"?a[?n, ?s, ?i, ...] — placeholder para conjunto com indicação explícita de tipo e número de argumentos, gera uma sequência de valores
Exemplo:
$db->query(
'SELECT * FROM `users` WHERE `name` IN (?a["?s", "?s"])',
["marquês d\"Arquien", "D'Artagnan"]
);Consulta SQL após conversão do template:
SELECT * FROM `users` WHERE `name` IN ("marquês d\"Arquien", "D\'Artagnan")Este placeholder é destinado para casos em que o nome da tabela ou campo é passado na consulta através de um parâmetro. Nomes de campos e tabelas são enquadrados com o símbolo "crase":
$db->query(
'SELECT ?f FROM ?f',
'name',
'database.table_name'
);Consulta SQL após conversão do template:
SELECT `name` FROM `database`.`table_name`A biblioteca requer que o programador respeite a sintaxe SQL. Isso significa que a seguinte consulta não funcionará:
$db->query(
'SELECT CONCAT("Hello, ", ?s, "!")',
'world'
);— o placeholder ?s deve ser colocado entre aspas simples ou duplas:
$db->query(
'SELECT concat("Hello, ", "?s", "!")',
'world'
);Consulta SQL após conversão do template:
SELECT concat("Hello, ", "world", "!")Para quem está acostumado a trabalhar com PDO isso pode parecer estranho, mas implementar um mecanismo que determine se em um caso é necessário colocar o valor do placeholder entre aspas ou não é uma tarefa muito não trivial que requer escrever um parser completo.
Veja no arquivo ../console/tests.php