From a430d22cf47f5a8cf83f7eb499f2a8a6a6de6084 Mon Sep 17 00:00:00 2001 From: zYne Date: Tue, 15 May 2007 10:07:05 +0000 Subject: [PATCH] --- draft/new-core/Query.php | 824 +++++++++++++++++++++++++++++++++- lib/Doctrine.php | 4 +- lib/Doctrine/Query/From.php | 167 +------ lib/Doctrine/Query/Parser.php | 384 ---------------- lib/Doctrine/Query/Select.php | 236 +--------- lib/Doctrine/Query/Where.php | 6 +- 6 files changed, 830 insertions(+), 791 deletions(-) diff --git a/draft/new-core/Query.php b/draft/new-core/Query.php index ad3d07189..c870d5e1d 100644 --- a/draft/new-core/Query.php +++ b/draft/new-core/Query.php @@ -44,7 +44,7 @@ class Doctrine_Query2 extends Doctrine_Hydrate2 implements Countable * @param boolean $limitSubqueryUsed */ private $limitSubqueryUsed = false; - + protected $_status = array('needsSubquery' => true); /** @@ -69,9 +69,15 @@ class Doctrine_Query2 extends Doctrine_Hydrate2 implements Countable * @var boolean $subqueriesProcessed Whether or not pending subqueries have already been processed. * Consequent calls to getQuery would result badly constructed queries * without this variable + * + * Since subqueries can be correlated, they can only be processed when + * the main query is fully constructed */ private $subqueriesProcessed = false; - + /** + * @var array $_parsers an array of parser objects + */ + protected $_parsers = array(); /** @@ -129,6 +135,818 @@ class Doctrine_Query2 extends Doctrine_Hydrate2 implements Countable return $this->isDistinct; } + /** + * getParser + * parser lazy-loader + * + * @throws Doctrine_Query_Exception if unknown parser name given + * @return Doctrine_Query_Part + */ + public function getParser($name) + { + if ( ! isset($this->_parsers[$name])) { + $class = 'Doctrine_Query_' . ucwords(strtolower($name)); + + Doctrine::autoload($class); + + if ( ! class_exists($class)) { + throw new Doctrine_Query_Exception('Unknown parser ' . $name); + } + + $this->_parsers[$name] = new $class($this); + } + + return $this->_parsers[$name]; + } + /** + * processPendingFields + * the fields in SELECT clause cannot be parsed until the components + * in FROM clause are parsed, hence this method is called everytime a + * specific component is being parsed. + * + * @throws Doctrine_Query_Exception if unknown component alias has been given + * @param string $componentAlias the alias of the component + * @return void + */ + public function processPendingFields($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + $table = $this->_aliasMap[$componentAlias]['table']; + + if (isset($this->pendingFields[$componentAlias])) { + $fields = $this->pendingFields[$componentAlias]; + + // check for wildcards + if (in_array('*', $fields)) { + $fields = $table->getColumnNames(); + } else { + // only auto-add the primary key fields if this query object is not + // a subquery of another query object + if ( ! $this->isSubquery) { + $fields = array_unique(array_merge($table->getPrimaryKeys(), $fields)); + } + } + } + foreach ($fields as $name) { + $name = $table->getColumnName($name); + + $this->parts['select'][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; + } + + $this->neededTables[] = $tableAlias; + + } + /** + * parseSelect + * parses the query select part and + * adds selected fields to pendingFields array + * + * @param string $dql + */ + public function parseSelect($dql) + { + $refs = Doctrine_Query::bracketExplode($dql, ','); + + foreach ($refs as $reference) { + if (strpos($reference, '(') !== false) { + if (substr($reference, 0, 1) === '(') { + // subselect found in SELECT part + $this->parseSubselect($reference); + } else { + $this->parseAggregateFunction2($reference); + } + } else { + + $e = explode('.', $reference); + if (count($e) > 2) { + $this->pendingFields[] = $reference; + } else { + $this->pendingFields[$e[0]][] = $e[1]; + } + } + } + } + /** + * parseSubselect + * + * parses the subquery found in DQL SELECT part and adds the + * parsed form into $pendingSubqueries stack + * + * @param string $reference + * @return void + */ + public function parseSubselect($reference) + { + $e = Doctrine_Query::bracketExplode($reference, ' '); + $alias = $e[1]; + + if (count($e) > 2) { + if (strtoupper($e[1]) !== 'AS') { + throw new Doctrine_Query_Exception('Syntax error near: ' . $reference); + } + $alias = $e[2]; + } + + $subquery = substr($e[0], 1, -1); + + $this->pendingSubqueries[] = array($subquery, $alias); + } + public function parseAggregateFunction2($func) + { + $e = Doctrine_Query::bracketExplode($func, ' '); + $func = $e[0]; + + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + + try { + $argStr = substr($func, ($pos + 1), -1); + $args = explode(',', $argStr); + + $func = call_user_func_array(array($this->conn->expression, $name), $args); + + if(substr($func, 0, 1) !== '(') { + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + } else { + $name = $func; + } + + $e2 = explode(' ', $args[0]); + + $distinct = ''; + if (count($e2) > 1) { + if (strtoupper($e2[0]) == 'DISTINCT') { + $distinct = 'DISTINCT '; + } + + $args[0] = $e2[1]; + } + + + + $parts = explode('.', $args[0]); + $owner = $parts[0]; + $alias = (isset($e[1])) ? $e[1] : $name; + + $e3 = explode('.', $alias); + + if (count($e3) > 1) { + $alias = $e3[1]; + $owner = $e3[0]; + } + + // a function without parameters eg. RANDOM() + if ($owner === '') { + $owner = 0; + } + + $this->pendingAggregates[$owner][] = array($name, $args, $distinct, $alias); + } catch(Doctrine_Expression_Exception $e) { + throw new Doctrine_Query_Exception('Unknown function ' . $func . '.'); + } + } + public function processPendingSubqueries() + { + if ($this->subqueriesProcessed === true) { + return false; + } + + foreach ($this->pendingSubqueries as $value) { + list($dql, $alias) = $value; + + $sql = $this->createSubquery()->parseQuery($dql, false)->getQuery(); + + reset($this->tableAliases); + + $tableAlias = current($this->tableAliases); + + reset($this->compAliases); + + $componentAlias = key($this->compAliases); + + $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); + + $this->parts['select'][] = '(' . $sql . ') AS ' . $sqlAlias; + + $this->aggregateMap[$alias] = $sqlAlias; + $this->subqueryAggregates[$componentAlias][] = $alias; + } + $this->subqueriesProcessed = true; + + return true; + } + public function processPendingAggregates($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + + if ( ! isset($this->tables[$tableAlias])) { + throw new Doctrine_Query_Exception('Unknown component path ' . $componentAlias); + } + + $root = current($this->tables); + $table = $this->tables[$tableAlias]; + $aggregates = array(); + + if(isset($this->pendingAggregates[$componentAlias])) { + $aggregates = $this->pendingAggregates[$componentAlias]; + } + + if ($root === $table) { + if (isset($this->pendingAggregates[0])) { + $aggregates += $this->pendingAggregates[0]; + } + } + + foreach($aggregates as $parts) { + list($name, $args, $distinct, $alias) = $parts; + + $arglist = array(); + foreach($args as $arg) { + $e = explode('.', $arg); + + + if (is_numeric($arg)) { + $arglist[] = $arg; + } elseif (count($e) > 1) { + //$tableAlias = $this->getTableAlias($e[0]); + $table = $this->tables[$tableAlias]; + + $e[1] = $table->getColumnName($e[1]); + + if( ! $table->hasColumn($e[1])) { + throw new Doctrine_Query_Exception('Unknown column ' . $e[1]); + } + + $arglist[] = $tableAlias . '.' . $e[1]; + } else { + $arglist[] = $e[0]; + } + } + + $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); + + if(substr($name, 0, 1) !== '(') { + $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $sqlAlias; + } else { + $this->parts['select'][] = $name . ' AS ' . $sqlAlias; + } + $this->aggregateMap[$alias] = $sqlAlias; + $this->neededTables[] = $tableAlias; + } + } + /** + * getQueryBase + * returns the base of the generated sql query + * On mysql driver special strategy has to be used for DELETE statements + * + * @return string the base of the generated sql query + */ + public function getQueryBase() + { + switch ($this->type) { + case self::DELETE: + $q = 'DELETE FROM '; + break; + case self::UPDATE: + $q = 'UPDATE '; + break; + case self::SELECT: + $distinct = ($this->isDistinct()) ? 'DISTINCT ' : ''; + + $q = 'SELECT ' . $distinct . implode(', ', $this->parts['select']) . ' FROM '; + break; + } + return $q; + } + /** + * buildFromPart + * + * @return string + */ + public function buildFromPart() + { + $q = ''; + foreach ($this->parts['from'] as $k => $part) { + if ($k === 0) { + $q .= $part; + continue; + } + // preserve LEFT JOINs only if needed + + if (substr($part, 0, 9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + $aliases = array_merge($this->subqueryAliases, + array_keys($this->neededTables)); + + if( ! in_array($e[3], $aliases) && + ! in_array($e[2], $aliases) && + + ! empty($this->pendingFields)) { + continue; + } + + } + + $e = explode(' ON ', $part); + + // we can always be sure that the first join condition exists + $e2 = explode(' AND ', $e[1]); + + $part = $e[0] . ' ON ' . array_shift($e2); + + if ( ! empty($e2)) { + $parser = new Doctrine_Query_JoinCondition($this); + $part .= ' AND ' . $parser->parse(implode(' AND ', $e2)); + } + + $q .= ' ' . $part; + } + return $q; + } + /** + * builds the sql query from the given parameters and applies things such as + * column aggregation inheritance and limit subqueries if needed + * + * @param array $params an array of prepared statement params (needed only in mysql driver + * when limit subquery algorithm is used) + * @return string the built sql query + */ + public function getQuery($params = array()) + { + if (empty($this->parts['select']) || empty($this->parts['from'])) { + return false; + } + + $needsSubQuery = false; + $subquery = ''; + $k = array_keys($this->_aliasMap); + $table = $this->_aliasMap[$k[0]]['table']; + + if( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) { + $needsSubQuery = true; + $this->limitSubqueryUsed = true; + } + + // process all pending SELECT part subqueries + $this->processPendingSubqueries(); + + // build the basic query + + $str = ''; + if($this->isDistinct()) { + $str = 'DISTINCT '; + } + + $q = $this->getQueryBase(); + $q .= $this->buildFromPart(); + + if ( ! empty($this->parts['set'])) { + $q .= ' SET ' . implode(', ', $this->parts['set']); + } + + $string = $this->applyInheritance(); + + if ( ! empty($string)) { + $this->parts['where'][] = '(' . $string . ')'; + } + + + $modifyLimit = true; + if ( ! empty($this->parts["limit"]) || ! empty($this->parts["offset"])) { + + if($needsSubQuery) { + $subquery = $this->getLimitSubquery(); + + + switch(strtolower($this->conn->getName())) { + case 'mysql': + // mysql doesn't support LIMIT in subqueries + $list = $this->conn->execute($subquery, $params)->fetchAll(PDO::FETCH_COLUMN); + $subquery = implode(', ', $list); + break; + case 'pgsql': + // pgsql needs special nested LIMIT subquery + $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias'; + break; + } + + $field = $this->aliasHandler->getShortAlias($table->getTableName()) . '.' . $table->getIdentifier(); + + // only append the subquery if it actually contains something + if($subquery !== '') { + array_unshift($this->parts['where'], $field. ' IN (' . $subquery . ')'); + } + + $modifyLimit = false; + } + } + + $q .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']):''; + $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']):''; + $q .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']):''; + $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']):''; + + if ($modifyLimit) { + $q = $this->conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']); + } + + // return to the previous state + if ( ! empty($string)) { + array_pop($this->parts['where']); + } + if ($needsSubQuery) { + array_shift($this->parts['where']); + } + return $q; + } + /** + * getLimitSubquery + * this is method is used by the record limit algorithm + * + * when fetching one-to-many, many-to-many associated data with LIMIT clause + * an additional subquery is needed for limiting the number of returned records instead + * of limiting the number of sql result set rows + * + * @return string the limit subquery + */ + public function getLimitSubquery() + { + $k = array_keys($this->tables); + $table = $this->tables[$k[0]]; + + // get short alias + $alias = $this->aliasHandler->getShortAlias($table->getTableName()); + $primaryKey = $alias . '.' . $table->getIdentifier(); + + // initialize the base of the subquery + $subquery = 'SELECT DISTINCT ' . $primaryKey; + + if ($this->conn->getDBH()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { + // pgsql needs the order by fields to be preserved in select clause + + foreach ($this->parts['orderby'] as $part) { + $e = explode(' ', $part); + + // don't add primarykey column (its already in the select clause) + if ($e[0] !== $primaryKey) { + $subquery .= ', ' . $e[0]; + } + } + } + + $subquery .= ' FROM ' . $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $alias; + + foreach ($this->parts['join'] as $parts) { + foreach ($parts as $part) { + // preserve LEFT JOINs only if needed + if (substr($part,0,9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + if ( ! in_array($e[3], $this->subqueryAliases) && + ! in_array($e[2], $this->subqueryAliases)) { + continue; + } + + } + + $subquery .= ' ' . $part; + } + } + + // all conditions must be preserved in subquery + $subquery .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']) : ''; + $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']) : ''; + $subquery .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']) : ''; + $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']) : ''; + + // add driver specific limit clause + $subquery = $this->conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']); + + $parts = Doctrine_Tokenizer::quoteExplode($subquery, ' ', "'", "'"); + + foreach($parts as $k => $part) { + if(strpos($part, "'") !== false) { + continue; + } + + if($this->aliasHandler->hasAliasFor($part)) { + $parts[$k] = $this->aliasHandler->generateNewAlias($part); + } + + if(strpos($part, '.') !== false) { + $e = explode('.', $part); + + $trimmed = ltrim($e[0], '( '); + $pos = strpos($e[0], $trimmed); + + $e[0] = substr($e[0], 0, $pos) . $this->aliasHandler->generateNewAlias($trimmed); + $parts[$k] = implode('.', $e); + } + } + $subquery = implode(' ', $parts); + + return $subquery; + } + /** + * tokenizeQuery + * splits the given dql query into an array where keys + * represent different query part names and values are + * arrays splitted using sqlExplode method + * + * example: + * + * parameter: + * $query = "SELECT u.* FROM User u WHERE u.name LIKE ?" + * returns: + * array('select' => array('u.*'), + * 'from' => array('User', 'u'), + * 'where' => array('u.name', 'LIKE', '?')) + * + * @param string $query DQL query + * @throws Doctrine_Query_Exception if some generic parsing error occurs + * @return array an array containing the query string parts + */ + public function tokenizeQuery($query) + { + $e = Doctrine_Tokenizer::sqlExplode($query, ' '); + + foreach($e as $k=>$part) { + $part = trim($part); + switch(strtolower($part)) { + case 'delete': + case 'update': + case 'select': + case 'set': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $p = $part; + $parts[$part] = array(); + break; + case 'order': + case 'group': + $i = ($k + 1); + if(isset($e[$i]) && strtolower($e[$i]) === "by") { + $p = $part; + $parts[$part] = array(); + } else + $parts[$p][] = $part; + break; + case "by": + continue; + default: + if( ! isset($p)) + throw new Doctrine_Query_Exception("Couldn't parse query."); + + $parts[$p][] = $part; + } + } + return $parts; + } + /** + * DQL PARSER + * parses a DQL query + * first splits the query in parts and then uses individual + * parsers for each part + * + * @param string $query DQL query + * @param boolean $clear whether or not to clear the aliases + * @throws Doctrine_Query_Exception if some generic parsing error occurs + * @return Doctrine_Query + */ + public function parseQuery($query, $clear = true) + { + if ($clear) { + $this->clear(); + } + + $query = trim($query); + $query = str_replace("\n", ' ', $query); + $query = str_replace("\r", ' ', $query); + + $parts = $this->tokenizeQuery($query); + + foreach($parts as $k => $part) { + $part = implode(' ', $part); + switch(strtolower($k)) { + case 'create': + $this->type = self::CREATE; + break; + case 'insert': + $this->type = self::INSERT; + break; + case 'delete': + $this->type = self::DELETE; + break; + case 'select': + $this->type = self::SELECT; + $this->parseSelect($part); + break; + case 'update': + $this->type = self::UPDATE; + $k = 'FROM'; + + case 'from': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + $parser->parse($part); + break; + case 'set': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + $this->parts['set'][] = $parser->parse($part); + break; + case 'group': + case 'order': + $k .= 'by'; + case 'where': + case 'having': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + + $name = strtolower($k); + $this->parts[$name][] = $parser->parse($part); + break; + case 'limit': + $this->parts['limit'] = trim($part); + break; + case 'offset': + $this->parts['offset'] = trim($part); + break; + } + } + + return $this; + } + public function load($path, $loadFields = true) + { + // parse custom join conditions + $e = explode(' ON ', $path); + + $joinCondition = ''; + + if (count($e) > 1) { + $joinCondition = ' AND ' . $e[1]; + $path = $e[0]; + } + + $tmp = explode(' ', $path); + $componentAlias = (count($tmp) > 1) ? end($tmp) : false; + + $e = preg_split("/[.:]/", $tmp[0], -1); + + $fullPath = $tmp[0]; + $prevPath = ''; + $fullLength = strlen($fullPath); + + if (isset($this->_aliasMap[$e[0]])) { + $table = $this->_aliasMap[$e[0]]['table']; + + $prevPath = $parent = array_shift($e); + } + + foreach ($e as $key => $name) { + // get length of the previous path + $length = strlen($prevPath); + + // build the current component path + $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; + + $delimeter = substr($fullPath, $length, 1); + + // if an alias is not given use the current path as an alias identifier + if (strlen($prevPath) !== $fullLength || ! $componentAlias) { + $componentAlias = $prevPath; + } + + if ( ! isset($table)) { + // process the root of the path + $table = $this->loadRoot($name, $componentAlias); + } else { + + + $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; + + $relation = $table->getRelation($name); + $this->_aliasMap[$componentAlias] = array('table' => $relation->getTable(), + 'parent' => $parent, + 'relation' => $relation); + if( ! $relation->isOneToOne()) { + $this->needsSubquery = true; + } + + $localAlias = $this->getShortAlias($parent, $table->getTableName()); + $foreignAlias = $this->getShortAlias($componentAlias, $relation->getTable()->getTableName()); + $localSql = $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $localAlias; + $foreignSql = $this->conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $foreignAlias; + + $map = $relation->getTable()->inheritanceMap; + + if ( ! $loadFields || ! empty($map) || $joinCondition) { + $this->subqueryAliases[] = $foreignAlias; + } + + if ($relation instanceof Doctrine_Relation_Association) { + $asf = $relation->getAssociationFactory(); + + $assocTableName = $asf->getTableName(); + + if( ! $loadFields || ! empty($map) || $joinCondition) { + $this->subqueryAliases[] = $assocTableName; + } + + $assocPath = $prevPath . '.' . $asf->getComponentName(); + + $assocAlias = $this->getShortAlias($assocPath, $asf->getTableName()); + + $queryPart = $join . $assocTableName . ' ' . $assocAlias . ' ON ' . $localAlias . '.' + . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getLocal(); + + if ($relation instanceof Doctrine_Relation_Association_Self) { + $queryPart .= ' OR ' . $localAlias . '.' . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getForeign(); + } + + $this->parts['from'][] = $queryPart; + + $queryPart = $join . $foreignSql . ' ON ' . $foreignAlias . '.' + . $relation->getTable()->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getForeign() + . $joinCondition; + + if ($relation instanceof Doctrine_Relation_Association_Self) { + $queryPart .= ' OR ' . $foreignTable . '.' . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getLocal(); + } + + } else { + $queryPart = $join . $foreignSql + . ' ON ' . $localAlias . '.' + . $relation->getLocal() . ' = ' . $foreignAlias . '.' . $relation->getForeign() + . $joinCondition; + } + $this->parts['from'][] = $queryPart; + } + if ($loadFields) { + + $restoreState = false; + // load fields if necessary + if ($loadFields && empty($this->pendingFields)) { + $this->pendingFields[$componentAlias] = array('*'); + + $restoreState = true; + } + + if(isset($this->pendingFields[$componentAlias])) { + $this->processPendingFields($componentAlias); + } + + if ($restoreState) { + $this->pendingFields = array(); + } + + + if(isset($this->pendingAggregates[$componentAlias]) || isset($this->pendingAggregates[0])) { + $this->processPendingAggregates($componentAlias); + } + } + } + } + /** + * loadRoot + * + * @param string $name + * @param string $componentAlias + */ + public function loadRoot($name, $componentAlias) + { + // get the connection for the component + $this->conn = Doctrine_Manager::getInstance() + ->getConnectionForComponent($name); + + $table = $this->conn->getTable($name); + $tableName = $table->getTableName(); + + // get the short alias for this table + $tableAlias = $this->aliasHandler->getShortAlias($componentAlias, $tableName); + // quote table name + $queryPart = $this->conn->quoteIdentifier($tableName); + + if ($this->type === self::SELECT) { + $queryPart .= ' ' . $tableAlias; + } + + $this->parts['from'][] = $queryPart; + $this->tableAliases[$tableAlias] = $componentAlias; + $this->_aliasMap[$componentAlias] = array('table' => $table); + + return $table; + } /** * count * @@ -283,7 +1101,7 @@ class Doctrine_Query2 extends Doctrine_Hydrate2 implements Countable */ public function select($select) { - return $this->getParser('from')->parse($select); + return $this->getParser('select')->parse($select); } /** * from diff --git a/lib/Doctrine.php b/lib/Doctrine.php index d570116ea..68ef82030 100644 --- a/lib/Doctrine.php +++ b/lib/Doctrine.php @@ -495,9 +495,9 @@ final class Doctrine case 'array': $ret[] = 'Array('; foreach ($var as $k => $v) { - $ret[] = $k . ' : ' . Doctrine::dump($v); + $ret[] = $k . ' : ' . Doctrine::dump($v, false); } - $ret[] = ')'; + $ret[] = ")"; break; case 'object': $ret[] = 'Object(' . get_class($var) . ')'; diff --git a/lib/Doctrine/Query/From.php b/lib/Doctrine/Query/From.php index a4a416145..b7ee4a2c7 100644 --- a/lib/Doctrine/Query/From.php +++ b/lib/Doctrine/Query/From.php @@ -31,8 +31,7 @@ Doctrine::autoload("Doctrine_Query_Part"); * @author Konsta Vesterinen */ class Doctrine_Query_From extends Doctrine_Query_Part -{ - +{ /** * DQL FROM PARSER * parses the from part of the query string @@ -85,169 +84,5 @@ class Doctrine_Query_From extends Doctrine_Query_Part $operator = ($last == 'INNER') ? ':' : '.'; } } - public function load($path, $loadFields = true) - { - // parse custom join conditions - $e = explode(' ON ', $path); - - $joinCondition = ''; - if (count($e) > 1) { - $joinCondition = ' AND ' . $e[1]; - $path = $e[0]; - } - - $tmp = explode(' ', $path); - $componentAlias = (count($tmp) > 1) ? end($tmp) : false; - - $e = preg_split("/[.:]/", $tmp[0], -1); - - $fullPath = $tmp[0]; - $prevPath = ''; - $fullLength = strlen($fullPath); - - if (isset($this->_aliasMap[$e[0]])) { - $table = $this->_aliasMap[$e[0]]['table']; - - $prevPath = $parent = array_shift($e); - } - - foreach ($e as $key => $name) { - // get length of the previous path - $length = strlen($prevPath); - - // build the current component path - $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; - - $delimeter = substr($fullPath, $length, 1); - - // if an alias is not given use the current path as an alias identifier - if (strlen($prevPath) !== $fullLength || ! $componentAlias) { - $componentAlias = $prevPath; - } - - if ( ! isset($table)) { - // process the root of the path - $table = $this->loadRoot($name, $componentAlias); - } else { - - - $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; - - $relation = $table->getRelation($name); - $this->_aliasMap[$componentAlias] = array('table' => $relation->getTable(), - 'parent' => $parent, - 'relation' => $relation); - if( ! $relation->isOneToOne()) { - $this->needsSubquery = true; - } - - $localAlias = $this->getShortAlias($parent, $table->getTableName()); - $foreignAlias = $this->getShortAlias($componentAlias, $relation->getTable()->getTableName()); - $localSql = $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $localAlias; - $foreignSql = $this->conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $foreignAlias; - - $map = $relation->getTable()->inheritanceMap; - - if ( ! $loadFields || ! empty($map) || $joinCondition) { - $this->subqueryAliases[] = $foreignAlias; - } - - if ($relation instanceof Doctrine_Relation_Association) { - $asf = $relation->getAssociationFactory(); - - $assocTableName = $asf->getTableName(); - - if( ! $loadFields || ! empty($map) || $joinCondition) { - $this->subqueryAliases[] = $assocTableName; - } - - $assocPath = $prevPath . '.' . $asf->getComponentName(); - - $assocAlias = $this->getShortAlias($assocPath, $asf->getTableName()); - - $queryPart = $join . $assocTableName . ' ' . $assocAlias . ' ON ' . $localAlias . '.' - . $table->getIdentifier() . ' = ' - . $assocAlias . '.' . $relation->getLocal(); - - if ($relation instanceof Doctrine_Relation_Association_Self) { - $queryPart .= ' OR ' . $localAlias . '.' . $table->getIdentifier() . ' = ' - . $assocAlias . '.' . $relation->getForeign(); - } - - $this->parts['from'][] = $queryPart; - - $queryPart = $join . $foreignSql . ' ON ' . $foreignAlias . '.' - . $relation->getTable()->getIdentifier() . ' = ' - . $assocAlias . '.' . $relation->getForeign() - . $joinCondition; - - if ($relation instanceof Doctrine_Relation_Association_Self) { - $queryPart .= ' OR ' . $foreignTable . '.' . $table->getIdentifier() . ' = ' - . $assocAlias . '.' . $relation->getLocal(); - } - - } else { - $queryPart = $join . $foreignSql - . ' ON ' . $localAlias . '.' - . $relation->getLocal() . ' = ' . $foreignAlias . '.' . $relation->getForeign() - . $joinCondition; - } - $this->parts['from'][] = $queryPart; - } - if ($loadFields) { - - $restoreState = false; - // load fields if necessary - if ($loadFields && empty($this->pendingFields)) { - $this->pendingFields[$componentAlias] = array('*'); - - $restoreState = true; - } - - if(isset($this->pendingFields[$componentAlias])) { - $this->processPendingFields($componentAlias); - } - - if ($restoreState) { - $this->pendingFields = array(); - } - - - if(isset($this->pendingAggregates[$componentAlias]) || isset($this->pendingAggregates[0])) { - $this->processPendingAggregates($componentAlias); - } - } - } - } - /** - * loadRoot - * - * @param string $name - * @param string $componentAlias - */ - public function loadRoot($name, $componentAlias) - { - // get the connection for the component - $this->conn = Doctrine_Manager::getInstance() - ->getConnectionForComponent($name); - - $table = $this->conn->getTable($name); - $tableName = $table->getTableName(); - - // get the short alias for this table - $tableAlias = $this->aliasHandler->getShortAlias($componentAlias, $tableName); - // quote table name - $queryPart = $this->conn->quoteIdentifier($tableName); - - if ($this->type === self::SELECT) { - $queryPart .= ' ' . $tableAlias; - } - - $this->parts['from'][] = $queryPart; - $this->tableAliases[$tableAlias] = $componentAlias; - $this->_aliasMap[$componentAlias] = array('table' => $table); - - return $table; - } } diff --git a/lib/Doctrine/Query/Parser.php b/lib/Doctrine/Query/Parser.php index 5212496ad..4ef893361 100644 --- a/lib/Doctrine/Query/Parser.php +++ b/lib/Doctrine/Query/Parser.php @@ -32,389 +32,5 @@ */ class Doctrine_Query_Parser { - /** - * getQueryBase - * returns the base of the generated sql query - * On mysql driver special strategy has to be used for DELETE statements - * - * @return string the base of the generated sql query - */ - public function getQueryBase() - { - switch ($this->type) { - case self::DELETE: - $q = 'DELETE FROM '; - break; - case self::UPDATE: - $q = 'UPDATE '; - break; - case self::SELECT: - $distinct = ($this->isDistinct()) ? 'DISTINCT ' : ''; - $q = 'SELECT ' . $distinct . implode(', ', $this->parts['select']) . ' FROM '; - break; - } - return $q; - } - /** - * buildFromPart - * - * @return string - */ - public function buildFromPart() - { - foreach ($this->parts['from'] as $k => $part) { - if ($k === 0) { - $q .= $part; - continue; - } - // preserve LEFT JOINs only if needed - - if (substr($part, 0, 9) === 'LEFT JOIN') { - $e = explode(' ', $part); - - $aliases = array_merge($this->subqueryAliases, - array_keys($this->neededTables)); - - if( ! in_array($e[3], $aliases) && - ! in_array($e[2], $aliases) && - - ! empty($this->pendingFields)) { - continue; - } - - } - - $e = explode(' ON ', $part); - - // we can always be sure that the first join condition exists - $e2 = explode(' AND ', $e[1]); - - $part = $e[0] . ' ON ' . array_shift($e2); - - if ( ! empty($e2)) { - $parser = new Doctrine_Query_JoinCondition($this); - $part .= ' AND ' . $parser->parse(implode(' AND ', $e2)); - } - - $q .= ' ' . $part; - } - } - /** - * builds the sql query from the given parameters and applies things such as - * column aggregation inheritance and limit subqueries if needed - * - * @param array $params an array of prepared statement params (needed only in mysql driver - * when limit subquery algorithm is used) - * @return string the built sql query - */ - public function getQuery($params = array()) - { - if (empty($this->parts['select']) || empty($this->parts['from'])) { - return false; - } - - $needsSubQuery = false; - $subquery = ''; - $k = array_keys($this->_aliasMap); - $table = $this->_aliasMap[$k[0]]['table']; - - if( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) { - $needsSubQuery = true; - $this->limitSubqueryUsed = true; - } - - // process all pending SELECT part subqueries - $this->processPendingSubqueries(); - - // build the basic query - - $str = ''; - if($this->isDistinct()) { - $str = 'DISTINCT '; - } - - $q = $this->getQueryBase(); - $q .= $this->buildFrom(); - - if ( ! empty($this->parts['set'])) { - $q .= ' SET ' . implode(', ', $this->parts['set']); - } - - $string = $this->applyInheritance(); - - if ( ! empty($string)) { - $this->parts['where'][] = '(' . $string . ')'; - } - - - $modifyLimit = true; - if ( ! empty($this->parts["limit"]) || ! empty($this->parts["offset"])) { - - if($needsSubQuery) { - $subquery = $this->getLimitSubquery(); - - - switch(strtolower($this->conn->getName())) { - case 'mysql': - // mysql doesn't support LIMIT in subqueries - $list = $this->conn->execute($subquery, $params)->fetchAll(PDO::FETCH_COLUMN); - $subquery = implode(', ', $list); - break; - case 'pgsql': - // pgsql needs special nested LIMIT subquery - $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias'; - break; - } - - $field = $this->aliasHandler->getShortAlias($table->getTableName()) . '.' . $table->getIdentifier(); - - // only append the subquery if it actually contains something - if($subquery !== '') { - array_unshift($this->parts['where'], $field. ' IN (' . $subquery . ')'); - } - - $modifyLimit = false; - } - } - - $q .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']):''; - $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']):''; - $q .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']):''; - $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']):''; - - if ($modifyLimit) { - $q = $this->conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']); - } - - // return to the previous state - if ( ! empty($string)) { - array_pop($this->parts['where']); - } - if ($needsSubQuery) { - array_shift($this->parts['where']); - } - return $q; - } - /** - * getLimitSubquery - * this is method is used by the record limit algorithm - * - * when fetching one-to-many, many-to-many associated data with LIMIT clause - * an additional subquery is needed for limiting the number of returned records instead - * of limiting the number of sql result set rows - * - * @return string the limit subquery - */ - public function getLimitSubquery() - { - $k = array_keys($this->tables); - $table = $this->tables[$k[0]]; - - // get short alias - $alias = $this->aliasHandler->getShortAlias($table->getTableName()); - $primaryKey = $alias . '.' . $table->getIdentifier(); - - // initialize the base of the subquery - $subquery = 'SELECT DISTINCT ' . $primaryKey; - - if ($this->conn->getDBH()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { - // pgsql needs the order by fields to be preserved in select clause - - foreach ($this->parts['orderby'] as $part) { - $e = explode(' ', $part); - - // don't add primarykey column (its already in the select clause) - if ($e[0] !== $primaryKey) { - $subquery .= ', ' . $e[0]; - } - } - } - - $subquery .= ' FROM ' . $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $alias; - - foreach ($this->parts['join'] as $parts) { - foreach ($parts as $part) { - // preserve LEFT JOINs only if needed - if (substr($part,0,9) === 'LEFT JOIN') { - $e = explode(' ', $part); - - if ( ! in_array($e[3], $this->subqueryAliases) && - ! in_array($e[2], $this->subqueryAliases)) { - continue; - } - - } - - $subquery .= ' '.$part; - } - } - - // all conditions must be preserved in subquery - $subquery .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']) : ''; - $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']) : ''; - $subquery .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']) : ''; - $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']) : ''; - - // add driver specific limit clause - $subquery = $this->conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']); - - $parts = self::quoteExplode($subquery, ' ', "'", "'"); - - foreach($parts as $k => $part) { - if(strpos($part, "'") !== false) { - continue; - } - - if($this->aliasHandler->hasAliasFor($part)) { - $parts[$k] = $this->aliasHandler->generateNewAlias($part); - } - - if(strpos($part, '.') !== false) { - $e = explode('.', $part); - - $trimmed = ltrim($e[0], '( '); - $pos = strpos($e[0], $trimmed); - - $e[0] = substr($e[0], 0, $pos) . $this->aliasHandler->generateNewAlias($trimmed); - $parts[$k] = implode('.', $e); - } - } - $subquery = implode(' ', $parts); - - return $subquery; - } - /** - * tokenizeQuery - * splits the given dql query into an array where keys - * represent different query part names and values are - * arrays splitted using sqlExplode method - * - * example: - * - * parameter: - * $query = "SELECT u.* FROM User u WHERE u.name LIKE ?" - * returns: - * array('select' => array('u.*'), - * 'from' => array('User', 'u'), - * 'where' => array('u.name', 'LIKE', '?')) - * - * @param string $query DQL query - * @throws Doctrine_Query_Exception if some generic parsing error occurs - * @return array an array containing the query string parts - */ - public function tokenizeQuery($query) - { - $e = Doctrine_Tokenizer::sqlExplode($query, ' '); - - foreach($e as $k=>$part) { - $part = trim($part); - switch(strtolower($part)) { - case 'delete': - case 'update': - case 'select': - case 'set': - case 'from': - case 'where': - case 'limit': - case 'offset': - case 'having': - $p = $part; - $parts[$part] = array(); - break; - case 'order': - case 'group': - $i = ($k + 1); - if(isset($e[$i]) && strtolower($e[$i]) === "by") { - $p = $part; - $parts[$part] = array(); - } else - $parts[$p][] = $part; - break; - case "by": - continue; - default: - if( ! isset($p)) - throw new Doctrine_Query_Exception("Couldn't parse query."); - - $parts[$p][] = $part; - } - } - return $parts; - } - /** - * DQL PARSER - * parses a DQL query - * first splits the query in parts and then uses individual - * parsers for each part - * - * @param string $query DQL query - * @param boolean $clear whether or not to clear the aliases - * @throws Doctrine_Query_Exception if some generic parsing error occurs - * @return Doctrine_Query - */ - public function parseQuery($query, $clear = true) - { - if ($clear) { - $this->clear(); - } - - $query = trim($query); - $query = str_replace("\n", ' ', $query); - $query = str_replace("\r", ' ', $query); - - $parts = $this->tokenizeQuery($query); - - foreach($parts as $k => $part) { - $part = implode(' ', $part); - switch(strtolower($k)) { - case 'create': - $this->type = self::CREATE; - break; - case 'insert': - $this->type = self::INSERT; - break; - case 'delete': - $this->type = self::DELETE; - break; - case 'select': - $this->type = self::SELECT; - $this->parseSelect($part); - break; - case 'update': - $this->type = self::UPDATE; - $k = 'FROM'; - - case 'from': - $class = 'Doctrine_Query_' . ucwords(strtolower($k)); - $parser = new $class($this); - $parser->parse($part); - break; - case 'set': - $class = 'Doctrine_Query_' . ucwords(strtolower($k)); - $parser = new $class($this); - $this->parts['set'][] = $parser->parse($part); - break; - case 'group': - case 'order': - $k .= 'by'; - case 'where': - case 'having': - $class = 'Doctrine_Query_' . ucwords(strtolower($k)); - $parser = new $class($this); - - $name = strtolower($k); - $this->parts[$name][] = $parser->parse($part); - break; - case 'limit': - $this->parts['limit'] = trim($part); - break; - case 'offset': - $this->parts['offset'] = trim($part); - break; - } - } - - return $this; - } } diff --git a/lib/Doctrine/Query/Select.php b/lib/Doctrine/Query/Select.php index ff228f46a..3f0a15108 100644 --- a/lib/Doctrine/Query/Select.php +++ b/lib/Doctrine/Query/Select.php @@ -32,240 +32,10 @@ Doctrine::autoload("Doctrine_Query_Part"); */ class Doctrine_Query_Select extends Doctrine_Query_Part { - /** - * processPendingFields - * the fields in SELECT clause cannot be parsed until the components - * in FROM clause are parsed, hence this method is called everytime a - * specific component is being parsed. - * - * @throws Doctrine_Query_Exception if unknown component alias has been given - * @param string $componentAlias the alias of the component - * @return void - */ - public function processPendingFields($componentAlias) + public function parse($dql) { - $tableAlias = $this->getTableAlias($componentAlias); - $table = $this->_aliasMap[$componentAlias]['table']; - - if (isset($this->pendingFields[$componentAlias])) { - $fields = $this->pendingFields[$componentAlias]; - - // check for wildcards - if (in_array('*', $fields)) { - $fields = $table->getColumnNames(); - } else { - // only auto-add the primary key fields if this query object is not - // a subquery of another query object - if ( ! $this->isSubquery) { - $fields = array_unique(array_merge($table->getPrimaryKeys(), $fields)); - } - } - } - foreach ($fields as $name) { - $name = $table->getColumnName($name); - - $this->parts['select'][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; - } + $this->query->parseSelect($dql); - $this->neededTables[] = $tableAlias; - - } - /** - * parseSelect - * parses the query select part and - * adds selected fields to pendingFields array - * - * @param string $dql - */ - public function parseSelect($dql) - { - $refs = Doctrine_Query::bracketExplode($dql, ','); - - foreach ($refs as $reference) { - if (strpos($reference, '(') !== false) { - if (substr($reference, 0, 1) === '(') { - // subselect found in SELECT part - $this->parseSubselect($reference); - } else { - $this->parseAggregateFunction2($reference); - } - } else { - - $e = explode('.', $reference); - if (count($e) > 2) { - $this->pendingFields[] = $reference; - } else { - $this->pendingFields[$e[0]][] = $e[1]; - } - } - } - } - /** - * parseSubselect - * - * parses the subquery found in DQL SELECT part and adds the - * parsed form into $pendingSubqueries stack - * - * @param string $reference - * @return void - */ - public function parseSubselect($reference) - { - $e = Doctrine_Query::bracketExplode($reference, ' '); - $alias = $e[1]; - - if (count($e) > 2) { - if (strtoupper($e[1]) !== 'AS') { - throw new Doctrine_Query_Exception('Syntax error near: ' . $reference); - } - $alias = $e[2]; - } - - $subquery = substr($e[0], 1, -1); - - $this->pendingSubqueries[] = array($subquery, $alias); - } - public function parseAggregateFunction2($func) - { - $e = Doctrine_Query::bracketExplode($func, ' '); - $func = $e[0]; - - $pos = strpos($func, '('); - $name = substr($func, 0, $pos); - - try { - $argStr = substr($func, ($pos + 1), -1); - $args = explode(',', $argStr); - - $func = call_user_func_array(array($this->conn->expression, $name), $args); - - if(substr($func, 0, 1) !== '(') { - $pos = strpos($func, '('); - $name = substr($func, 0, $pos); - } else { - $name = $func; - } - - $e2 = explode(' ', $args[0]); - - $distinct = ''; - if(count($e2) > 1) { - if(strtoupper($e2[0]) == 'DISTINCT') - $distinct = 'DISTINCT '; - - $args[0] = $e2[1]; - } - - - - $parts = explode('.', $args[0]); - $owner = $parts[0]; - $alias = (isset($e[1])) ? $e[1] : $name; - - $e3 = explode('.', $alias); - - if(count($e3) > 1) { - $alias = $e3[1]; - $owner = $e3[0]; - } - - // a function without parameters eg. RANDOM() - if ($owner === '') { - $owner = 0; - } - - $this->pendingAggregates[$owner][] = array($name, $args, $distinct, $alias); - } catch(Doctrine_Expression_Exception $e) { - throw new Doctrine_Query_Exception('Unknown function ' . $func . '.'); - } - } - public function processPendingSubqueries() - { - if ($this->subqueriesProcessed === true) { - return false; - } - - foreach ($this->pendingSubqueries as $value) { - list($dql, $alias) = $value; - - $sql = $this->createSubquery()->parseQuery($dql, false)->getQuery(); - - reset($this->tableAliases); - - $tableAlias = current($this->tableAliases); - - reset($this->compAliases); - - $componentAlias = key($this->compAliases); - - $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); - - $this->parts['select'][] = '(' . $sql . ') AS ' . $sqlAlias; - - $this->aggregateMap[$alias] = $sqlAlias; - $this->subqueryAggregates[$componentAlias][] = $alias; - } - $this->subqueriesProcessed = true; - - return true; - } - public function processPendingAggregates($componentAlias) - { - $tableAlias = $this->getTableAlias($componentAlias); - - if ( ! isset($this->tables[$tableAlias])) { - throw new Doctrine_Query_Exception('Unknown component path ' . $componentAlias); - } - - $root = current($this->tables); - $table = $this->tables[$tableAlias]; - $aggregates = array(); - - if(isset($this->pendingAggregates[$componentAlias])) { - $aggregates = $this->pendingAggregates[$componentAlias]; - } - - if ($root === $table) { - if (isset($this->pendingAggregates[0])) { - $aggregates += $this->pendingAggregates[0]; - } - } - - foreach($aggregates as $parts) { - list($name, $args, $distinct, $alias) = $parts; - - $arglist = array(); - foreach($args as $arg) { - $e = explode('.', $arg); - - - if (is_numeric($arg)) { - $arglist[] = $arg; - } elseif (count($e) > 1) { - //$tableAlias = $this->getTableAlias($e[0]); - $table = $this->tables[$tableAlias]; - - $e[1] = $table->getColumnName($e[1]); - - if( ! $table->hasColumn($e[1])) { - throw new Doctrine_Query_Exception('Unknown column ' . $e[1]); - } - - $arglist[] = $tableAlias . '.' . $e[1]; - } else { - $arglist[] = $e[0]; - } - } - - $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); - - if(substr($name, 0, 1) !== '(') { - $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $sqlAlias; - } else { - $this->parts['select'][] = $name . ' AS ' . $sqlAlias; - } - $this->aggregateMap[$alias] = $sqlAlias; - $this->neededTables[] = $tableAlias; - } + return $this->query; } } diff --git a/lib/Doctrine/Query/Where.php b/lib/Doctrine/Query/Where.php index 735a2326d..3ce934514 100644 --- a/lib/Doctrine/Query/Where.php +++ b/lib/Doctrine/Query/Where.php @@ -110,7 +110,7 @@ class Doctrine_Query_Where extends Doctrine_Query_Condition foreach ($values as $value) { $where[] = $alias . '.' . $relation->getLocal() . ' IN (SELECT '.$relation->getForeign() - . ' FROM ' . $relation->getTable()->getTableName() + . ' FROM ' . $relation->getTable()->getTableName() . ' WHERE ' . $field . $operator . $value . ')'; } $where = implode(' AND ', $where); @@ -178,8 +178,8 @@ class Doctrine_Query_Where extends Doctrine_Query_Condition $fieldname = $field; } - $where = $fieldname . ' ' - . $operator . ' ' . $value; + $where = $fieldname . ' ' + . $operator . ' ' . $value; } } }