Skip to content

Commit b9a8677

Browse files
author
Marc TEYSSIER
committed
Merge pull request #6 from moufmouf/twig
Twig integration
2 parents 0a7ee81 + f5c57b2 commit b9a8677

13 files changed

+337
-8
lines changed

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ It comes with 2 great features:
1414

1515
- [**MagicParameters**: it helps you work with SQL queries that require a variable number of parameters.](#parameters)
1616
- [**MagicJoin**: it writes JOINs for you!](#joins)
17+
- [**MagicTwig**: use Twig templating in your SQL queries](#twig)
1718

1819
Installation
1920
------------
@@ -106,6 +107,62 @@ $completeSql = $magicQuery->build($sql);
106107

107108
Want to know more? <a class="btn btn-primary btn-large" href="doc/magic_join.md">Check out the MagicJoin guide!</a>
108109

110+
<a name="twig"></a>
111+
Use Twig templating in your SQL queries!
112+
----------------------------------------
113+
114+
Discarding unused parameters and auto-joining keys is not enough? You have very specific needs? Say hello to
115+
Twig integration!
116+
117+
Using Twig integration, you can directly add Twig conditions right into your SQL.
118+
119+
```php
120+
use Mouf\Database\MagicQuery;
121+
122+
$sql = "SELECT users.* FROM users {% if isAdmin %} WHERE users.admin = 1 {% endif %}";
123+
124+
$magicQuery = new MagicQuery();
125+
// By default, Twig integration is disabled. You need to enable it.
126+
$magicQuery->setEnableTwig(true);
127+
128+
$completeSql = $magicQuery->build($sql, ['isAdmin' => true]);
129+
// Parameters are passed to the Twig SQL query, and the SQL query is returned.
130+
```
131+
132+
<div class="alert alert-info"><strong>Heads up!</strong> The Twig integration cannot be used to insert parameters
133+
into the SQL query. You should use classic SQL parameters for this. This means that instead if writing
134+
<code>{{ id }}</code>, you should write <code>:id</code>.</div>
135+
136+
Want to know more? <a class="btn btn-primary" href="doc/magic_twig.md">Check out the MagicTwig guide!</a>
137+
138+
<a name="twig"></a>
139+
Use Twig templating in your SQL queries!
140+
----------------------------------------
141+
142+
Discarding unused parameters and auto-joining keys is not enough? You have very specific needs? Say hello to
143+
Twig integration!
144+
145+
Using Twig integration, you can directly add Twig conditions right into your SQL.
146+
147+
```php
148+
use Mouf\Database\MagicQuery;
149+
150+
$sql = "SELECT users.* FROM users {% if isAdmin %} WHERE users.admin = 1 {% endif %}";
151+
152+
$magicQuery = new MagicQuery();
153+
// By default, Twig integration is disabled. You need to enable it.
154+
$magicQuery->setEnableTwig(true);
155+
156+
$completeSql = $magicQuery->build($sql, ['isAdmin' => true]);
157+
// Parameters are passed to the Twig SQL query, and the SQL query is returned.
158+
```
159+
160+
<div class="alert alert-info"><strong>Heads up!</strong> The Twig integration cannot be used to insert parameters
161+
into the SQL query. You should use classic SQL parameters for this. This means that instead if writing
162+
<code>{{ id }}</code>, you should write <code>:id</code>.</div>
163+
164+
Want to know more? <a class="btn btn-primary" href="doc/magic_twig.md">Check out the MagicTwig guide!</a>
165+
109166
Is it a MySQL only tool?
110167
------------------------
111168

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"mouf/utils.value.value-interface": "~1.0",
1919
"mouf/utils.common.paginable-interface": "~1.0",
2020
"mouf/utils.common.sortable-interface": "~1.0",
21-
"mouf/schema-analyzer": "~1.0"
21+
"mouf/schema-analyzer": "~1.0",
22+
"twig/twig": "~1.14"
2223
},
2324
"require-dev": {
2425
"phpunit/phpunit": "~4.0",

doc/discard_unused_parameters.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ $magicQuery = new MagicQuery();
6565
$sql = $magicQuery->build($sql, $params);
6666
```
6767

68+
####Forcing some parameters to be null
69+
70+
MagicQuery assumes that if a parameter is null, you want to completely remove the code related to this
71+
parameter from the SQL.
72+
73+
But sometimes, you actually want to test parameters against the NULL value.
74+
In those cases, you need to disable the MagicParameter feature of MagicQuery for those parameters.
75+
76+
<div class="alert alert-info">To disable MagicParameter for a given parameter, simply add an exclamation
77+
mark (!) after the parameter name.</div>
78+
79+
```php
80+
// This query uses twice the "status" parameter. Once with ! and once without
81+
$sql = "SELECT * FROM products p WHERE status1 = :status AND status2 = :status!";
82+
83+
$magicQuery = new MagicQuery();
84+
// We don't pass the "status" parameter
85+
$sql = $magicQuery->build($sql, []);
86+
// The part "status1 = :status" is discarded, but the part "status2 = :status!" is kept
87+
// $sql == "SELECT * FROM products p WHERE status2 = null"
88+
```
89+
6890
###Other alternatives
6991

7092
To avoid concatenating strings, frameworks and libraries have used different strategies. Using a full ORM (like

doc/magic_twig.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Use Twig templating in your SQL queries!
2+
----------------------------------------
3+
4+
Using Twig integration, you can directly add Twig conditions right into your SQL.
5+
6+
```php
7+
use Mouf\Database\MagicQuery;
8+
9+
$sql = "SELECT users.* FROM users {% if isAdmin %} WHERE users.admin = 1 {% endif %}";
10+
11+
$magicQuery = new MagicQuery();
12+
// By default, Twig integration is disabled. You need to enable it.
13+
$magicQuery->setEnableTwig(true);
14+
15+
$completeSql = $magicQuery->build($sql, ['isAdmin' => true]);
16+
// Parameters are passed to the Twig SQL query, and the SQL query is returned.
17+
```
18+
19+
Limitations
20+
-----------
21+
22+
<div class="alert alert-info"><strong>Heads up!</strong> The Twig integration cannot be used to insert parameters
23+
into the SQL query. You should use classic SQL parameters for this. This means that instead if writing
24+
<code>{{ id }}</code>, you should write <code>:id</code>.</div>
25+
26+
You cannot directly use Twig parameters because Twig transformation is applied before SQL parsing. If parameters
27+
where replaced by Twig before SQL is parsed, the caching of the transformation *SQL => parsed SQL* would become
28+
inefficient.
29+
30+
For this reason, if you try to use `{{ parameter }}` instead of `:parameter` in your SQL query, an exception will
31+
be thrown.
32+
33+
Usage
34+
-----
35+
36+
For most queries, we found out that MagicJoin combined to MagicParameters is enough.
37+
For this reason, MagicTwig is disabled by default. If you want to enable it, you can simply call
38+
`$magicQuery->setEnableTwig(true)` before calling the `build` method.
39+
40+
MagicTwig can typically be useful when you have parts of a query that needs to be added conditionally, depending
41+
on provided parameters.

phpunit.xml.dist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@
1111
syntaxCheck="false"
1212
bootstrap="tests/bootstrap.php"
1313
>
14+
15+
<php>
16+
<var name="db_url" value="mysql://root:@localhost/"/>
17+
</php>
18+
1419
<testsuites>
1520
<testsuite name="Mouf Test Suite">
1621
<directory>./tests/</directory>
1722
</testsuite>
1823
</testsuites>
24+
25+
<filter>
26+
<whitelist processUncoveredFilesFromWhitelist="true">
27+
<directory suffix=".php">src/</directory>
28+
</whitelist>
29+
</filter>
1930
</phpunit>

src/Mouf/Database/MagicQuery.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Mouf\Database;
44

55
use Doctrine\Common\Cache\VoidCache;
6+
use Mouf\Database\MagicQuery\Twig\SqlTwigEnvironmentFactory;
67
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
78
use SQLParser\Node\ColRef;
89
use SQLParser\Node\Equal;
@@ -26,6 +27,11 @@ class MagicQuery
2627
private $connection;
2728
private $cache;
2829
private $schemaAnalyzer;
30+
/**
31+
* @var \Twig_Environment
32+
*/
33+
private $twigEnvironment;
34+
private $enableTwig = false;
2935

3036
/**
3137
* @param \Doctrine\DBAL\Connection $connection
@@ -45,6 +51,17 @@ public function __construct($connection = null, $cache = null, SchemaAnalyzer $s
4551
}
4652
}
4753

54+
/**
55+
* Whether Twig parsing should be enabled or not.
56+
* Defaults to false.
57+
* @param bool $enableTwig
58+
* @return $this
59+
*/
60+
public function setEnableTwig($enableTwig = true) {
61+
$this->enableTwig = $enableTwig;
62+
return $this;
63+
}
64+
4865
/**
4966
* Returns merged SQL from $sql and $parameters. Any parameters not available will be striped down
5067
* from the SQL.
@@ -58,6 +75,9 @@ public function __construct($connection = null, $cache = null, SchemaAnalyzer $s
5875
*/
5976
public function build($sql, array $parameters = array())
6077
{
78+
if ($this->enableTwig) {
79+
$sql = $this->getTwigEnvironment()->render($sql, $parameters);
80+
}
6181
$select = $this->parse($sql);
6282
return $this->toSql($select, $parameters);
6383
}
@@ -218,4 +238,14 @@ private function getSchemaAnalyzer() {
218238
private function getConnectionUniqueId() {
219239
return hash('md4', $this->connection->getHost()."-".$this->connection->getPort()."-".$this->connection->getDatabase()."-".$this->connection->getDriver()->getName());
220240
}
241+
242+
/**
243+
* @return \Twig_Environment
244+
*/
245+
private function getTwigEnvironment() {
246+
if ($this->twigEnvironment === null) {
247+
$this->twigEnvironment = SqlTwigEnvironmentFactory::getTwigEnvironment();
248+
}
249+
return $this->twigEnvironment;
250+
}
221251
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
namespace Mouf\Database\MagicQuery\Twig;
3+
4+
use Mouf\Database\MagicQueryException;
5+
6+
class ForbiddenTwigParameterInSqlException extends MagicQueryException
7+
{
8+
9+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
namespace Mouf\Database\MagicQuery\Twig;
3+
4+
use Doctrine\DBAL\Connection;
5+
6+
/**
7+
* Class in charge of creating the Twig environment
8+
*/
9+
class SqlTwigEnvironmentFactory
10+
{
11+
private static $twig;
12+
13+
public static function getTwigEnvironment() {
14+
if (self::$twig) {
15+
return self::$twig;
16+
}
17+
18+
$stringLoader = new StringLoader();
19+
20+
$options = array(
21+
// The cache directory is in the temporary directory and reproduces the path to the directory (to avoid cache conflict between apps).
22+
'cache' => self::getCacheDirectory(),
23+
'strict_variables' => true
24+
);
25+
26+
$twig = new \Twig_Environment($stringLoader, $options);
27+
28+
// Default escaper will throw an exception. This is because we want to use SQL parameters instead of Twig.
29+
// This ahs a number of advantages, especially in terms of caching.
30+
$twig->getExtension('core')->setEscaper('sql', function() {
31+
throw new ForbiddenTwigParameterInSqlException('You cannot use Twig expressions (like "{{ id }}"). Instead, you should use SQL parameters (like ":id"). Twig integration is limited to Twig statements (like "{% for .... %}"');
32+
});
33+
34+
// Default autoescape mode: sql
35+
$twig->addExtension(new \Twig_Extension_Escaper('sql'));
36+
37+
self::$twig = $twig;
38+
39+
return $twig;
40+
}
41+
42+
private static function getCacheDirectory() {
43+
// If we are running on a Unix environment, let's prepend the cache with the user id of the PHP process.
44+
// This way, we can avoid rights conflicts.
45+
46+
// @codeCoverageIgnoreStart
47+
if (function_exists('posix_geteuid')) {
48+
$posixGetuid = '_'.posix_geteuid();
49+
} else {
50+
$posixGetuid = '';
51+
}
52+
// @codeCoverageIgnoreEnd
53+
$cacheDirectory = rtrim(sys_get_temp_dir(), '/\\').'/magicquerysqltwigtemplate'.$posixGetuid.str_replace(":", "", __DIR__);
54+
55+
return $cacheDirectory;
56+
}
57+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
namespace Mouf\Database\MagicQuery\Twig;
3+
4+
5+
/**
6+
* This loader completely bypasses the loader mechanism, by directly passing the key as a template.
7+
* Useful in our very case.
8+
*
9+
* This is a reimplementation of Twig's String loader that has been deprecated.
10+
* We enable it back in our case because there won't be a million of different cache keys.
11+
* And yes, we know what we are doing :)
12+
*/
13+
class StringLoader implements \Twig_LoaderInterface
14+
{
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
public function getSource($name)
19+
{
20+
return $name;
21+
}
22+
/**
23+
* {@inheritdoc}
24+
*/
25+
public function getCacheKey($name)
26+
{
27+
return $name;
28+
}
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function isFresh($name, $time)
33+
{
34+
return true;
35+
}
36+
}

src/SQLParser/Node/AbstractTwoOperandsOperator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ public function toSql(array $parameters = array(), Connection $dbConnection = nu
101101
if ($conditionsMode == self::CONDITION_GUESS) {
102102
$bypass = false;
103103
if ($this->leftOperand instanceof Parameter) {
104-
if (!isset($parameters[$this->leftOperand->getName()])) {
104+
if ($this->leftOperand->isDiscardedOnNull() && !isset($parameters[$this->leftOperand->getName()])) {
105105
$bypass = true;
106106
}
107107
}
108108
if ($this->rightOperand instanceof Parameter) {
109-
if (!isset($parameters[$this->rightOperand->getName()])) {
109+
if ($this->rightOperand->isDiscardedOnNull() && !isset($parameters[$this->rightOperand->getName()])) {
110110
$bypass = true;
111111
}
112112
}

0 commit comments

Comments
 (0)