Funciones personalizadas en Doctrine ORM (Parte 1)

Doctrine es uno de los ORM más populares y utilizados en PHP y, en los últimos tiempos, ha ganado aún más popularidad tras convertirse en el ORM por defecto en Symfony. Doctrine soporta los motores de bases de datos más habituales (MySQL, PostgreSQL, SQL Server, Oracle, etc.) y permite realizar consultas utilizando un lenguaje independiente de los mismos llamado Doctrine Query Language (DQL). DQL es un lenguaje de consulta similar a SQL y está inspirado en HQL, el lenguaje de Hibernate, que es otro de los pesos pesados en el mundo de los ORM.

DQL permite expresar consultas en función de entidades y las relaciones entre ellas. Estas consultas basadas en objetos y relaciones son transformadas por Doctrine en consultas SQL nativas apropiadas para el motor de base de datos utilizado. A diferencia de SQL, DQL soporta un conjunto reducido de funciones, que podemos utilizar para realizar consultas. Sin embargo, cada motor de base de datos dispone de una mayor cantidad de funciones específicas de dicho motor con las que podemos realizar consultas más complejas e interesantes. Afortunadamente, Doctrine nos permite definir funciones personalizadas para cada motor de base de datos y superar esta limitación. Esta funcionalidad nos ha resultado bastante útil en varios proyectos y por ello he querido dedicarle un par de posts.

Comenzamos con la primera parte,  ¡allá vamos!

Definiendo nuestra función personalizada

Para definir una función personalizada es necesario crear una subclase de la clase Doctrine\ORM\Query\AST\Functions\FunctionNode de Doctrine, que deberá implementar los siguientes métodos:

  • parse(): Indica a Doctrine cómo debe parsear nuestra función, es decir, aquí definiremos la estructura de la llamada a nuestra función, indicando la posición de símbolos como los paréntesis, parámetros, etc.
  • getSql(): Genera el SQL nativo correspondiente a nuestra función. Aquí haremos uso de los valores previamente parseados en parse() (parámetros de la función).

Para poner esto en práctica vamos a ver cómo definir la función LPAD() presente en motores como MySQL o PostgreSQL, que permite añadir relleno por la izquierda a una cadena de texto. La estructura de la llamada a esta función (que definiremos en el método parse()) va a ser la siguiente:

LPAD ( [Campo] , [Longitud] , [Relleno] )

donde:

  • LPAD: Nombre de la función.
  • ( : Paréntesis de apertura.
  • [Campo]: Parámetro, nombre del campo sobre el que se va a aplicar el padding.
  • ,: Coma, separador de parámetros.
  • [Longitud]: Parámetro, longitud total una vez aplicado el relleno.
  • ,: Coma, separador de parámetros.
  • [Relleno]: Parámetro, carácter de relleno.
  • ) : Paréntesis de cierre.

El método getSql() es trivial para esta función en MySQL, ya que la estructura es la misma que la descrita. Teniendo esto en cuenta, ya podemos implementar nuestra función de la siguiente forma:

<?php namespace MyProject\DoctrineExtensions\Query\MySql; use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\AST\Functions\FunctionNode; /** * LeftPad ::= "LPAD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" */ class LeftPad extends FunctionNode { public $stringExpression    = null; public $lengthExpression    = null; public $padStringExpression = null; public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->stringExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$this->lengthExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$this->padStringExpression = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
 
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'LPAD(' .
$this->stringExpression->dispatch($sqlWalker) . ', ' .
$this->lengthExpression->dispatch($sqlWalker) . ', ' .
$this->padStringExpression->dispatch($sqlWalker) .
')';
}
}

* Nota: en este ejemplo estoy suponiendo que mi espacio de nombres es MyProject\DoctrineExtensions\Query\MySql, que correspondería al directorio MyProject/DoctrineExtensions/Query/MySql. En cada proyecto, este espacio de nombres puede ser diferente.

Como se puede ver, simplemente hemos creado la clase LeftPad, que hereda de FunctionNode y definido los métodos parse() y getSql(). Hay que tener en cuenta que el nombre de la clase (LeftPad) NO es el nombre de la función que usaremos en DQL. Dicho nombre se define al registrar la función en Doctrine, tal y como veremos en la siguiente sección.

