Skip to content

Commit a1506ee

Browse files
committed
Working on Twig support
1 parent ddcff9d commit a1506ee

File tree

8 files changed

+150
-42
lines changed

8 files changed

+150
-42
lines changed

README.md

Lines changed: 29 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,34 @@ $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+
109138
Is it a MySQL only tool?
110139
------------------------
111140

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@
1616
<directory>./tests/</directory>
1717
</testsuite>
1818
</testsuites>
19+
20+
<filter>
21+
<whitelist processUncoveredFilesFromWhitelist="true">
22+
<directory suffix=".php">src/</directory>
23+
</whitelist>
24+
</filter>
1925
</phpunit>

src/Mouf/Database/MagicQuery.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ class MagicQuery
2727
private $connection;
2828
private $cache;
2929
private $schemaAnalyzer;
30+
/**
31+
* @var \Twig_Environment
32+
*/
3033
private $twigEnvironment;
34+
private $enableTwig = false;
3135

3236
/**
3337
* @param \Doctrine\DBAL\Connection $connection
@@ -45,9 +49,17 @@ public function __construct($connection = null, $cache = null, SchemaAnalyzer $s
4549
if ($schemaAnalyzer) {
4650
$this->schemaAnalyzer = $schemaAnalyzer;
4751
}
48-
if ($this->connection) {
49-
$this->twigEnvironment = SqlTwigEnvironmentFactory::getTwigEnvironment($this->connection);
50-
}
52+
}
53+
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 = true;
62+
return $this;
5163
}
5264

5365
/**
@@ -63,6 +75,9 @@ public function __construct($connection = null, $cache = null, SchemaAnalyzer $s
6375
*/
6476
public function build($sql, array $parameters = array())
6577
{
78+
if ($this->enableTwig) {
79+
$sql = $this->getTwigEnvironment()->render($sql, $parameters);
80+
}
6681
$select = $this->parse($sql);
6782
return $this->toSql($select, $parameters);
6883
}
@@ -223,4 +238,14 @@ private function getSchemaAnalyzer() {
223238
private function getConnectionUniqueId() {
224239
return hash('md4', $this->connection->getHost()."-".$this->connection->getPort()."-".$this->connection->getDatabase()."-".$this->connection->getDriver()->getName());
225240
}
241+
242+
/**
243+
* @return \Twig_Environment
244+
*/
245+
private function getTwigEnvironment() {
246+
if ($this->twigEnvironment === null) {
247+
$this->twigEnvironment = SqlTwigEnvironmentFactory::getTwigEnvironment($this->connection);
248+
}
249+
return $this->twigEnvironment;
250+
}
226251
}
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+
}

src/Mouf/Database/MagicQuery/Twig/SqlTwigEnvironmentFactory.php

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,11 @@ public static function getTwigEnvironment(Connection $connection = null) {
1919

2020
$twig = new \Twig_Environment($stringLoader, $options);
2121

22-
if ($connection !== null) {
23-
24-
$twig->getExtension('core')->setEscaper('sql', function(\Twig_Environment $env, $string, $charset) use ($connection) {
25-
var_dump($string);
26-
// ARGH! Escapers seems to be not called if $string is empty!
27-
if ($string === null) {
28-
return "null";
29-
} else {
30-
return $connection->quote($string);
31-
}
32-
});
33-
34-
// SQL identifier (table or column names....)
35-
$twig->getExtension('core')->setEscaper('sqli', function(\Twig_Environment $env, $string, $charset) use ($connection) {
36-
return $connection->quoteIdentifier($string);
37-
});
38-
39-
} else {
40-
$twig->getExtension('core')->setEscaper('sql', function(\Twig_Environment $env, $string, $charset) use ($connection) {
41-
if ($string === null) {
42-
return "null";
43-
} else {
44-
return "'".addslashes($string)."'";
45-
}
46-
});
47-
48-
$twig->getExtension('core')->setEscaper('sqli', function(\Twig_Environment $env, $string, $charset) use ($connection) {
49-
// Note: we don't know how to escape backticks in a column name. In order to avoid injection,
50-
// we remove any backticks.
51-
return "`".str_replace('`', '', $string)."`";
52-
});
53-
}
22+
// Default escaper will throw an exception. This is because we want to use SQL parameters instead of Twig.
23+
// This ahs a number of advantages, especially in terms of caching.
24+
$twig->getExtension('core')->setEscaper('sql', function(\Twig_Environment $env, $string, $charset) use ($connection) {
25+
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 .... %}"');
26+
});
5427

5528
// Default autoescape mode: sql
5629
$twig->addExtension(new \Twig_Extension_Escaper('sql'));
@@ -61,11 +34,14 @@ public static function getTwigEnvironment(Connection $connection = null) {
6134
private static function getCacheDirectory() {
6235
// If we are running on a Unix environment, let's prepend the cache with the user id of the PHP process.
6336
// This way, we can avoid rights conflicts.
37+
38+
// @codeCoverageIgnoreStart
6439
if (function_exists('posix_geteuid')) {
6540
$posixGetuid = '_'.posix_geteuid();
6641
} else {
6742
$posixGetuid = '';
6843
}
44+
// @codeCoverageIgnoreEnd
6945
$cacheDirectory = rtrim(sys_get_temp_dir(), '/\\').'/magicquerysqltwigtemplate'.$posixGetuid.str_replace(":", "", __DIR__);
7046

7147
return $cacheDirectory;

tests/Mouf/Database/MagicQuery/Twig/SqlTwigEnvironmentFactoryTest.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class SqlTwigEnvironmentFactoryTest extends \PHPUnit_Framework_TestCase
1111
{
12-
public function testWithConnection()
12+
public function getTwigWithConnection()
1313
{
1414
$config = new \Doctrine\DBAL\Configuration();
1515
// TODO: put this in conf variable
@@ -19,7 +19,7 @@ public function testWithConnection()
1919
$conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
2020

2121
$twigEnvironment = SqlTwigEnvironmentFactory::getTwigEnvironment($conn);
22-
$this->doTestTwig($twigEnvironment);
22+
return $twigEnvironment;
2323
}
2424

2525
/**
@@ -34,13 +34,22 @@ public function testWithConnection()
3434
$this->doTestTwig($twigEnvironment);
3535
}*/
3636

37+
/**
38+
*
39+
*/
40+
public function testIf() {
3741

38-
private function doTestTwig(\Twig_Environment $twig) {
39-
$result = $twig->render("SELECT * FROM toto WHERE id = {{ id }}", ["id"=>null]);
40-
$this->assertEquals("SELECT * FROM toto WHERE id = null", $result);
42+
$twig = $this->getTwigWithConnection();
43+
$sql = $twig->render("SELECT * FROM toto {% if id %}WHERE id = :id{% endif %}", ["id"=>12]);
44+
$this->assertEquals("SELECT * FROM toto WHERE id = :id", $sql);
45+
}
4146

42-
$result = $twig->render("SELECT * FROM toto WHERE id = {{ id }}", [ "id" => "myid" ]);
43-
$this->assertEquals("SELECT * FROM toto WHERE id = 'myid'", $result);
47+
/**
48+
* @expectedException Twig_Error_Runtime
49+
*/
50+
public function testException() {
4451

52+
$twig = $this->getTwigWithConnection();
53+
$twig->render("SELECT * FROM toto WHERE id = {{ id }}", ["id"=>null]);
4554
}
4655
}

tests/Mouf/Database/MagicQueryTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,19 @@ public function testMisconfiguration() {
105105

106106
}
107107

108+
/**
109+
*
110+
*/
111+
public function testTwig() {
112+
$magicQuery = new MagicQuery();
113+
$magicQuery->setEnableTwig(true);
114+
115+
$sql = "SELECT * FROM toto {% if id %}WHERE status = 'on'{% endif %}";
116+
$this->assertEquals("SELECT * FROM toto WHERE status = 'on'", $this->simplifySql($magicQuery->build($sql, ["id"=>12])));
117+
$this->assertEquals("SELECT * FROM toto", $this->simplifySql($magicQuery->build($sql, ['id'=>null])));
118+
}
119+
120+
108121
/**
109122
* Removes all artifacts.
110123
*/

0 commit comments

Comments
 (0)