|
2 | 2 |
|
3 | 3 | namespace Mouf\Database;
|
4 | 4 |
|
| 5 | +use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer; |
| 6 | +use SQLParser\Node\ColRef; |
| 7 | +use SQLParser\Node\Equal; |
| 8 | +use SQLParser\Node\NodeInterface; |
| 9 | +use SQLParser\Node\Table; |
| 10 | +use SQLParser\Node\Traverser\DetectMagicJoinSelectVisitor; |
| 11 | +use SQLParser\Node\Traverser\DetectTablesVisitor; |
| 12 | +use SQLParser\Node\Traverser\MagicJoinSelect; |
| 13 | +use SQLParser\Node\Traverser\NodeTraverser; |
| 14 | +use SQLParser\Query\Select; |
5 | 15 | use SQLParser\Query\StatementFactory;
|
6 | 16 | use SQLParser\SQLParser;
|
7 | 17 | use SQLParser\SqlRenderInterface;
|
8 | 18 |
|
9 | 19 | /**
|
10 | 20 | * The class MagicQuery offers special SQL voodoo methods to automatically strip down unused parameters
|
11 |
| - * from parameterized SQL statements. |
| 21 | + * from parametrized SQL statements. |
12 | 22 | */
|
13 | 23 | class MagicQuery
|
14 | 24 | {
|
15 | 25 | private $connection;
|
| 26 | + private $cache; |
| 27 | + private $schemaAnalyzer; |
16 | 28 |
|
17 | 29 | /**
|
18 | 30 | * @param \Doctrine\DBAL\Connection $connection
|
| 31 | + * @param \Doctrine\Common\Cache\Cache $cache |
| 32 | + * @param SchemaAnalyzer $schemaAnalyzer (optional). If not set, it is initialized from the connection. |
19 | 33 | */
|
20 |
| - public function __construct($connection = null) |
| 34 | + public function __construct($connection = null, $cache = null, SchemaAnalyzer $schemaAnalyzer = null) |
21 | 35 | {
|
22 | 36 | $this->connection = $connection;
|
| 37 | + $this->cache = $cache; |
| 38 | + if ($schemaAnalyzer) { |
| 39 | + $this->schemaAnalyzer = $schemaAnalyzer; |
| 40 | + } |
23 | 41 | }
|
24 | 42 |
|
25 | 43 | /**
|
26 | 44 | * Returns merged SQL from $sql and $parameters. Any parameters not available will be striped down
|
27 | 45 | * from the SQL.
|
28 | 46 | *
|
| 47 | + * This is equivalent to calling `parse` and `toSql` successively. |
| 48 | + * |
29 | 49 | * @param string $sql
|
30 | 50 | * @param array $parameters
|
31 | 51 | *
|
32 | 52 | * @return string
|
33 | 53 | */
|
34 | 54 | public function build($sql, array $parameters = array())
|
35 | 55 | {
|
36 |
| - $parser = new SQLParser(); |
37 |
| - $parsed = $parser->parse($sql); |
| 56 | + $select = $this->parse($sql); |
| 57 | + return $this->toSql($select, $parameters); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Parses the $sql passed in parameter and returns a tree representation of it. |
| 62 | + * This tree representation can be used to manipulate the SQL. |
| 63 | + * |
| 64 | + * @param string $sql |
| 65 | + * @return NodeInterface |
| 66 | + * @throws MagicQueryMissingConnectionException |
| 67 | + * @throws MagicQueryParserException |
| 68 | + */ |
| 69 | + public function parse($sql) { |
| 70 | + $select = false; |
| 71 | + |
| 72 | + if ($this->cache !== null) { |
| 73 | + // We choose md4 because it is fast. |
| 74 | + $cacheKey = "request_".hash("md4", $sql); |
| 75 | + $select = $this->cache->fetch($cacheKey); |
| 76 | + } |
| 77 | + |
| 78 | + if ($select === false) { |
| 79 | + $parser = new SQLParser(); |
| 80 | + $parsed = $parser->parse($sql); |
| 81 | + |
| 82 | + if ($parsed == false) { |
| 83 | + throw new MagicQueryParserException('Unable to parse query "'.$sql.'"'); |
| 84 | + } |
| 85 | + |
| 86 | + $select = StatementFactory::toObject($parsed); |
38 | 87 |
|
39 |
| - if ($parsed == false) { |
40 |
| - throw new MagicQueryParserException('Unable to parse query "'.$sql.'"'); |
| 88 | + $this->magicJoin($select); |
| 89 | + |
| 90 | + if ($this->cache !== null) { |
| 91 | + // Let's store the tree |
| 92 | + $this->cache->save($cacheKey, $select); |
| 93 | + } |
41 | 94 | }
|
| 95 | + return $select; |
| 96 | + } |
42 | 97 |
|
43 |
| - $select = StatementFactory::toObject($parsed); |
| 98 | + /** |
| 99 | + * Transforms back a tree of SQL node into a SQL string. |
| 100 | + * |
| 101 | + * @param NodeInterface $sqlNode |
| 102 | + * @param array $parameters |
| 103 | + * @return string |
| 104 | + */ |
| 105 | + public function toSql(NodeInterface $sqlNode, array $parameters = array()) { |
| 106 | + return $sqlNode->toSql($parameters, $this->connection, 0, SqlRenderInterface::CONDITION_GUESS); |
| 107 | + } |
44 | 108 |
|
45 |
| - $sql = $select->toSql($parameters, $this->connection, 0, SqlRenderInterface::CONDITION_GUESS); |
| 109 | + /** |
| 110 | + * Scans the SQL statement and replaces the "magicjoin" part with the correct joins. |
| 111 | + * |
| 112 | + * @param NodeInterface $select |
| 113 | + * @throws MagicQueryMissingConnectionException |
| 114 | + */ |
| 115 | + private function magicJoin(NodeInterface $select) { |
| 116 | + // Let's find if this is a MagicJoin query. |
| 117 | + $magicJoinDetector = new DetectMagicJoinSelectVisitor(); |
| 118 | + $nodeTraverser = new NodeTraverser(); |
| 119 | + $nodeTraverser->addVisitor($magicJoinDetector); |
46 | 120 |
|
47 |
| - return $sql; |
| 121 | + $nodeTraverser->walk($select); |
| 122 | + |
| 123 | + $magicJoinSelects = $magicJoinDetector->getMagicJoinSelects(); |
| 124 | + if ($magicJoinSelects) { |
| 125 | + foreach ($magicJoinSelects as $magicJoinSelect) { |
| 126 | + // For each select in the query (there can be nested selects!), let's find the list of tables. |
| 127 | + $this->magicJoinOnOneQuery($magicJoinSelect); |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * For one given MagicJoin select, let's apply MagicJoin |
| 134 | + * @param MagicJoinSelect $magicJoinSelect |
| 135 | + * @return Select |
| 136 | + */ |
| 137 | + private function magicJoinOnOneQuery(MagicJoinSelect $magicJoinSelect) { |
| 138 | + $tableSearchNodeTraverser = new NodeTraverser(); |
| 139 | + $detectTableVisitor = new DetectTablesVisitor(); |
| 140 | + $tableSearchNodeTraverser->addVisitor($detectTableVisitor); |
| 141 | + |
| 142 | + $select = $magicJoinSelect->getSelect(); |
| 143 | + |
| 144 | + $tableSearchNodeTraverser->walk($select); |
| 145 | + $tables = $detectTableVisitor->getTables(); |
| 146 | + |
| 147 | + $mainTable = $magicJoinSelect->getMainTable(); |
| 148 | + // Let's remove the main table from the list of tables to be linked: |
| 149 | + unset($tables[$mainTable]); |
| 150 | + |
| 151 | + $foreignKeysSet = new \SplObjectStorage(); |
| 152 | + $completePath = []; |
| 153 | + |
| 154 | + foreach ($tables as $table) { |
| 155 | + $path = $this->getSchemaAnalyzer()->getShortestPath($mainTable, $table); |
| 156 | + foreach ($path as $foreignKey) { |
| 157 | + // If the foreign key is not already in our complete path, let's add it. |
| 158 | + if (!$foreignKeysSet->contains($foreignKey)) { |
| 159 | + $completePath[] = $foreignKey; |
| 160 | + $foreignKeysSet->attach($foreignKey); |
| 161 | + } |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + // At this point, we have a complete path, we now just have to rewrite the FROM section. |
| 166 | + $tableNode = new Table(); |
| 167 | + $tableNode->setTable($mainTable); |
| 168 | + $tables = [ |
| 169 | + $tableNode |
| 170 | + ]; |
| 171 | + $currentTable = $mainTable; |
| 172 | + |
| 173 | + foreach ($completePath as $foreignKey) { |
| 174 | + /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */ |
| 175 | + |
| 176 | + $onNode = new Equal(); |
| 177 | + $leftCol = new ColRef(); |
| 178 | + $leftCol->setTable($foreignKey->getLocalTableName()); |
| 179 | + $leftCol->setColumn($foreignKey->getLocalColumns()[0]); |
| 180 | + |
| 181 | + $rightCol = new ColRef(); |
| 182 | + $rightCol->setTable($foreignKey->getForeignTableName()); |
| 183 | + $rightCol->setColumn($foreignKey->getForeignColumns()[0]); |
| 184 | + |
| 185 | + $onNode->setLeftOperand($leftCol); |
| 186 | + $onNode->setRightOperand($rightCol); |
| 187 | + |
| 188 | + $tableNode = new Table(); |
| 189 | + $tableNode->setJoinType("LEFT JOIN"); |
| 190 | + $tableNode->setRefClause($onNode); |
| 191 | + |
| 192 | + if ($foreignKey->getLocalTableName() == $currentTable) { |
| 193 | + $tableNode->setTable($foreignKey->getForeignTableName()); |
| 194 | + $currentTable = $foreignKey->getForeignTableName(); |
| 195 | + } else { |
| 196 | + $tableNode->setTable($foreignKey->getLocalTableName()); |
| 197 | + $currentTable = $foreignKey->getLocalTableName(); |
| 198 | + } |
| 199 | + |
| 200 | + $tables[] = $tableNode; |
| 201 | + } |
| 202 | + |
| 203 | + $select->setFrom($tables); |
| 204 | + |
| 205 | + } |
| 206 | + |
| 207 | + /** |
| 208 | + * @return SchemaAnalyzer |
| 209 | + */ |
| 210 | + private function getSchemaAnalyzer() { |
| 211 | + if ($this->schemaAnalyzer === null) { |
| 212 | + if (!$this->schemaAnalyzer) { |
| 213 | + throw new MagicQueryMissingConnectionException('In order to use MagicJoin, you need to configure a DBAL connection.'); |
| 214 | + } |
| 215 | + |
| 216 | + $this->schemaAnalyzer = new SchemaAnalyzer($this->connection->getSchemaManager()->createSchema()); |
| 217 | + } |
| 218 | + return $this->schemaAnalyzer; |
48 | 219 | }
|
49 | 220 | }
|
0 commit comments