En el método parse() estamos indicando a Doctrine cómo debe parsear la llamada a nuestra función. El código es bastante explicativo y se parece a la lista de parámetros y tokens que hemos definido cuando analizábamos la estructura que debía tener nuestra función. Para indicar al parser que debe encontrar un token, utilizamos la función $parser->match(), tal y como se hace para el nombre de la función ($parser->match(Lexer::T_IDENTIFIER)) o el paréntesis de apertura ($parser->match(Lexer::T_OPEN_PARENTHESIS)). En el caso de los parámetros, estamos utilizando 2 funciones:

  • $parser->ArithmeticPrimary(): Valores “aritméticos” no entrecomillados, tales como números o nombres de campos. Utilizamos esta función para obtener el valor de los 2 primeros parámetros (nombre de campo y longitud de relleno).
  • $parser->StringPrimary(): Valores de cadena de texto entrecomilladas. Utilizamos esta función para obtener el tercer parámetro (cadena de relleno).

Los resultados de estas llamadas se guardan en las propiedades $stringExpression (nombre del campo), $lengthExpression (longitud de relleno) y $padStringExpression (carácter de relleno), para utilizarse posteriormente en la función getSql(). Una vez parseada la llamada a nuestra función, Doctrine llamará al método getSql() para obtener el SQL nativo correspondiente. Como se puede ver, en este caso la implementación de este método es bastante sencilla.

Registrando y utilizando nuestra función

Una vez definida la función, sólo nos queda registrarla en Doctrine para poder empezar a utilizarla. Las funciones se registran en el objeto de configuración de Doctrine (\Doctrine\ORM\Configuration) haciendo uso de una de las siguientes funciones, dependiendo del tipo del valor devuelto por nuestra función:

  • $config->addCustomStringFunction($name, $class);: Funciones que devuelven cadenas de texto.
  • $config->addCustomNumericFunction($name, $class);: Funciones que devuelven números.
  • $config->addCustomDatetimeFunction($name, $class);: Funciones que devuelven fecha/hora.

En cualquiera de estas llamadas, $name debe ser el nombre que usaremos para llamar a nuestra función en DQL (en este ejemplo LPAD) y $class el nombre de la clase (incluido el espacio de nombres) que define dicha función (en nuestro caso MyProject\DoctrineExtensions\Query\MySql\LeftPad)) y que hemos definido previamente. De esta forma, registrar nuestra función es tan sencillo como:

<?php $config = new \Doctrine\ORM\Configuration(); $config->addCustomStringFunction('LPAD', 'MyProject\\DoctrineExtensions\\Query\\MySql\\LeftPad');
$em = EntityManager::create($dbParams, $config);

* Nota: $dbParams son los parámetros de configuración de la conexión a la base de datos.

Una vez registrada nuestra función, ya podemos utilizarla en nuestras consultas DQL (o usando el Query Builder). Si, por ejemplo, tuviéramos una entidad llamada Producto, con un código de producto alfanumérico sku, y quisiéramos consultar por dicho número de producto pero rellenado con ceros (hasta longitud 10), podríamos realizar la siguiente consulta:

$query = $em->createQuery(
"SELECT p
FROM Entities\Producto p
WHERE LPAD(p.sku,10,'0') = '0000000001'"
);

Conclusiones

En este post hemos visto que, aunque la definición de funciones personalizadas en Doctrine se considera un tema avanzado, en realidad es bastante sencillo una vez revisado algún ejemplo. Esta funcionalidad nos permite extender el parser de Doctrine y crear nuestras propias funciones, que podremos utilizar a la hora de realizar consultas con DQL. De esta forma, podemos ir creando nuestra propia librería de funciones que nos permitirá realizar consultas más complejas y aprovechar al máximo las capacidades de cada motor de base de datos. Además, gracias a la popularidad de Doctrine, no es difícil encontrar librerías de funciones personalizadas ya desarrolladas, tales como las extensiones utilizadas en OroCRM.

The following two tabs change content below.

Mikel Pintor

Desarrollador web.

Latest posts by Mikel Pintor (see all)

Compartir: