StringQuery.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\model;
12 
20 
21 /**
22  * StringQuery executes queries from a string representation. Queries are
23  * constructed like WHERE clauses in sql, except that foreign key relations between the
24  * different types are not necessary. Attributes have to be prepended with the
25  * type name (or in case of ambiguity the role name), e.g. Author.name instead of name.
26  *
27  * @note: The query does not search in objects, that are created inside the current transaction.
28  *
29  * The following example shows the usage:
30  *
31  * @code
32  * $queryStr = "Author.name LIKE '%ingo%' AND (Recipe.name LIKE '%Salat%' OR Recipe.portions = 4)";
33  * $query = new StringQuery('Author');
34  * $query->setConditionString($queryStr);
35  * $authorOIDs = $query->execute(false);
36  * @endcode
37  *
38  * @author ingo herwig <ingo@wemove.com>
39  */
40 class StringQuery extends ObjectQuery {
41 
42  private $_condition = '';
43 
44  /**
45  * Set the query condition string
46  * @param $condition The query definition string
47  */
48  public function setConditionString($condition) {
49  $this->_condition = $condition;
50  }
51 
52  /**
53  * @see AbstractQuery::buildQuery()
54  */
55  protected function buildQuery($orderby=null, PagingInfo $pagingInfo=null) {
56  $queryType = $this->getQueryType();
57  $mapper = self::getMapper($queryType);
58 
59  // create the attribute string (use the default select from the mapper,
60  // since we are only interested in the attributes)
61  $selectStmt = $mapper->getSelectSQL(null, null, null, $pagingInfo, $this->getId());
62  if (!$selectStmt->isCached()) {
63  // initialize the statement
64  $selectStmt->distinct(true);
65 
66  $persistenceFacade = ObjectFactory::getInstance('persistenceFacade');
67  $quoteIdentifierSymbol = $mapper->getQuoteIdentifierSymbol();
68  // get all referenced types/roles from the condition and translate
69  // attributes to column names
70  $conditionString = $this->_condition;
71  $otherRoles = array();
72  $tokens = StringUtil::splitQuoted($conditionString, "/[\s=<>()!]+/", "'", true);
73  $operators = array('and', 'or', 'not', 'like', 'regexp', 'is', 'null');
74  foreach ($tokens as $token) {
75  if (strlen($token) > 0) {
76  if (!in_array(strtolower($token), $operators)) {
77  // three possibilities left: token is
78  // 1. type or attribute (not allowed)
79  // 2. type.attribute
80  // 3. searchterm
81  if (!preg_match('/^\'|^[0-9]/', $token)) {
82  // token is no searchterm (does not start with a quote or a number)
83  $token = str_replace($quoteIdentifierSymbol, '', $token);
84  $pos = strpos($token, '.');
85  if ($pos > 0) {
86  // token is type/role.attribute
87  list($typeOrRole, $attribute) = explode('.', $token, 2);
88  // check if the token is a type
89  $fqType = $persistenceFacade->isKnownType($typeOrRole) ?
90  $persistenceFacade->getFullyQualifiedType($typeOrRole) : null;
91  if ($fqType == null || $fqType != $queryType) {
92  // find connection if the token does not match the queryType
93  if (!isset($otherRoles[$typeOrRole])) {
94  // find the path from the queryType to the other type/role
95  // (role is preferred)
96  $paths = NodeUtil::getConnections($queryType, $typeOrRole, null);
97  if (sizeof($paths) == 0) {
98  // fallback: search for type
99  $paths = NodeUtil::getConnections($queryType, null, $typeOrRole);
100  }
101  if (sizeof($paths) == 0) {
102  // no connection found
103  throw new IllegalArgumentException("There is no connection between '".$queryType."' and '".$typeOrRole."'.");
104  }
105  elseif (sizeof($paths) > 1) {
106  // more than one connection found
107  throw new IllegalArgumentException("There is more than one connection between '".$queryType."' and '".$typeOrRole."'. ".
108  "Try to use a role name for the target end.");
109  }
110  // exactly one connection (store it for later reference)
111  $otherRoles[$typeOrRole] = $paths[0];
112  }
113  // find the type of the referenced node
114  $path = $otherRoles[$typeOrRole];
115  $type = $path->getEndType();
116  }
117  else {
118  $type = $queryType;
119  }
120 
121  // map the attributes to columns
122  list($table, $column) = self::mapToDatabase($type, $attribute);
123  $conditionString = str_replace($quoteIdentifierSymbol.$attribute.$quoteIdentifierSymbol,
124  $quoteIdentifierSymbol.$column.$quoteIdentifierSymbol, $conditionString);
125  }
126  else {
127  throw new IllegalArgumentException("Please specify the type/role to that the attribute '".$token."' belongs: e.g. Author.name.");
128  }
129  }
130  }
131  }
132  }
133  if (strlen($conditionString)) {
134  $selectStmt->where($conditionString);
135  }
136 
137  // get relation conditions
138  // NOTE: temporarily created objects must be detached from the transaction
139  $tx = $persistenceFacade->getTransaction();
140  $rootNode = $persistenceFacade->create($queryType);
141  $tx->detach($rootNode->getOID());
142  foreach ($otherRoles as $typeOrRole => $pathDescription) {
143  $relationDescriptions = $pathDescription->getPath();
144  $parent = $rootNode;
145  foreach ($relationDescriptions as $relationDescription) {
146  $node = $persistenceFacade->create($relationDescription->getOtherType());
147  $tx->detach($node->getOID());
148  $parent->addNode($node, $relationDescription->getOtherRole());
149  $parent = $node;
150  }
151  // set the table name of the target node to the name that is
152  // referenced in the query condition
153  $node->setProperty(self::PROPERTY_TABLE_NAME, $typeOrRole);
154  }
155  $this->processObjectTemplate($rootNode, $selectStmt);
156  }
157 
158  // set orderby after all involved tables are known in order to
159  // prefix the correct table name
160  $this->processOrderBy($orderby, $selectStmt);
161 
162  // reset internal variables
163  $this->resetInternals();
164 
165  return $selectStmt;
166  }
167 
168  /**
169  * Map a application type and value name to the appropriate database names
170  * @param $type The type to map
171  * @param $valueName The name of the value to map
172  * @return An array with the table and column name or null if no mapper is found
173  */
174  protected static function mapToDatabase($type, $valueName) {
175  $mapper = self::getMapper($type);
176  $attributeDescription = $mapper->getAttribute($valueName);
177 
178  $table = $mapper->getRealTableName();
179  $column = $attributeDescription->getColumn();
180  return array($table, $column);
181  }
182 }
183 ?>
resetInternals()
Reset internal variables.
Node related interfaces and classes.
Definition: namespaces.php:26
StringQuery executes queries from a string representation.
Definition: StringQuery.php:40
IllegalArgumentException signals an exception in method arguments.
ObjectQuery implements a template based object query.
buildQuery($orderby=null, PagingInfo $pagingInfo=null)
Definition: StringQuery.php:55
getId()
Get the query id.
processOrderBy($orderby, SelectStatement $selectStmt)
Process an object template.
static getInstance($name, $dynamicConfiguration=array())
PagingInfo contains information about a paged list.
Definition: PagingInfo.php:18
processObjectTemplate(PersistentObject $tpl, SelectStatement $selectStmt)
Process an object template.
static splitQuoted($string, $delim='//', $quoteChr='"', $preserve=false)
Split string preserving quoted strings code based on: http://www.php.net/manual/en/function.explode.php#94024.
Definition: StringUtil.php:239
setConditionString($condition)
Set the query condition string.
Definition: StringQuery.php:48
static getConnections($type, $otherRole, $otherType, $hierarchyType='all')
Get the shortest paths that connect a type to another type.
Definition: NodeUtil.php:39
static mapToDatabase($type, $valueName)
Map a application type and value name to the appropriate database names.