1
0
mirror of synced 2024-12-14 23:26:04 +03:00
doctrine2/cookbook/en/dql-user-defined-functions.txt
Jonathan H. Wage 1e7193134a -
2010-04-06 14:36:40 -04:00

198 lines
8.7 KiB
Plaintext

By default DQL supports a limited subset of all the vendor-specific SQL functions
common between all the vendors. However in many cases once you have decided on a
specific database vendor, you will never change it during the life of your project.
This decision for a specific vendor potentially allows you to make use of powerful
SQL features that are unique to the vendor.
> **Note**
>
> It is worth to mention that Doctrine 2 also allows you to handwrite your SQL instead of extending
> the DQL parser, which is sort of an advanced extension point. You can map arbitrary SQL to your
> objects and gain access to vendor specific functionalities using the `EntityManager#createNativeQuery()` API.
The DQL Parser has hooks to register functions that can then be used in your DQL queries and transformed into SQL,
allowing to extend Doctrines Query capabilities to the vendors strength. This post explains the
Used-Defined Functions API (UDF) of the Dql Parser and shows some examples to give you
some hints how you would extend DQL.
There are three types of functions in DQL, those that return a numerical value,
those that return a string and those that return a Date. Your custom method
has to be registered as either one of those. The return type information
is used by the DQL parser to check possible syntax errors during the parsing
process, for example using a string function return value in a math expression.
## Registering your own DQL functions
You can register your functions adding them to the ORM configuration:
[php]
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction($name, $class);
$config->addCustomNumericFunction($name, $class);
$config->addCustomDatetimeFunction($name, $class);
$em = EntityManager::create($dbParams, $config);
The `$name` is the name the function will be referred to in the DQL query. `$class` is a
string of a class-name which has to extend `Doctrine\ORM\Query\Node\FunctionNode`.
This is a class that offers all the necessary API and methods to implement
a UDF.
In this post we will implement some MySql specific Date calculation methods,
which are quite handy in my opinion:
## Date Diff
[Mysql's DateDiff function](http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_datediff)
takes two dates as argument and calculates the difference in days with `date1-date2`.
The DQL parser is a top-down recursive descent parser to generate the
Abstract-Syntax Tree (AST) and uses a TreeWalker approach to generate the appropriate
SQL from the AST. This makes reading the Parser/TreeWalker code managable
in a finite amount of time.
The `FunctionNode` class I referred to earlier requires you to implement
two methods, one for the parsing process (obviously) called `parse` and
one for the TreeWalker process called `getSql()`. I show you the code for
the DateDiff method and discuss it step by step:
[php]
/**
* DateDiffFunction ::= "DATEDIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
*/
class DateDiff extends FunctionNode
{
// (1)
public $firstDateExpression = null;
public $secondDateExpression = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER); // (2)
$parser->match(Lexer::T_OPEN_PARENTHESIS); // (3)
$this->firstDateExpression = $parser->ArithmeticPrimary(); // (4)
$parser->match(Lexer::T_COMMA); // (5)
$this->secondDateExpression = $parser->ArithmeticPrimary(); // (6)
$parser->match(Lexer::T_CLOSE_PARENTHESIS); // (3)
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'DATEDIFF(' .
$this->firstDateExpression->dispatch($sqlWalker) . ', ' .
$this->secondDateExpression->dispatch($sqlWalker) .
')'; // (7)
}
}
The Parsing process of the DATEDIFF function is going to find two expressions
the date1 and the date2 values, whose AST Node representations will be saved
in the variables of the DateDiff FunctionNode instance at (1).
The parse() method has to cut the function call "DATEDIFF" and its argument
into pieces. Since the parser detects the function using a lookahead the
T_IDENTIFIER of the function name has to be taken from the stack (2), followed
by a detection of the arguments in (4)-(6). The opening and closing parenthesis
have to be detected also. This happens during the Parsing process and leads
to the generation of a DateDiff FunctionNode somewhere in the AST of the
dql statement.
The `ArithmeticPrimary` method call is the most common denominator of valid
EBNF tokens taken from the [DQL EBNF grammer](http://www.doctrine-project.org/documentation/manual/2_0/en/dql-doctrine-query-language#ebnf)
that matches our requirements for valid input into the DateDiff Dql function.
Picking the right tokens for your methods is a tricky business, but the EBNF
grammer is pretty helpful finding it, as is looking at the Parser source code.
Now in the TreeWalker process we have to pick up this node and generate SQL
from it, which apprently is quite easy looking at the code in (7). Since
we don't know which type of AST Node the first and second Date expression
are we are just dispatching them back to the SQL Walker to generate SQL from
and then wrap our DATEDIFF function call around this output.
Now registering this DateDiff FunctionNode with the ORM using:
[php]
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction('DATEDIFF', 'DoctrineExtensions\Query\MySql\DateDiff');
We can do fancy stuff like:
[sql]
SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATEDIFF(CURRENT_TIME(), p.created) < 7
## Date Add
Often useful it the ability to do some simple date calculations in your DQL query
using [MySql's DATE_ADD function](http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-add).
I'll skip the bla and show the code for this function:
[php]
/**
* DateAddFunction ::=
* "DATE_ADD" "(" ArithmeticPrimary ", INTERVAL" ArithmeticPrimary Identifier ")"
*/
class DateAdd extends FunctionNode
{
public $firstDateExpression = null;
public $intervalExpression = null;
public $unit = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstDateExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$parser->match(Lexer::T_IDENTIFIER);
$this->intervalExpression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_IDENTIFIER);
/* @var $lexer Lexer */
$lexer = $parser->getLexer();
$this->unit = $lexer->token['value'];
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'DATE_ADD(' .
$this->firstDateExpression->dispatch($sqlWalker) . ', INTERVAL ' .
$this->intervalExpression->dispatch($sqlWalker) . ' ' . $this->unit .
')';
}
}
The only difference compared to the DATEDIFF here is, we additionally need the `Lexer` to access
the value of the `T_IDENTIFIER` token for the Date Interval unit, for example the MONTH in:
[sql]
SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATE_ADD(CURRENT_TIME(), INTERVAL 4 MONTH) > p.created
The above method now only supports the specification using `INTERVAL`, to also
allow a real date in DATE_ADD we need to add some decision logic to the parsing
process (makes up for a nice excercise).
Now as you see, the Parsing process doesn't catch all the possible SQL errors,
here we don't match for all the valid inputs for the interval unit.
However where necessary we rely on the database vendors SQL parser to show us further errors
in the parsing process, for example if the Unit would not be one of the supported values
by MySql.
## Conclusion
Now that you all know how you can implement vendor specific SQL functionalities in DQL,
we would be excited to see user extensions that add vendor specific function packages,
for example more math functions, XML + GIS Support, Hashing functions and so on.
For 2.0 we will come with the current set of functions, however for a future
version we will re-evaluate if we can abstract even more vendor sql functions
and extend the DQL languages scope.
Code for this Extension to DQL and other Doctrine Extensions can be found
[in my Github DoctrineExtensions repository](http://github.com/beberlei/DoctrineExtensions).