Otros idiomas:
- English documentation
- Русская документация
- Documentation française
- Deutsche Dokumentation
- Documentazione italiana
- 日本語ドキュメント
- 한국어 문서
- 简体中文文档
- 繁體中文文件
- Dokumentasi Bahasa Indonesia
- Documentação em Português (BR)
- हिंदी दस्तावेज़
- التوثيق بالعربية
- Türkçe Dokümantasyon
- Tài liệu tiếng Việt
Puede descargarla como archivo, clonarla desde este sitio o instalarla mediante composer (enlace a packagist.org):
composer require krugozor/database
krugozor/database es una biblioteca de clases PHP >= 8.0 para trabajar de manera simple, cómoda, rápida y segura con bases de datos MySQL, utilizando la extensión PHP mysqli.
¿Por qué se necesita una clase personalizada para MySQL si PHP ya tiene la abstracción PDO y la extensión mysqli?
Las principales desventajas de todas las bibliotecas para trabajar con bases de datos MySQL en PHP son:
- Verbosidad
- Para prevenir inyecciones SQL, los desarrolladores tienen dos caminos:
- Utilizar sentencias preparadas.
- Escapar manualmente los parámetros que van al cuerpo de la consulta SQL. Pasar los parámetros de cadena a través de mysqli_real_escape_string y convertir los parámetros numéricos esperados a los tipos correspondientes —
intyfloat.
- Ambos enfoques tienen desventajas colosales:
- Las sentencias preparadas son terriblemente verbosas. Usar "fuera de la caja" la abstracción PDO o la extensión mysqli, sin agregar todos los métodos para obtener datos del DBMS es simplemente imposible — ¡para obtener un valor de una tabla es necesario escribir al menos 5 líneas de código! ¡Y así para cada consulta!
- El escape manual de parámetros que van al cuerpo de la consulta SQL — ni siquiera se discute. Un buen programador es un programador perezoso. Todo debe estar automatizado al máximo.
- Para prevenir inyecciones SQL, los desarrolladores tienen dos caminos:
- Imposibilidad de obtener la consulta SQL para depuración
- Para entender por qué una consulta SQL no funciona en el programa, es necesario depurarla — encontrar un error lógico o sintáctico. Para encontrar el error, es necesario "ver" la consulta SQL misma, sobre la cual la base de datos "se quejó", con los parámetros sustituidos en su cuerpo. Es decir, tener SQL completo y formado. Si el desarrollador usa PDO con sentencias preparadas, esto es... ¡IMPOSIBLE! No hay mecanismos máximamente convenientes para esto PREVISTOS en las bibliotecas nativas. Solo queda hacer contorsiones o mirar en el registro de la base de datos.
- Elimina la verbosidad — en lugar de 3 o más líneas de código para ejecutar una consulta al usar la biblioteca "nativa", escribes solo una.
- Escapa todos los parámetros que van al cuerpo de la consulta, según el tipo de marcador de posición especificado — protección confiable contra inyecciones SQL.
- No reemplaza la funcionalidad del adaptador mysqli "nativo", sino que simplemente lo complementa.
- Extensible. En esencia, la biblioteca proporciona solo un analizador y la ejecución de consultas SQL con protección garantizada contra inyecciones SQL. Puede heredar de cualquier clase de la biblioteca y, utilizando tanto los mecanismos de la biblioteca como los mecanismos de
mysqliymysqli_result, crear los métodos necesarios para trabajar.
La mayoría de los wrappers para varios controladores de bases de datos son un amontonamiento de código inútil con una arquitectura horrible. Sus autores, sin comprender ellos mismos el propósito práctico de sus wrappers, los convierten en constructores de consultas (sql builder), bibliotecas ActiveRecord y otras soluciones ORM.
La biblioteca krugozor/database no es nada de lo mencionado. ¡Es solo una herramienta conveniente para trabajar con SQL normal en el marco del DBMS MySQL — y nada más!
Los placeholders (marcadores de posición) son marcadores tipados especiales que se escriben en la cadena de consulta SQL en lugar de valores explícitos (parámetros de consulta). Los valores mismos se pasan "después", como argumentos subsiguientes del método principal que ejecuta la consulta SQL:
$result = $db->query(
"SELECT * FROM `users` WHERE `name` = '?s' AND `age` = ?i",
"D'Artagnan", 41
);Los parámetros de consulta SQL que han pasado por el sistema de placeholders son procesados por mecanismos especiales de escape, dependiendo del tipo de marcador de posición. Es decir, ahora no es necesario encerrar las variables en funciones de escape como mysqli_real_escape_string() o convertirlas a tipo numérico, como se hacía antes:
<?php
// Antes, antes de cada consulta al DBMS hacíamos
// aproximadamente esto (y muchos todavía no hacen "esto"):
$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");Ahora escribir consultas se ha vuelto fácil, rápido y, lo más importante, la biblioteca krugozor/database previene completamente todas las posibles inyecciones SQL.
Los tipos de marcadores de posición y su propósito se describen a continuación. Antes de familiarizarse con los tipos de marcadores de posición, es necesario entender cómo funciona el mecanismo de la biblioteca.
PHP es un lenguaje débilmente tipado y durante el desarrollo de esta biblioteca surgió un dilema ideológico. Imaginemos que tenemos una tabla con la siguiente estructura:
`name` varchar not null
`flag` tinyint not nully la biblioteca DEBE (por alguna razón, posiblemente no dependiente del desarrollador) ejecutar la siguiente consulta:
$db->query(
"INSERT INTO `t` SET `name` = '?s', `flag` = ?i",
null, false
);En este ejemplo, se intenta escribir el valor null en el campo de texto not null name,
y el tipo booleano false en el campo numérico flag. ¿Qué hacer en esta situación?
- ¿Quién es responsable de la validación de los parámetros de consulta - el código del cliente o la biblioteca?
- ¿Es necesario en este caso interrumpir la ejecución del programa o, tal vez, aplicar algunas manipulaciones para que los datos se escriban en la base de datos?
- ¿Podemos interpretar el valor
falsepara la columnatinyintcomo valor0ynullcomo cadena vacía para la columnaname? - ¿Cómo podemos simplificar o estandarizar en nuestro código esta problemática?
En vista de las preguntas planteadas, se decidió implementar en esta biblioteca dos modos de funcionamiento.
- Mysql::MODE_STRICT — modo estricto de correspondencia entre tipo de marcador de posición y tipo de argumento.
En el modo
Mysql::MODE_STRICTel tipo de argumento debe corresponder al tipo de marcador de posición. Por ejemplo, el intento de pasar como argumento el valor55.5o'55.5'para el marcador de posición de tipo entero?igenerará una excepción:
// establecemos el modo estricto
$db->setTypeMode(Mysql::MODE_STRICT);
// esta expresión no se ejecutará, se lanzará una excepción:
// intento de especificar para el marcador de posición de tipo "integer" un valor de tipo "double" en la plantilla de consulta "SELECT ?i"
$db->query('SELECT ?i', 55.5);- Mysql::MODE_TRANSFORM — modo de conversión del argumento al tipo de marcador de posición en caso de no correspondencia entre tipo de marcador de posición y tipo de argumento. El modo
Mysql::MODE_TRANSFORMestá establecido por defecto y es un modo "tolerante" — en caso de no correspondencia entre tipo de marcador de posición y tipo de argumento no genera una excepción, sino que intenta convertir el argumento al tipo de marcador de posición requerido mediante el propio lenguaje PHP. Por cierto, yo, como autor de la biblioteca, siempre uso precisamente este modo, el modo estricto (Mysql::MODE_STRICT) nunca lo he usado en el trabajo real, pero quizás justo a usted le será necesario.
Se permiten las siguientes conversiones en el modo Mysql::MODE_TRANSFORM:
- Al tipo
int(marcador de posición?i) se convierten- números de punto flotante, representados tanto en tipo
stringcomo en tipodouble boolTRUE se convierte enint(1), FALSE se convierte enint(0)nullse convierte enint(0)
- números de punto flotante, representados tanto en tipo
- Al tipo
double(marcador de posición?d) se convierten- números enteros, representados tanto en tipo
stringcomo en tipoint boolTRUE se convierte enfloat(1), FALSE se convierte enfloat(0)nullse convierte enfloat(0)
- números enteros, representados tanto en tipo
- Al tipo
string(marcador de posición?s) se conviertenboolTRUE se convierte enstring(1) "1", FALSE se convierte enstring(1) "0". Este comportamiento difiere de la conversión del tipoboolainten PHP, ya que a menudo, en la práctica, el tipo booleano se escribe en MySQL precisamente como número.- valor de tipo
numericse convierte en cadena según las reglas de conversión de PHP nullse convierte enstring(0) ""
- Al tipo
null(marcador de posición?n) se convierten- cualquier argumento.
- Para arrays, objetos y recursos no se permiten conversiones.
$db->query(
'SELECT * FROM `users` WHERE `id` = ?i', 123
);Consulta SQL después de la conversión de la plantilla:
SELECT * FROM `users` WHERE `id` = 123¡ATENCIÓN! Si opera con números que exceden PHP_INT_MAX, entonces:
- Opérelos exclusivamente como cadenas en sus programas.
- No use este marcador de posición, use el marcador de posición de cadena
?s(ver abajo). El hecho es que los números que excedenPHP_INT_MAX, PHP los interpreta como números de punto flotante. El analizador de la biblioteca intentará convertir el parámetro al tipoint, como resultado «el resultado será indefinido, ya que float no tiene suficiente precisión para devolver el resultado correcto. ¡En este caso no se emitirá ni advertencia ni aviso!» — php.net.
$db->query(
'SELECT * FROM `prices` WHERE `cost` IN (?d, ?d)',
12.56, '12.33'
);Consulta SQL después de la conversión de la plantilla:
SELECT * FROM `prices` WHERE `cost` IN (12.56, 12.33)¡ATENCIÓN! Si utiliza la biblioteca para trabajar con el tipo de datos double, establezca la configuración regional apropiada para que el separador entre parte entera y fraccionaria sea igual tanto a nivel de PHP como a nivel de DBMS.
Los valores de los argumentos se escapan con el método mysqli::real_escape_string():
$db->query(
'SELECT "?s"',
"¡Todos ustedes son tontos, y yo soy D'Artagnan!"
);Consulta SQL después de la conversión de la plantilla:
SELECT "¡Todos ustedes son tontos, y yo soy D\'Artagnan!"Los valores de los argumentos se escapan con el método mysqli::real_escape_string() + escape de caracteres especiales utilizados en el operador LIKE (% y _):
$db->query('SELECT "?S"', '% _');Consulta SQL después de la conversión de la plantilla:
SELECT "\% \_"Los valores de cualquier argumento se ignoran, los marcadores de posición se reemplazan con la cadena NULL en la consulta SQL:
$db->query('SELECT ?n', 123);Consulta SQL después de la conversión de la plantilla:
SELECT NULL?A* — marcador de posición para conjunto asociativo de array asociativo, genera una secuencia de pares de la forma clave = valor
donde el símbolo * es uno de los marcadores de posición:
i(marcador de posición para números enteros)d(marcador de posición para números de punto flotante)s(marcador de posición para tipo cadena)
las reglas de conversión y escape son las mismas que para los tipos escalares individuales descritos arriba. Ejemplo:
$db->query(
'INSERT INTO `test` SET ?Ai',
['first' => '123', 'second' => 456]
);Consulta SQL después de la conversión de la plantilla:
INSERT INTO `test` SET `first` = "123", `second` = "456"?a* — marcador de posición para conjunto de array simple (o también asociativo), genera una secuencia de valores
donde * es uno de los tipos:
i(marcador de posición para números enteros)d(marcador de posición para números de punto flotante)s(marcador de posición para tipo cadena)
las reglas de conversión y escape son las mismas que para los tipos escalares individuales descritos arriba. Ejemplo:
$db->query(
'SELECT * FROM `test` WHERE `id` IN (?ai)',
[123, 456]
);Consulta SQL después de la conversión de la plantilla:
SELECT * FROM `test` WHERE `id` IN ("123", "456")?A[?n, ?s, ?i, ...] — marcador de posición para conjunto asociativo con indicación explícita de tipo y número de argumentos, genera una secuencia de pares clave = valor
Ejemplo:
$db->query(
'INSERT INTO `users` SET ?A[?i, "?s"]',
['age' => 41, 'name' => "D'Artagnan"]
);Consulta SQL después de la conversión de la plantilla:
INSERT INTO `users` SET `age` = 41,`name` = "D\'Artagnan"?a[?n, ?s, ?i, ...] — marcador de posición para conjunto con indicación explícita de tipo y número de argumentos, genera una secuencia de valores
Ejemplo:
$db->query(
'SELECT * FROM `users` WHERE `name` IN (?a["?s", "?s"])',
["marqués d\"Arquién", "D'Artagnan"]
);Consulta SQL después de la conversión de la plantilla:
SELECT * FROM `users` WHERE `name` IN ("marqués d\"Arquién", "D\'Artagnan")Este marcador de posición está destinado para casos en que el nombre de tabla o campo se pasa en la consulta a través de un parámetro. Los nombres de campos y tablas se encierran con el símbolo "comilla invertida":
$db->query(
'SELECT ?f FROM ?f',
'name',
'database.table_name'
);Consulta SQL después de la conversión de la plantilla:
SELECT `name` FROM `database`.`table_name`La biblioteca requiere que el programador respete la sintaxis SQL. Esto significa que la siguiente consulta no funcionará:
$db->query(
'SELECT CONCAT("Hello, ", ?s, "!")',
'world'
);— el marcador de posición ?s debe encerrarse entre comillas simples o dobles:
$db->query(
'SELECT concat("Hello, ", "?s", "!")',
'world'
);Consulta SQL después de la conversión de la plantilla:
SELECT concat("Hello, ", "world", "!")Para quienes están acostumbrados a trabajar con PDO esto puede parecer extraño, pero implementar un mecanismo que determine si en un caso es necesario encerrar el valor del marcador de posición entre comillas o no es una tarea muy no trivial que requiere escribir un analizador completo.
Ver en el archivo ../console/tests.php