ObjectQuery.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 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 
30 
31 /**
32  * ObjectQuery implements a template based object query. This class provides the
33  * user with object templates on which query conditions may be set. Object templates
34  * are Node instances whose attribute values are used as conditions on the
35  * appropriate attributes. A value maybe a scalar or a Criteria instance. For
36  * example $authorTpl->setValue("name", Criteria::forValue("LIKE", "%ingo%") means searching
37  * for authors whose name contains 'ingo'. If only a scalar is given LIKE '%...%' is assumed.
38  *
39  * A value condition of a template is joined with the preceeding conditions using the combine
40  * operator (Criteria::OPERATOR_AND, Criteria::OPERATOR_OR) given in the Criteria assigned to
41  * the template value.
42  * The set of conditions of a template is preceded by the operator (Criteria::OPERATOR_AND,
43  * Criteria::OPERATOR_OR) given in the ObjectQuery::PROPERTY_COMBINE_OPERATOR property (default:
44  * Criteria::OPERATOR_AND) of the template
45  * (see ObjectQuery::getObjectTemplate()).
46  *
47  * Multiple conditions for one value are built using different object templates of the
48  * same type. Conditions sets of different object templates are grouped with brackets if
49  * they are passed to ObjectQuery::makeGroup().
50  *
51  * @note: If there are two object templates of the same type as the query type linked in
52  * a parent child relation, than the nodes that are selected depend on the attributes of
53  * the first object template that is received by ObjectQuery::getObjectTemplate.
54  *
55  * @note: The query does not search in objects, that are created inside the current transaction.
56  *
57  * The following example shows the usage:
58  *
59  * @code
60  * // The code builds the following query condition:
61  * // WHERE (Author.name LIKE '%ingo%' AND Author.email LIKE '%wemove%') OR (Author.name LIKE '%herwig%') AND
62  * // (Recipe.created >= '2004-01-01') AND (Recipe.created < '2005-01-01') AND ((Recipe.name LIKE '%Salat%') OR (Recipe.portions = 4))
63  *
64  * $query = new ObjectQuery('Author');
65  *
66  * // (Author.name LIKE '%ingo%' AND Author.email LIKE '%wemove%')
67  * $authorTpl1 = $query->getObjectTemplate('Author');
68  * $authorTpl1->setValue("name", Criteria::asValue("LIKE", "%ingo%"));
69  * $authorTpl1->setValue("email", Criteria::asValue("LIKE", "%wemove%"));
70  *
71  * // OR Author.name LIKE '%herwig%'
72  * $authorTpl2 = $query->getObjectTemplate('Author', null, Criteria::OPERATOR_OR);
73  * $authorTpl2->setValue("name", Criteria::asValue("LIKE", "%herwig%"));
74  *
75  * // Recipe.created >= '2004-01-01' AND Recipe.created < '2005-01-01'
76  * $recipeTpl1 = $query->getObjectTemplate('Recipe');
77  * $recipeTpl1->setValue("created", Criteria::asValue(">=", "2004-01-01"));
78  * $recipeTpl2 = $query->getObjectTemplate('Recipe');
79  * $recipeTpl2->setValue("created", Criteria::asValue("<", "2005-01-01"));
80  *
81  * // AND (Recipe.name LIKE '%Salat%' OR Recipe.portions = 4)
82  * // could have be built using one template, but this demonstrates the usage
83  * // of the ObjectQuery::makeGroup() method
84  * $recipeTpl3 = $query->getObjectTemplate('Recipe');
85  * $recipeTpl3->setValue("name", Criteria::asValue("LIKE", "%Salat%"));
86  * $recipeTpl4 = $query->getObjectTemplate('Recipe', null, Criteria::OPERATOR_OR);
87  * $recipeTpl4->setValue("portions", Criteria::asValue("=", 4));
88  * $query->makeGroup([$recipeTpl3, $recipeTpl4], Criteria::OPERATOR_AND);
89  *
90  * $authorTpl1->addNode($recipeTpl1, 'Recipe');
91  * $authorTpl1->addNode($recipeTpl2, 'Recipe');
92  * $authorTpl1->addNode($recipeTpl3, 'Recipe');
93  * $authorTpl1->addNode($recipeTpl4, 'Recipe');
94  * $authorList = $query->execute(BuildDepth::SINGLE);
95  * @endcode
96  *
97  * @note There are some limitations when using this class:
98  * - This class works only with Nodes as PersistentObjects
99  * - This class only works for Nodes mapped by RDBMapper subclasses.
100  * - All objects have to reside in the same datastore (the connection is taken from the first mapper)
101  * - Since the query values are set together with the operator in a single string,
102  * they must be converted to data store format already
103  *
104  * @author ingo herwig <ingo@wemove.com>
105  */
106 class ObjectQuery extends AbstractQuery {
107 
108  const PROPERTY_COMBINE_OPERATOR = "object_query_combine_operator";
109  const PROPERTY_TABLE_NAME = "object_query_table_name";
110  const PROPERTY_INITIAL_OID = "object_query_initial_oid";
111 
112  private $id = '';
113  private $typeNode = null;
114  private $isTypeNodeInQuery = false;
115  private $rootNodes = [];
116  private $conditions = [];
117  private $groups = [];
118  private $groupedOIDs = [];
119  private $processedNodes = [];
120  private $involvedTypes = [];
121  private $aliasCounter = 1;
122  private $observedObjects = [];
123  private $parameterCriteriaMap = [];
124 
125  private static $logger = null;
126 
127  /**
128  * Constructor.
129  * @param $type The type to search for
130  * @param $queryId Identifier for the query cache (maybe null to prevent caching) (default: _null_)
131  */
132  public function __construct($type, $queryId=SelectStatement::NO_CACHE) {
133  // don't use PersistenceFacade::create, because template instances must be transient
134  $mapper = self::getMapper($type);
135  $this->typeNode = $mapper->create($type, BuildDepth::SINGLE);
136  $this->rootNodes[] = $this->typeNode;
137  $this->id = $queryId == null ? SelectStatement::NO_CACHE : $queryId;
138  ObjectFactory::getInstance('eventManager')->addListener(ValueChangeEvent::NAME,
139  [$this, 'valueChanged']);
140  if (self::$logger == null) {
141  self::$logger = LogManager::getLogger(__CLASS__);
142  }
143  }
144 
145  /**
146  * Destructor.
147  */
148  public function __destruct() {
149  ObjectFactory::getInstance('eventManager')->removeListener(ValueChangeEvent::NAME,
150  [$this, 'valueChanged']);
151  }
152 
153  /**
154  * Get the query id
155  * @return String
156  */
157  public function getId() {
158  return $this->id;
159  }
160 
161  /**
162  * Get an object template for a given type.
163  * @param $type The type to query for
164  * @param $alias An alias name to be used in the query. if null, use the default name (default: _null_)
165  * @param $combineOperator One of the Criteria::OPERATOR constants that precedes
166  * the conditions described in the template (default: _Criteria::OPERATOR_AND_)
167  * @return Node
168  */
169  public function getObjectTemplate($type, $alias=null, $combineOperator=Criteria::OPERATOR_AND) {
170  $template = null;
171 
172  // use the typeNode, the first time a node template of the query type is requested
173  $persistenceFacade = ObjectFactory::getInstance('persistenceFacade');
174  $fqType = $persistenceFacade->getFullyQualifiedType($type);
175  if ($fqType == $this->typeNode->getType() && !$this->isTypeNodeInQuery) {
176  $template = $this->typeNode;
177  $this->isTypeNodeInQuery = true;
178  // the typeNode is contained already in the rootNodes array
179  }
180  else {
181  // don't use PersistenceFacade::create, because template instances must be transient
182  $mapper = self::getMapper($fqType);
183  $template = $mapper->create($fqType, BuildDepth::SINGLE);
184  $this->rootNodes[] = $template;
185  }
186  $template->setProperty(self::PROPERTY_COMBINE_OPERATOR, $combineOperator);
187  if ($alias != null) {
188  $template->setProperty(self::PROPERTY_TABLE_NAME, $alias);
189  }
190  $initialOid = $template->getOID()->__toString();
191  $template->setProperty(self::PROPERTY_INITIAL_OID, $initialOid);
192  $this->observedObjects[$initialOid] = $template;
193  return $template;
194  }
195 
196  /**
197  * Register an object template at the query.
198  * @param $template Node instance to register as template
199  * @param $alias An alias name to be used in the query. if null, use the default name (default: _null_)
200  * @param $combineOperator One of the Criteria::OPERATOR constants that precedes
201  * the conditions described in the template (default: Criteria::OPERATOR_AND)
202  */
203  public function registerObjectTemplate(Node $template, $alias=null, $combineOperator=Criteria::OPERATOR_AND) {
204  if ($template != null) {
205  $initialOid = $template->getOID();
206  $initialOidStr = $initialOid->__toString();
207  $template->setProperty(self::PROPERTY_INITIAL_OID, $initialOidStr);
208  $this->observedObjects[$initialOidStr] = $template;
209 
210  // call the setters for all attributes in order to register them in the query
211  $includePKs = !ObjectId::isDummyId($initialOid->getFirstId());
212  $template->copyValues($template, $includePKs);
213 
214  $template->setProperty(self::PROPERTY_COMBINE_OPERATOR, $combineOperator);
215  if ($alias != null) {
216  $template->setProperty(self::PROPERTY_TABLE_NAME, $alias);
217  }
218 
219  // replace the typeNode, the first time a node template of the query type is registered
220  if ($template->getType() == $this->typeNode->getType() && !$this->isTypeNodeInQuery) {
221  $newRootNodes = [$template];
222  foreach($this->rootNodes as $node) {
223  if ($node != $this->typeNode) {
224  $newRootNodes[] = $node;
225  }
226  }
227  $this->rootNodes = $newRootNodes;
228  $this->isTypeNodeInQuery = true;
229  }
230  else {
231  $this->rootNodes[] = $template;
232  }
233  }
234  }
235 
236  /**
237  * Group different templates together to realize brackets in the query.
238  * @note Grouped templates will be ignored, when iterating over the object tree and appended at the end.
239  * @param $templates An array of references to the templates contained in the group
240  * @param $combineOperator One of the Criteria::OPERATOR constants that precedes the group (default: _Criteria::OPERATOR_AND_)
241  */
242  public function makeGroup($templates, $combineOperator=Criteria::OPERATOR_AND) {
243  $this->groups[] = ['tpls' => $templates, self::PROPERTY_COMBINE_OPERATOR => $combineOperator];
244  // store grouped nodes in an extra array to separate them from the others
245  for ($i=0; $i<sizeof($templates); $i++) {
246  if ($templates[$i] != null) {
247  $this->groupedOIDs[] = $templates[$i]->getOID();
248  }
249  else {
250  throw new IllegalArgumentException("Null value found in group");
251  }
252  }
253  }
254 
255  /**
256  * Get the condition part of the query. This is especially useful to
257  * build a StringQuery from the query objects.
258  * @return String
259  */
260  public function getQueryCondition() {
261  $query = $this->getQueryString();
262  $tmp = preg_split("/ WHERE /i", $query);
263  if (sizeof($tmp) > 1) {
264  $tmp = preg_split("/ ORDER /i", $tmp[1]);
265  return $tmp[0];
266  }
267  return '';
268  }
269 
270  /**
271  * @see AbstractQuery::getLogger()
272  */
273  protected function getLogger() {
274  return self::$logger;
275  }
276 
277  /**
278  * @see AbstractQuery::getQueryType()
279  */
280  public function getQueryType() {
281  return $this->typeNode->getType();
282  }
283 
284  /**
285  * @see AbstractQuery::buildQuery()
286  */
287  protected function buildQuery($buildDepth, $orderby=null, PagingInfo $pagingInfo=null) {
288  $type = $this->typeNode->getType();
289  $mapper = self::getMapper($type);
290  $this->involvedTypes[$type] = true;
291 
292  // create the attribute string (use the default select from the mapper,
293  // since we are only interested in the attributes)
294  $tableName = self::processTableName($this->typeNode);
295  $attributes = $buildDepth === false ? $mapper->getPkNames() : null;
296  $selectStmt = $mapper->getSelectSQL(null, $tableName['alias'], $attributes, null, $pagingInfo, $this->getId());
297  if (!$selectStmt->isCached()) {
298  // initialize the statement
299  $selectStmt->quantifier(SelectStatement::QUANTIFIER_DISTINCT);
300 
301  // process all root nodes except for grouped nodes
302  foreach ($this->rootNodes as $curNode) {
303  if (!in_array($curNode->getOID(), $this->groupedOIDs)) {
304  $this->processObjectTemplate($curNode, $selectStmt);
305  }
306  }
307 
308  // process groups
309  for ($i=0, $countI=sizeof($this->groups); $i<$countI; $i++) {
310  $group = $this->groups[$i];
311  $tmpSelectStmt = SelectStatement::get($mapper, $this->getId().'_g'.$i);
312  $tmpSelectStmt->from($this->typeNode->getProperty(self::PROPERTY_TABLE_NAME));
313  for ($j=0, $countJ=sizeof($group['tpls']); $j<$countJ; $j++) {
314  $tpl = $group['tpls'][$j];
315  $this->processObjectTemplate($tpl, $tmpSelectStmt);
316  }
317  $where = $tmpSelectStmt->getRawState(SelectStatement::WHERE);
318  $combineOperator = $group[self::PROPERTY_COMBINE_OPERATOR];
319  $condition = '';
320  foreach ($where->getExpressionData() as $expressionData) {
321  $expression = is_array($expressionData) ? $expressionData[0] : $expressionData;
322  $condition .= $expression;
323  }
324  $selectStmt->where('('.$condition.')', $combineOperator);
325  }
326 
327  // set orderby after all involved tables are known in order to
328  // prefix the correct table name
329  $this->processOrderBy($orderby, $selectStmt);
330 
331  // set parameter order to be reused next time
332  $selectStmt->setMeta('parameterCriteriaMap', $this->parameterCriteriaMap);
333  }
334 
335  // set parameters
336  $selectStmt->setParameters($this->getParameters($this->conditions,
337  $selectStmt->getMeta('parameterCriteriaMap')));
338 
339  // reset internal variables
340  $this->resetInternals();
341 
342  return $selectStmt;
343  }
344 
345  /**
346  * Process an object template
347  * @param $tpl The object template
348  * @param $selectStmt A SelectStatement instance
349  */
350  protected function processObjectTemplate(PersistentObject $tpl, SelectStatement $selectStmt) {
351  // avoid infinite recursion
352  $oidStr = $tpl->getOID()->__toString();
353  if (isset($this->processedNodes[$oidStr])) {
354  return;
355  }
356 
357  $mapper = self::getMapper($tpl->getType());
358  $tableName = self::processTableName($tpl);
359  $this->involvedTypes[$tpl->getType()] = true;
360 
361  // add condition
362  $condition = '';
363  $iter = new NodeValueIterator($tpl, false);
364  foreach($iter as $valueName => $value) {
365  // check if the value was set when building the query
366  if (isset($this->conditions[$oidStr][$valueName])) {
367  $criterion = $this->conditions[$oidStr][$valueName];
368  if ($criterion instanceof Criteria) {
369  $attributeDesc = $mapper->getAttribute($valueName);
370  if ($attributeDesc) {
371  // add the combine operator, if there are already other conditions
372  if (strlen($condition) > 0) {
373  $condition .= ' '.$criterion->getCombineOperator().' ';
374  }
375  // because the attributes are not selected with alias, the column name has to be used
376  $index = sizeof(array_keys($this->parameterCriteriaMap));
377  $placeholder = ':'.$tableName['alias'].'_'.$attributeDesc->getColumn().$index;
378  list($criteriaCondition, $criteriaPlaceholder) = $mapper->renderCriteria($criterion,
379  $placeholder, $tableName['alias'], $attributeDesc->getColumn());
380  $condition .= $criteriaCondition;
381  if ($criteriaPlaceholder) {
382  $value = $this->getParameterPosition($criterion, $this->conditions);
383  if (is_array($criteriaPlaceholder)) {
384  $value = array_fill(0, sizeof($criteriaPlaceholder), $value);
385  $this->parameterCriteriaMap = array_merge($this->parameterCriteriaMap,
386  array_combine($criteriaPlaceholder, $value));
387  }
388  else {
389  $this->parameterCriteriaMap[$criteriaPlaceholder] = $value;
390  }
391  }
392  }
393  }
394  }
395  }
396  if (strlen($condition) > 0) {
397  $combineOperator = $tpl->getProperty(self::PROPERTY_COMBINE_OPERATOR);
398  $selectStmt->where('('.$condition.')', $combineOperator);
399  }
400 
401  // register the node as processed
402  $this->processedNodes[$oidStr] = $tpl;
403 
404  // add relations to children (this includes also many to many relations)
405  // and process children
406  foreach ($mapper->getRelations() as $relationDescription) {
407  $children = $tpl->getValue($relationDescription->getOtherRole());
408  if ($children) {
409  $children = !is_array($children) ? [$children] : $children;
410  for ($i=0, $count=sizeof($children); $i<$count; $i++) {
411  $curChild = $children[$i];
412  if ($curChild instanceof Node) {
413  // process relations
414 
415  // don't process the relation twice (e.g. in a many to many relation, both
416  // ends are child ends)
417  if (!isset($this->processedNodes[$curChild->getOID()->__toString()])) {
418  // don't join the tables twice
419  $childTableName = self::processTableName($curChild);
420  $fromPart = $selectStmt->getRawState(SelectStatement::TABLE);
421  if (!isset($fromPart[$childTableName['alias']])) {
422  $childMapper = self::getMapper($curChild->getType());
423  if ($relationDescription instanceof RDBManyToOneRelationDescription) {
424  $idAttr = $childMapper->getAttribute($relationDescription->getIdName());
425  $fkAttr = $mapper->getAttribute($relationDescription->getFkName());
426  $joinCondition = $tpl->getProperty(self::PROPERTY_TABLE_NAME).'.'.$fkAttr->getColumn().' = '.
427  $curChild->getProperty(self::PROPERTY_TABLE_NAME).'.'.$idAttr->getColumn();
428  $selectStmt->join([$childTableName['alias'] => $childTableName['name']], $joinCondition, []);
429  }
430  elseif ($relationDescription instanceof RDBOneToManyRelationDescription) {
431  $idAttr = $mapper->getAttribute($relationDescription->getIdName());
432  $fkAttr = $childMapper->getAttribute($relationDescription->getFkName());
433  $joinCondition = $curChild->getProperty(self::PROPERTY_TABLE_NAME).'.'.$fkAttr->getColumn().' = '.
434  $tpl->getProperty(self::PROPERTY_TABLE_NAME).'.'.$idAttr->getColumn();
435  $selectStmt->join([$childTableName['alias'] => $childTableName['name']], $joinCondition, []);
436  }
437  elseif ($relationDescription instanceof RDBManyToManyRelationDescription) {
438  $thisRelationDescription = $relationDescription->getThisEndRelation();
439  $otherRelationDescription = $relationDescription->getOtherEndRelation();
440 
441  $nmMapper = self::getMapper($thisRelationDescription->getOtherType());
442  $otherFkAttr = $nmMapper->getAttribute($otherRelationDescription->getFkName());
443  $otherIdAttr = $childMapper->getAttribute($otherRelationDescription->getIdName());
444  $thisFkAttr = $nmMapper->getAttribute($thisRelationDescription->getFkName());
445  $thisIdAttr = $mapper->getAttribute($thisRelationDescription->getIdName());
446 
447  $nmAlias = $nmMapper->getRealTableName().ucfirst($childTableName['alias']);
448 
449  $joinCondition1 = $nmAlias.'.'.$thisFkAttr->getColumn().' = '.
450  $tpl->getProperty(self::PROPERTY_TABLE_NAME).'.'.
451  $thisIdAttr->getColumn();
452  $joinCondition2 = $curChild->getProperty(self::PROPERTY_TABLE_NAME).'.'.$otherIdAttr->getColumn().' = '.
453  $nmAlias.'.'.$otherFkAttr->getColumn();
454 
455  $selectStmt->join([$nmAlias => $nmMapper->getRealTableName()], $joinCondition1, []);
456  $selectStmt->join([$childTableName['alias'] => $childTableName['name']], $joinCondition2, []);
457 
458  // register the nm type
459  $this->involvedTypes[$nmMapper->getType()] = true;
460  }
461  }
462  }
463 
464  // process child
465  if (!in_array($curChild->getOID(), $this->groupedOIDs)) {
466  $this->processObjectTemplate($curChild, $selectStmt);
467  }
468  }
469  }
470  }
471  }
472  }
473 
474  /**
475  * Process an object template
476  * @param $orderby An array holding names of attributes to order by, maybe appended with 'ASC', 'DESC' (maybe null)
477  * @param $selectStmt A SelectStatement instance
478  */
479  protected function processOrderBy($orderby, SelectStatement $selectStmt) {
480  $persistenceFacade = ObjectFactory::getInstance('persistenceFacade');
481  if ($orderby) {
482  $ok = false;
483 
484  // reset current order by
485  $selectStmt->reset(SelectStatement::ORDER);
486 
487  foreach ($orderby as $curOrderBy) {
488  $orderByParts = preg_split('/ /', $curOrderBy);
489  $orderAttribute = $orderByParts[0];
490  $orderDirection = sizeof($orderByParts) > 1 ? $orderByParts[1] : 'ASC';
491  $orderType = null;
492  $orderTypeMapper = null;
493  if (strpos($orderAttribute, '.') > 0) {
494  // the type is included in the attribute
495  $orderAttributeParts = preg_split('/\./', $orderAttribute);
496  $orderAttribute = array_pop($orderAttributeParts);
497  $orderType = join('.', $orderAttributeParts);
498  $orderTypeMapper = self::getMapper($orderType);
499  }
500  else {
501  // check all involved types
502  foreach (array_keys($this->involvedTypes) as $curType) {
503  $mapper = self::getMapper($curType);
504  if ($mapper->hasAttribute($orderAttribute)) {
505  $orderTypeMapper = $mapper;
506  break;
507  }
508  }
509  }
510  if ($orderTypeMapper) {
511  $orderTableName = $orderTypeMapper->getRealTableName();
512  // get table alias if defined and if type was not set explicitly
513  if (!$orderType) {
514  $aliasNames = $selectStmt->getAliasNames($orderTableName);
515  $orderTableName = sizeof($aliasNames) > 0 ? $aliasNames[0] : $orderTableName;
516  }
517  $orderAttributeDesc = $orderTypeMapper->getAttribute($orderAttribute);
518  $orderColumnName = $orderAttributeDesc->getColumn();
519 
520  if ($orderTableName) {
521  $orderAttributeFinal = $orderTableName.'.'.$orderColumnName;
522  $selectStmt->order([$orderAttributeFinal.' '.$orderDirection]);
523  $ok = true;
524  }
525  }
526  }
527  if (!$ok) {
528  throw new UnknownFieldException($orderAttribute, "The sort field name '".$orderAttribute."' is unknown");
529  }
530  }
531  // add order columns to selected columns as required for distinct queries
532  $order = $selectStmt->getRawState(SelectStatement::ORDER);
533  if (sizeof($order) > 0) {
534  $columnNames = array_keys($selectStmt->getRawState(SelectStatement::COLUMNS));
535  foreach($order as $curOrderBy) {
536  $orderByParts = preg_split('/ /', $curOrderBy);
537  $orderAttribute = $orderByParts[0];
538  $orderType = null;
539  if (strpos($orderAttribute, '.') > 0) {
540  // the type is included in the attribute
541  $orderAttributeParts = preg_split('/\./', $orderAttribute);
542  $orderAttribute = array_pop($orderAttributeParts);
543  $orderType = join('.', $orderAttributeParts);
544  }
545  // add the column if it does not exist yet, or if it belongs to an alias
546  if (!in_array($orderAttribute, $columnNames) || $orderType != null) {
547  $queryType = $this->getQueryType();
548  $joinName = ($orderType != $queryType && $orderType != $persistenceFacade->getSimpleType($queryType)) ? $orderType: null;
549  $selectStmt->addColumns([$orderAttribute => $orderAttribute], $joinName);
550  $columnNames[] = $orderAttribute;
551  }
552  }
553  }
554  }
555 
556  /**
557  * Get an array of parameter values for the given criteria
558  * @param $criteria An array of Criteria instances that define conditions on the object's attributes (maybe null)
559  * @param $parameters Array defining the parameter order
560  * @return Array
561  */
562  protected function getParameters($criteria, array $parameters) {
563  $result = [];
564  // flatten conditions
565  $criteriaFlat = [];
566  foreach ($criteria as $key => $curCriteria) {
567  foreach ($curCriteria as $criterion) {
568  if ($criterion instanceof Criteria) {
569  $mapper = self::getMapper($criterion->getType());
570  $valueName = $criterion->getAttribute();
571  $attributeDesc = $mapper->getAttribute($valueName);
572  if ($attributeDesc) {
573  $criteriaFlat[] = $criterion;
574  }
575  }
576  }
577  }
578  // get parameters in order
579  $criteriaValuePosition = [];
580  foreach ($parameters as $placeholder => $index) {
581  $value = $criteriaFlat[$index]->getValue();
582  if (is_array($value)) {
583  // criteria has array value
584  // initialize position counting for this criteria (defined by index)
585  if (!isset($criteriaValuePosition[$index])) {
586  $criteriaValuePosition[$index] = 0;
587  }
588  // set the value from the array
589  $result[$placeholder] = $value[$criteriaValuePosition[$index]];
590  $criteriaValuePosition[$index]++;
591  }
592  else {
593  // criteria has single value
594  $result[$placeholder] = $value;
595  }
596  }
597  return $result;
598  }
599 
600  protected function getParameterPosition($criterion, $criteria) {
601  $i=0;
602  foreach ($criteria as $key => $curCriteria) {
603  foreach ($curCriteria as $curCriterion) {
604  if ($curCriterion instanceof Criteria) {
605  if ($curCriterion->__toString() === $criterion->__toString()) {
606  return $i;
607  }
608  $i++;
609  }
610  }
611  }
612  }
613 
614  /**
615  * Reset internal variables. Must be called after buildQuery
616  */
617  protected function resetInternals() {
618  $this->processedNodes = [];
619  $this->parameterCriteriaMap = [];
620  $this->involvedTypes = [];
621  $this->aliasCounter = 1;
622  }
623 
624  /**
625  * Get the table name for the template and calculate an alias if
626  * necessary.
627  * @param $tpl The object template
628  * @return Associative array with keys 'name', 'alias'
629  */
630  protected function processTableName(Node $tpl) {
631  $mapper = self::getMapper($tpl->getType());
632  $mapperTableName = $mapper->getRealTableName();
633 
634  $tableName = $tpl->getProperty(self::PROPERTY_TABLE_NAME);
635  if ($tableName == null) {
636  $tableName = $mapperTableName;
637 
638  // if the template is the child of another node of the same type,
639  // we must use a table alias
640  $parents = $tpl->getParentsEx(null, null, $tpl->getType());
641  foreach ($parents as $curParent) {
642  $curParentTableName = $curParent->getProperty(self::PROPERTY_TABLE_NAME);
643  if ($curParentTableName == $tableName) {
644  $tableName .= '_'.($this->aliasCounter++);
645  }
646  }
647  // set the table name for later reference
648  $tpl->setProperty(self::PROPERTY_TABLE_NAME, $tableName);
649  }
650 
651  return ['name' => $mapperTableName, 'alias' => $tableName];
652  }
653 
654  /**
655  * Listen to ValueChangeEvents
656  * @param $event ValueChangeEvent instance
657  */
658  public function valueChanged(ValueChangeEvent $event) {
659  $object = $event->getObject();
660  $name = $event->getValueName();
661  $initialOid = $object->getProperty(self::PROPERTY_INITIAL_OID);
662  if (isset($this->observedObjects[$initialOid])) {
663  $newValue = $event->getNewValue();
664  // make a criteria from newValue and make sure that all properties are set
665  if (!($newValue instanceof Criteria)) {
666  // LIKE fallback, if the value is not a Criteria instance
667  $mapper = self::getMapper($object->getType());
668  $pkNames = $mapper->getPkNames();
669  if (!in_array($name, $pkNames)) {
670  // use like condition on any attribute, if it's a string
671  // other value changes will be ignored!
672  if (is_string($newValue)) {
673  $newValue = new Criteria($object->getType(), $name, 'LIKE', '%'.$newValue.'%');
674  }
675  }
676  else {
677  // don't search for pk names with LIKE
678  $newValue = new Criteria($object->getType(), $name, '=', $newValue);
679  }
680  }
681  else {
682  // make sure that type and name are set even if the Criteria is constructed
683  // via Criteria::forValue()
684  $newValue->setType($object->getType());
685  $newValue->setAttribute($name);
686  }
687 
688  $oid = $object->getOID()->__toString();
689  // store change in internal array to have it when constructing the query
690  if (!isset($this->conditions[$oid])) {
691  $this->conditions[$oid] = [];
692  }
693  $this->conditions[$oid][$name] = $newValue;
694  }
695  }
696 }
697 ?>
makeGroup($templates, $combineOperator=Criteria::OPERATOR_AND)
Group different templates together to realize brackets in the query.
getId()
Get the query id.
getType()
Get the type of the object.
static isDummyId($id)
Check if a given id is a dummy id.
Definition: ObjectId.php:239
getObjectTemplate($type, $alias=null, $combineOperator=Criteria::OPERATOR_AND)
Get an object template for a given type.
Instances of RDBManyToManyRelationDescription describe a many to many relation from 'this' end to 'ot...
getValueName()
Get the name of the value that has changed.
processTableName(Node $tpl)
Get the table name for the template and calculate an alias if necessary.
NodeValueIterator is used to iterate over all persistent values of a Node (not including relations).
copyValues(PersistentObject $object, $copyPkValues=true)
getOID()
Get the object id of the PersistentObject.
__construct($type, $queryId=SelectStatement::NO_CACHE)
Constructor.
Instances of RDBManyToOneRelationDescription describe a many to one relation from 'this' end (many) t...
IllegalArgumentException signals an exception in method arguments.
getObject()
Get the object whose value has changed.
registerObjectTemplate(Node $template, $alias=null, $combineOperator=Criteria::OPERATOR_AND)
Register an object template at the query.
Criteria defines a condition on a PersistentObject's attribute used to select specific instances.
Definition: Criteria.php:21
getValue($name)
Get the value of an attribute.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:28
ValueChangeEvent signals a change of a value of a PersistentObject instance.
addColumns(array $columns, $joinName=null)
Add columns to the statement.
BuildDepth values are used to define the depth when loading object trees.
Definition: BuildDepth.php:19
getParentsEx(ObjectId $oid=null, $role=null, $type=null, $values=null, $properties=null, $useRegExp=true)
Get the parents that match given conditions.
Definition: Node.php:674
static getMapper($type)
Get the mapper for a Node and check if it is a supported one.
getQueryString($buildDepth=BuildDepth::SINGLE, $orderby=null)
Get the query serialized to a string.
buildQuery($buildDepth, $orderby=null, PagingInfo $pagingInfo=null)
join($name, $on, $columns=self::SQL_STAR, $type=self::JOIN_INNER)
getAliasNames($table)
Get the alias names for a table name.
AbstractQuery is the base class for all query classes.
processOrderBy($orderby, SelectStatement $selectStmt)
Process an object template.
static getLogger($name)
Get the logger with the given name.
Definition: LogManager.php:37
processObjectTemplate(PersistentObject $tpl, SelectStatement $selectStmt)
Process an object template.
Node adds the concept of relations to PersistentObject.
Definition: Node.php:34
static getInstance($name, $dynamicConfiguration=[])
UnknownFieldException signals an unknown field.
Instances of RDBOneToManyRelationDescription describe a one to many relation from 'this' end (one) to...
getQueryCondition()
Get the condition part of the query.
PagingInfo contains information about a paged list.
Definition: PagingInfo.php:18
PersistentObject defines the interface of all persistent objects.
Node related interfaces and classes.
Definition: namespaces.php:26
valueChanged(ValueChangeEvent $event)
Listen to ValueChangeEvents.
resetInternals()
Reset internal variables.
getParameters($criteria, array $parameters)
Get an array of parameter values for the given criteria.
static get(RDBMapper $mapper, $id=self::NO_CACHE)
Get the SelectStatement instance with the given id.
LogManager is used to retrieve Logger instances.
Definition: LogManager.php:20
getProperty($name)
Get the value of a named property in the object.
ObjectFactory implements the service locator pattern by wrapping a Factory instance and providing sta...
getParameterPosition($criterion, $criteria)
ObjectQuery implements a template based object query.