@@ -37,849 +37,849 @@ |
||
37 | 37 | class ContentRepository implements RepositoryInterface |
38 | 38 | { |
39 | 39 | |
40 | - /** |
|
41 | - * Tell whether it is a raw result (array) or object being returned. |
|
42 | - * |
|
43 | - * @var bool |
|
44 | - */ |
|
45 | - protected $rawResult = false; |
|
46 | - |
|
47 | - /** |
|
48 | - * The data type to be returned, e.g fe_users, fe_groups, tt_content, etc... |
|
49 | - * |
|
50 | - * @var string |
|
51 | - */ |
|
52 | - protected $dataType; |
|
53 | - |
|
54 | - /** |
|
55 | - * The source field is useful in the context of MM relations to know who is the caller |
|
56 | - * e.g findByItems which eventually corresponds to a field name. |
|
57 | - * |
|
58 | - * @var string |
|
59 | - */ |
|
60 | - protected $sourceFieldName = ''; |
|
61 | - |
|
62 | - /** |
|
63 | - * @var array |
|
64 | - */ |
|
65 | - protected $errorMessages = []; |
|
66 | - |
|
67 | - /** |
|
68 | - * @var QuerySettingsInterface |
|
69 | - */ |
|
70 | - protected $defaultQuerySettings; |
|
71 | - |
|
72 | - /** |
|
73 | - * @var DataHandler |
|
74 | - */ |
|
75 | - protected $dataHandler; |
|
76 | - |
|
77 | - /** |
|
78 | - * Constructor |
|
79 | - * |
|
80 | - * @param string $dataType |
|
81 | - */ |
|
82 | - public function __construct($dataType) |
|
83 | - { |
|
84 | - $this->dataType = $dataType; |
|
85 | - } |
|
86 | - |
|
87 | - /** |
|
88 | - * Returns all objects of this repository. |
|
89 | - * |
|
90 | - * @return Content[] |
|
91 | - */ |
|
92 | - public function findAll() |
|
93 | - { |
|
94 | - $query = $this->createQuery(); |
|
95 | - return $query->execute(); |
|
96 | - } |
|
97 | - |
|
98 | - /** |
|
99 | - * Returns all "distinct" values for a given property. |
|
100 | - * |
|
101 | - * @param string $propertyName |
|
102 | - * @param Matcher $matcher |
|
103 | - * @param Order|null $order |
|
104 | - * @return Content[] |
|
105 | - */ |
|
106 | - public function findDistinctValues($propertyName, Matcher $matcher = null, Order $order = null): array |
|
107 | - { |
|
108 | - $query = $this->createQuery(); |
|
109 | - $query->setDistinct($propertyName); |
|
110 | - |
|
111 | - // Remove empty values from selection. |
|
112 | - $constraint = $query->logicalNot($query->equals($propertyName, '')); |
|
113 | - |
|
114 | - // Add some additional constraints from the Matcher object. |
|
115 | - $matcherConstraint = null; |
|
116 | - if ($matcher !== null) { |
|
117 | - $matcherConstraint = $this->computeConstraints($query, $matcher); |
|
118 | - } |
|
119 | - |
|
120 | - // Assemble the final constraints or not. |
|
121 | - if ($matcherConstraint) { |
|
122 | - $query->logicalAnd([$matcherConstraint, $constraint]); |
|
123 | - $query->matching($query->logicalAnd([$matcherConstraint, $constraint])); |
|
124 | - } else { |
|
125 | - $query->matching($constraint); |
|
126 | - } |
|
127 | - |
|
128 | - if ($order) { |
|
129 | - $query->setOrderings($order->getOrderings()); |
|
130 | - } |
|
131 | - |
|
132 | - return $query->execute(); |
|
133 | - } |
|
134 | - |
|
135 | - /** |
|
136 | - * Returns all "distinct" values for a given property. |
|
137 | - * |
|
138 | - * @param string $propertyName |
|
139 | - * @param Matcher $matcher |
|
140 | - * @return int |
|
141 | - */ |
|
142 | - public function countDistinctValues($propertyName, Matcher $matcher = null): int |
|
143 | - { |
|
144 | - $query = $this->createQuery(); |
|
145 | - $query->setDistinct($propertyName); |
|
146 | - |
|
147 | - // Remove empty values from selection. |
|
148 | - $constraint = $query->logicalNot($query->equals($propertyName, '')); |
|
149 | - |
|
150 | - // Add some additional constraints from the Matcher object. |
|
151 | - $matcherConstraint = null; |
|
152 | - if (!is_null($matcher)) { |
|
153 | - $matcherConstraint = $this->computeConstraints($query, $matcher); |
|
154 | - } |
|
155 | - |
|
156 | - // Assemble the final constraints or not. |
|
157 | - if ($matcherConstraint) { |
|
158 | - $query->logicalAnd([$matcherConstraint, $constraint]); |
|
159 | - $query->matching($query->logicalAnd([$matcherConstraint, $constraint])); |
|
160 | - } else { |
|
161 | - $query->matching($constraint); |
|
162 | - } |
|
163 | - |
|
164 | - return $query->count(); |
|
165 | - } |
|
166 | - |
|
167 | - /** |
|
168 | - * Finds an object matching the given identifier. |
|
169 | - * |
|
170 | - * @param int $uid The identifier of the object to find |
|
171 | - * @return Content|null |
|
172 | - * @api |
|
173 | - */ |
|
174 | - public function findByUid($uid) |
|
175 | - { |
|
176 | - return $this->findByIdentifier($uid); |
|
177 | - } |
|
178 | - |
|
179 | - /** |
|
180 | - * Finds all Contents given specified matches. |
|
181 | - * |
|
182 | - * @param string $propertyName |
|
183 | - * @param array $values |
|
184 | - * @return Content[] |
|
185 | - */ |
|
186 | - public function findIn($propertyName, array $values): array |
|
187 | - { |
|
188 | - $query = $this->createQuery(); |
|
189 | - $query->matching($query->in($propertyName, $values)); |
|
190 | - return $query->execute(); |
|
191 | - } |
|
192 | - |
|
193 | - /** |
|
194 | - * Finds all Contents given specified matches. |
|
195 | - * |
|
196 | - * @param Matcher $matcher |
|
197 | - * @param Order $order The order |
|
198 | - * @param int $limit |
|
199 | - * @param int $offset |
|
200 | - * @return Content[] |
|
201 | - */ |
|
202 | - public function findBy(Matcher $matcher, Order $order = null, $limit = null, $offset = null): array |
|
203 | - { |
|
204 | - |
|
205 | - $query = $this->createQuery(); |
|
206 | - |
|
207 | - $limit = (int)$limit; // make sure to cast |
|
208 | - if ($limit > 0) { |
|
209 | - $query->setLimit($limit); |
|
210 | - } |
|
211 | - |
|
212 | - if ($order) { |
|
213 | - $query->setOrderings($order->getOrderings()); |
|
214 | - |
|
215 | - // Loops around the orderings adding if necessary a dummy condition |
|
216 | - // to make sure the relations can be resolved when transforming the query to plain SQL. |
|
217 | - foreach ($order->getOrderings() as $ordering => $direction) { |
|
218 | - if ($this->hasForeignRelationIn($ordering)) { |
|
219 | - $relationalField = $this->getForeignRelationFrom($ordering); |
|
220 | - $matcher->like($relationalField . '.uid', ''); |
|
221 | - } |
|
222 | - } |
|
223 | - } |
|
224 | - |
|
225 | - if ($offset) { |
|
226 | - $query->setOffset($offset); |
|
227 | - } |
|
228 | - |
|
229 | - $constraints = $this->computeConstraints($query, $matcher); |
|
230 | - |
|
231 | - if ($constraints) { |
|
232 | - $query->matching($constraints); |
|
233 | - } |
|
234 | - |
|
235 | - return $query->execute(); |
|
236 | - } |
|
237 | - |
|
238 | - /** |
|
239 | - * Find one Content object given specified matches. |
|
240 | - * |
|
241 | - * @param Matcher $matcher |
|
242 | - * @return Content |
|
243 | - */ |
|
244 | - public function findOneBy(Matcher $matcher): Content |
|
245 | - { |
|
246 | - |
|
247 | - $query = $this->createQuery(); |
|
248 | - |
|
249 | - $constraints = $this->computeConstraints($query, $matcher); |
|
250 | - |
|
251 | - if ($constraints) { |
|
252 | - $query->matching($constraints); |
|
253 | - } |
|
254 | - |
|
255 | - $query->setLimit(1); // only take one! |
|
256 | - |
|
257 | - $resultSet = $query->execute(); |
|
258 | - if ($resultSet) { |
|
259 | - $resultSet = current($resultSet); |
|
260 | - } |
|
261 | - return $resultSet; |
|
262 | - } |
|
263 | - |
|
264 | - /** |
|
265 | - * Count all Contents given specified matches. |
|
266 | - * |
|
267 | - * @param Matcher $matcher |
|
268 | - * @return int |
|
269 | - */ |
|
270 | - public function countBy(Matcher $matcher): int |
|
271 | - { |
|
272 | - |
|
273 | - $query = $this->createQuery(); |
|
274 | - |
|
275 | - $constraints = $this->computeConstraints($query, $matcher); |
|
276 | - |
|
277 | - if ($constraints) { |
|
278 | - $query->matching($constraints); |
|
279 | - } |
|
280 | - |
|
281 | - return $query->count(); |
|
282 | - } |
|
283 | - |
|
284 | - /** |
|
285 | - * Update a content with new information. |
|
286 | - * |
|
287 | - * @param Content $content |
|
288 | - * @param $language |
|
289 | - * @return bool |
|
290 | - */ |
|
291 | - public function localize($content, $language): bool |
|
292 | - { |
|
293 | - |
|
294 | - // Security check |
|
295 | - $this->getContentValidator()->validate($content); |
|
296 | - $this->getLanguageValidator()->validate($language); |
|
297 | - |
|
298 | - $dataType = $content->getDataType(); |
|
299 | - $handler = $this->getDataHandlerFactory()->action(ProcessAction::LOCALIZE)->forType($dataType)->getDataHandler(); |
|
300 | - |
|
301 | - $handlerResult = $handler->processLocalize($content, $language); |
|
302 | - $this->errorMessages = $handler->getErrorMessages(); |
|
303 | - return $handlerResult; |
|
304 | - } |
|
305 | - |
|
306 | - /** |
|
307 | - * Update a content with new information. |
|
308 | - * |
|
309 | - * @param Content $content |
|
310 | - * @return bool |
|
311 | - */ |
|
312 | - public function update($content) |
|
313 | - { |
|
314 | - |
|
315 | - // Security check. |
|
316 | - $this->getContentValidator()->validate($content); |
|
317 | - |
|
318 | - $dataType = $content->getDataType(); |
|
319 | - $handler = $this->getDataHandlerFactory()->action(ProcessAction::UPDATE)->forType($dataType)->getDataHandler(); |
|
320 | - |
|
321 | - $handlerResult = $handler->processUpdate($content); |
|
322 | - $this->errorMessages = $handler->getErrorMessages(); |
|
323 | - return $handlerResult; |
|
324 | - } |
|
325 | - |
|
326 | - /** |
|
327 | - * Removes an object from this repository. |
|
328 | - * |
|
329 | - * @param Content $content |
|
330 | - * @return boolean |
|
331 | - */ |
|
332 | - public function remove($content) |
|
333 | - { |
|
334 | - $dataType = $content->getDataType(); |
|
335 | - $handler = $this->getDataHandlerFactory()->action(ProcessAction::REMOVE)->forType($dataType)->getDataHandler(); |
|
336 | - |
|
337 | - $handlerResult = $handler->processRemove($content); |
|
338 | - $this->errorMessages = $handler->getErrorMessages(); |
|
339 | - return $handlerResult; |
|
340 | - } |
|
341 | - |
|
342 | - /** |
|
343 | - * Move a content within this repository. |
|
344 | - * The $target corresponds to the pid to move the records to. |
|
345 | - * It can also be a negative value in case of sorting. The negative value would be the uid of its predecessor. |
|
346 | - * |
|
347 | - * @param Content $content |
|
348 | - * @param string $target |
|
349 | - * @return bool |
|
350 | - */ |
|
351 | - public function move($content, $target): bool |
|
352 | - { |
|
353 | - |
|
354 | - // Security check. |
|
355 | - $this->getContentValidator()->validate($content); |
|
356 | - |
|
357 | - $dataType = $content->getDataType(); |
|
358 | - $handler = $this->getDataHandlerFactory()->action(ProcessAction::MOVE)->forType($dataType)->getDataHandler(); |
|
359 | - |
|
360 | - $handlerResult = $handler->processMove($content, $target); |
|
361 | - $this->errorMessages = $handler->getErrorMessages(); |
|
362 | - return $handlerResult; |
|
363 | - } |
|
364 | - |
|
365 | - /** |
|
366 | - * Copy a content within this repository. |
|
367 | - * |
|
368 | - * @param Content $content |
|
369 | - * @return bool |
|
370 | - */ |
|
371 | - public function copy($content, $target): bool |
|
372 | - { |
|
373 | - |
|
374 | - // Security check. |
|
375 | - $this->getContentValidator()->validate($content); |
|
376 | - |
|
377 | - $dataType = $content->getDataType(); |
|
378 | - $handler = $this->getDataHandlerFactory()->action(ProcessAction::COPY)->forType($dataType)->getDataHandler(); |
|
379 | - |
|
380 | - $handlerResult = $handler->processCopy($content, $target); |
|
381 | - $this->errorMessages = $handler->getErrorMessages(); |
|
382 | - return $handlerResult; |
|
383 | - } |
|
384 | - |
|
385 | - /** |
|
386 | - * Adds an object to this repository. |
|
387 | - * |
|
388 | - * @param object $object The object to add |
|
389 | - * @return void |
|
390 | - * @api |
|
391 | - */ |
|
392 | - public function add($object) |
|
393 | - { |
|
394 | - throw new \BadMethodCallException('Repository does not support the add() method.', 1375805599); |
|
395 | - } |
|
396 | - |
|
397 | - /** |
|
398 | - * Returns the total number objects of this repository. |
|
399 | - * |
|
400 | - * @return integer The object count |
|
401 | - * @api |
|
402 | - */ |
|
403 | - public function countAll() |
|
404 | - { |
|
405 | - $query = $this->createQuery(); |
|
406 | - return $query->count(); |
|
407 | - } |
|
408 | - |
|
409 | - /** |
|
410 | - * Removes all objects of this repository as if remove() was called for |
|
411 | - * all of them. |
|
412 | - * |
|
413 | - * @return void |
|
414 | - * @api |
|
415 | - */ |
|
416 | - public function removeAll() |
|
417 | - { |
|
418 | - // TODO: Implement removeAll() method. |
|
419 | - } |
|
420 | - |
|
421 | - /** |
|
422 | - * Finds an object matching the given identifier. |
|
423 | - * |
|
424 | - * @param mixed $identifier The identifier of the object to find |
|
425 | - * @return Content|null |
|
426 | - * @api |
|
427 | - */ |
|
428 | - public function findByIdentifier($identifier) |
|
429 | - { |
|
430 | - $query = $this->createQuery(); |
|
431 | - |
|
432 | - $result = $query->matching( |
|
433 | - $query->equals('uid', $identifier) |
|
434 | - ) |
|
435 | - ->execute(); |
|
436 | - |
|
437 | - if (is_array($result)) { |
|
438 | - $result = current($result); |
|
439 | - } |
|
440 | - |
|
441 | - return $result; |
|
442 | - } |
|
443 | - |
|
444 | - /** |
|
445 | - * Dispatches magic methods (findBy[Property]()) |
|
446 | - * |
|
447 | - * @param string $methodName The name of the magic method |
|
448 | - * @param string $arguments The arguments of the magic method |
|
449 | - * @return mixed |
|
450 | - * @api |
|
451 | - */ |
|
452 | - public function __call($methodName, $arguments) |
|
453 | - { |
|
454 | - if (substr($methodName, 0, 6) === 'findBy' && strlen($methodName) > 7) { |
|
455 | - $propertyName = strtolower(substr(substr($methodName, 6), 0, 1)) . substr(substr($methodName, 6), 1); |
|
456 | - $result = $this->processMagicCall($propertyName, $arguments[0]); |
|
457 | - } elseif (substr($methodName, 0, 9) === 'findOneBy' && strlen($methodName) > 10) { |
|
458 | - $propertyName = strtolower(substr(substr($methodName, 9), 0, 1)) . substr(substr($methodName, 9), 1); |
|
459 | - $result = $this->processMagicCall($propertyName, $arguments[0], 'one'); |
|
460 | - } elseif (substr($methodName, 0, 7) === 'countBy' && strlen($methodName) > 8) { |
|
461 | - $propertyName = strtolower(substr(substr($methodName, 7), 0, 1)) . substr(substr($methodName, 7), 1); |
|
462 | - $result = $this->processMagicCall($propertyName, $arguments[0], 'count'); |
|
463 | - } else { |
|
464 | - throw new UnsupportedMethodException('The method "' . $methodName . '" is not supported by the repository.', 1360838010); |
|
465 | - } |
|
466 | - return $result; |
|
467 | - } |
|
468 | - |
|
469 | - /** |
|
470 | - * Returns a query for objects of this repository |
|
471 | - * |
|
472 | - * @return Query |
|
473 | - * @api |
|
474 | - */ |
|
475 | - public function createQuery() |
|
476 | - { |
|
477 | - /** @var Query $query */ |
|
478 | - $query = GeneralUtility::makeInstance(Query::class, $this->dataType); |
|
479 | - $query->setSourceFieldName($this->sourceFieldName); |
|
480 | - |
|
481 | - if ($this->defaultQuerySettings) { |
|
482 | - $query->setTypo3QuerySettings($this->defaultQuerySettings); |
|
483 | - } else { |
|
484 | - |
|
485 | - // Initialize and pass the query settings at this level. |
|
486 | - $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class); |
|
487 | - |
|
488 | - // Default choice for the BE. |
|
489 | - if ($this->isBackendMode()) { |
|
490 | - $querySettings->setIgnoreEnableFields(true); |
|
491 | - } |
|
492 | - |
|
493 | - $query->setTypo3QuerySettings($querySettings); |
|
494 | - } |
|
495 | - |
|
496 | - return $query; |
|
497 | - } |
|
498 | - |
|
499 | - /** |
|
500 | - * Sets the property names to order the result by per default. |
|
501 | - * Expected like this: |
|
502 | - * array( |
|
503 | - * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING, |
|
504 | - * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING |
|
505 | - * ) |
|
506 | - * |
|
507 | - * @param array $defaultOrderings The property names to order by |
|
508 | - * @return void |
|
509 | - * @api |
|
510 | - */ |
|
511 | - public function setDefaultOrderings(array $defaultOrderings) |
|
512 | - { |
|
513 | - throw new \BadMethodCallException('Repository does not support the setDefaultOrderings() method.', 1375805598); |
|
514 | - } |
|
515 | - |
|
516 | - /** |
|
517 | - * Sets the default query settings to be used in this repository |
|
518 | - * |
|
519 | - * @param QuerySettingsInterface $defaultQuerySettings The query settings to be used by default |
|
520 | - * @return void |
|
521 | - * @api |
|
522 | - */ |
|
523 | - public function setDefaultQuerySettings(QuerySettingsInterface $defaultQuerySettings) |
|
524 | - { |
|
525 | - $this->defaultQuerySettings = $defaultQuerySettings; |
|
526 | - } |
|
527 | - |
|
528 | - /** |
|
529 | - * @return void |
|
530 | - */ |
|
531 | - public function resetDefaultQuerySettings(): void |
|
532 | - { |
|
533 | - $this->defaultQuerySettings = null; |
|
534 | - } |
|
535 | - |
|
536 | - |
|
537 | - /** |
|
538 | - * @return array |
|
539 | - */ |
|
540 | - public function getErrorMessages(): array |
|
541 | - { |
|
542 | - return $this->errorMessages; |
|
543 | - } |
|
544 | - |
|
545 | - /** |
|
546 | - * @param string $sourceFieldName |
|
547 | - * @return $this |
|
548 | - */ |
|
549 | - public function setSourceFieldName($sourceFieldName): self |
|
550 | - { |
|
551 | - $this->sourceFieldName = $sourceFieldName; |
|
552 | - return $this; |
|
553 | - } |
|
554 | - |
|
555 | - /** |
|
556 | - * @return string |
|
557 | - */ |
|
558 | - public function getDataType(): string |
|
559 | - { |
|
560 | - return $this->dataType; |
|
561 | - } |
|
562 | - |
|
563 | - /** |
|
564 | - * Tell whether the order has a foreign table in its expression, e.g. "metadata.title". |
|
565 | - * |
|
566 | - * @param string $ordering |
|
567 | - * @return bool |
|
568 | - */ |
|
569 | - protected function hasForeignRelationIn($ordering): bool |
|
570 | - { |
|
571 | - return strpos($ordering, '.') !== false; |
|
572 | - } |
|
573 | - |
|
574 | - /** |
|
575 | - * Extract the foreign relation of the ordering "metadata.title" -> "metadata" |
|
576 | - * |
|
577 | - * @param string $ordering |
|
578 | - * @return string |
|
579 | - */ |
|
580 | - protected function getForeignRelationFrom($ordering): string |
|
581 | - { |
|
582 | - $parts = explode('.', $ordering); |
|
583 | - return $parts[0]; |
|
584 | - } |
|
585 | - |
|
586 | - /** |
|
587 | - * Get the constraints |
|
588 | - * |
|
589 | - * @param Query $query |
|
590 | - * @param Matcher $matcher |
|
591 | - * @return ConstraintInterface|null |
|
592 | - */ |
|
593 | - protected function computeConstraints(Query $query, Matcher $matcher): ?ConstraintInterface |
|
594 | - { |
|
595 | - $constraints = null; |
|
596 | - |
|
597 | - $collectedConstraints = []; |
|
598 | - |
|
599 | - // Search term |
|
600 | - $constraint = $this->computeSearchTermConstraint($query, $matcher); |
|
601 | - if ($constraint) { |
|
602 | - $collectedConstraints[] = $constraint; |
|
603 | - } |
|
604 | - |
|
605 | - foreach ($matcher->getSupportedOperators() as $operator) { |
|
606 | - $constraint = $this->computeConstraint($query, $matcher, $operator); |
|
607 | - if ($constraint) { |
|
608 | - $collectedConstraints[] = $constraint; |
|
609 | - } |
|
610 | - } |
|
611 | - |
|
612 | - if (count($collectedConstraints) > 1) { |
|
613 | - $logical = $matcher->getDefaultLogicalSeparator(); |
|
614 | - $constraints = $query->$logical($collectedConstraints); |
|
615 | - } elseif (!empty($collectedConstraints)) { |
|
616 | - |
|
617 | - // true means there is one constraint only and should become the result |
|
618 | - $constraints = current($collectedConstraints); |
|
619 | - } |
|
620 | - |
|
621 | - // Trigger signal for post processing the computed constraints object. |
|
622 | - $constraints = $this->emitPostProcessConstraintsSignal($query, $constraints); |
|
623 | - |
|
624 | - return $constraints; |
|
625 | - } |
|
626 | - |
|
627 | - /** |
|
628 | - * Computes the search constraint and returns it. |
|
629 | - * |
|
630 | - * @param Query $query |
|
631 | - * @param Matcher $matcher |
|
632 | - * @return ConstraintInterface|null |
|
633 | - */ |
|
634 | - protected function computeSearchTermConstraint(Query $query, Matcher $matcher): ?ConstraintInterface |
|
635 | - { |
|
636 | - |
|
637 | - $result = null; |
|
638 | - |
|
639 | - // Search term case |
|
640 | - if ($matcher->getSearchTerm()) { |
|
641 | - |
|
642 | - $fields = GeneralUtility::trimExplode(',', Tca::table($this->dataType)->getSearchFields(), true); |
|
643 | - |
|
644 | - $constraints = []; |
|
645 | - $likeClause = sprintf('%%%s%%', $matcher->getSearchTerm()); |
|
646 | - foreach ($fields as $fieldNameAndPath) { |
|
647 | - if ($this->isSuitableForLike($fieldNameAndPath, $matcher->getSearchTerm())) { |
|
648 | - |
|
649 | - $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->dataType); |
|
650 | - $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $this->dataType); |
|
651 | - |
|
652 | - if (Tca::table($dataType)->hasField($fieldName) && Tca::table($dataType)->field($fieldName)->hasRelation()) { |
|
653 | - $foreignTable = Tca::table($dataType)->field($fieldName)->getForeignTable(); |
|
654 | - $fieldNameAndPath = $fieldNameAndPath . '.' . Tca::table($foreignTable)->getLabelField(); |
|
655 | - } |
|
656 | - $constraints[] = $query->like($fieldNameAndPath, $likeClause); |
|
657 | - } |
|
658 | - } |
|
659 | - $logical = $matcher->getLogicalSeparatorForSearchTerm(); |
|
660 | - $result = $query->$logical($constraints); |
|
661 | - } |
|
662 | - |
|
663 | - return $result; |
|
664 | - } |
|
665 | - |
|
666 | - /** |
|
667 | - * It does not make sense to have a "like" in presence of numerical field, e.g "uid". |
|
668 | - * Tell whether the given value makes sense for a "like" clause. |
|
669 | - * |
|
670 | - * @param string $fieldNameAndPath |
|
671 | - * @param string $value |
|
672 | - * @return bool |
|
673 | - */ |
|
674 | - protected function isSuitableForLike($fieldNameAndPath, $value): bool |
|
675 | - { |
|
676 | - $isSuitable = true; |
|
677 | - |
|
678 | - // true means it is a string |
|
679 | - if (!MathUtility::canBeInterpretedAsInteger($value)) { |
|
680 | - |
|
681 | - $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->dataType); |
|
682 | - $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $this->dataType); |
|
683 | - |
|
684 | - if (Tca::table($dataType)->field($fieldName)->isNumerical() |
|
685 | - && !Tca::table($dataType)->field($fieldName)->hasRelation() |
|
686 | - ) { |
|
687 | - $isSuitable = false; |
|
688 | - } |
|
689 | - } |
|
690 | - |
|
691 | - return $isSuitable; |
|
692 | - } |
|
693 | - |
|
694 | - /** |
|
695 | - * Computes the constraint for matches and returns it. |
|
696 | - * |
|
697 | - * @param Query $query |
|
698 | - * @param Matcher $matcher |
|
699 | - * @param string $operator |
|
700 | - * @return ConstraintInterface|null |
|
701 | - */ |
|
702 | - protected function computeConstraint(Query $query, Matcher $matcher, $operator): ?ConstraintInterface |
|
703 | - { |
|
704 | - $result = null; |
|
705 | - |
|
706 | - $operatorName = ucfirst($operator); |
|
707 | - $getCriteria = sprintf('get%s', $operatorName); |
|
708 | - $criteria = $matcher->$getCriteria(); |
|
709 | - |
|
710 | - if (!empty($criteria)) { |
|
711 | - $constraints = []; |
|
712 | - |
|
713 | - foreach ($criteria as $criterion) { |
|
714 | - |
|
715 | - $fieldNameAndPath = $criterion['fieldNameAndPath']; |
|
716 | - $operand = $criterion['operand']; |
|
717 | - |
|
718 | - // Compute a few variables... |
|
719 | - // $dataType is generally equals to $this->dataType but not always... if fieldName is a path. |
|
720 | - $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->dataType); |
|
721 | - $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $this->dataType); |
|
722 | - $fieldPath = $this->getFieldPathResolver()->stripFieldName($fieldNameAndPath, $this->dataType); |
|
723 | - |
|
724 | - if (Tca::table($dataType)->field($fieldName)->hasRelation()) { |
|
725 | - if (MathUtility::canBeInterpretedAsInteger($operand)) { |
|
726 | - $fieldNameAndPath = $fieldName . '.uid'; |
|
727 | - } else { |
|
728 | - $foreignTableName = Tca::table($dataType)->field($fieldName)->getForeignTable(); |
|
729 | - $foreignTable = Tca::table($foreignTableName); |
|
730 | - $fieldNameAndPath = $fieldName . '.' . $foreignTable->getLabelField(); |
|
731 | - } |
|
732 | - |
|
733 | - // If different means we should restore the prepended path segment for proper SQL parser. |
|
734 | - // This is true for a composite field, e.g items.sys_file_metadata for categories. |
|
735 | - if ($fieldName !== $fieldPath) { |
|
736 | - $fieldNameAndPath = $fieldPath . '.' . $fieldNameAndPath; |
|
737 | - } |
|
738 | - } |
|
739 | - |
|
740 | - if (strpos($operator, 'not') === 0) { |
|
741 | - $strippedOperator = strtolower(substr($operator, 3)); |
|
742 | - $constraints[] = $query->logicalNot($query->$strippedOperator($fieldNameAndPath, $criterion['operand'])); |
|
743 | - } else { |
|
744 | - $constraints[] = $query->$operator($fieldNameAndPath, $criterion['operand']); |
|
745 | - } |
|
746 | - } |
|
747 | - |
|
748 | - $getLogicalSeparator = sprintf('getLogicalSeparatorFor%s', $operatorName); |
|
749 | - $logical = method_exists($matcher, $getLogicalSeparator) |
|
750 | - ? $matcher->$getLogicalSeparator() |
|
751 | - : $matcher->getDefaultLogicalSeparator(); |
|
752 | - |
|
753 | - $result = $query->$logical($constraints); |
|
754 | - } |
|
755 | - |
|
756 | - return $result; |
|
757 | - } |
|
758 | - |
|
759 | - /** |
|
760 | - * @return DataHandler |
|
761 | - */ |
|
762 | - protected function getDataHandler(): DataHandler |
|
763 | - { |
|
764 | - if (!$this->dataHandler) { |
|
765 | - $this->dataHandler = GeneralUtility::makeInstance(DataHandler::class); |
|
766 | - } |
|
767 | - return $this->dataHandler; |
|
768 | - } |
|
769 | - |
|
770 | - /** |
|
771 | - * Handle the magic call by properly creating a Query object and returning its result. |
|
772 | - * |
|
773 | - * @param string $propertyName |
|
774 | - * @param string $value |
|
775 | - * @param string $flag |
|
776 | - * @return mixed |
|
777 | - */ |
|
778 | - protected function processMagicCall($propertyName, $value, $flag = '') |
|
779 | - { |
|
780 | - |
|
781 | - $fieldName = Property::name($propertyName)->of($this->dataType)->toFieldName(); |
|
782 | - |
|
783 | - /** @var $matcher Matcher */ |
|
784 | - $matcher = GeneralUtility::makeInstance(Matcher::class, [], $this->getDataType()); |
|
785 | - |
|
786 | - $table = Tca::table($this->dataType); |
|
787 | - if ($table->field($fieldName)->isGroup()) { |
|
788 | - |
|
789 | - $valueParts = explode('.', $value, 2); |
|
790 | - $fieldName = $fieldName . '.' . $valueParts[0]; |
|
791 | - $value = $valueParts[1]; |
|
792 | - } |
|
793 | - |
|
794 | - $matcher->equals($fieldName, $value); |
|
795 | - |
|
796 | - if ($flag === 'count') { |
|
797 | - $result = $this->countBy($matcher); |
|
798 | - } else { |
|
799 | - $result = $this->findBy($matcher); |
|
800 | - } |
|
801 | - return $flag === 'one' && !empty($result) ? reset($result) : $result; |
|
802 | - } |
|
803 | - |
|
804 | - /** |
|
805 | - * @return DataHandlerFactory|object |
|
806 | - */ |
|
807 | - protected function getDataHandlerFactory() |
|
808 | - { |
|
809 | - return GeneralUtility::makeInstance(DataHandlerFactory::class); |
|
810 | - } |
|
811 | - |
|
812 | - /** |
|
813 | - * Returns whether the current mode is Backend |
|
814 | - * |
|
815 | - * @return bool |
|
816 | - */ |
|
817 | - protected function isBackendMode(): bool |
|
818 | - { |
|
819 | - return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); |
|
820 | - } |
|
821 | - |
|
822 | - /** |
|
823 | - * @return FieldPathResolver|object |
|
824 | - */ |
|
825 | - protected function getFieldPathResolver() |
|
826 | - { |
|
827 | - return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
828 | - } |
|
829 | - |
|
830 | - /** |
|
831 | - * @return ContentValidator|object |
|
832 | - */ |
|
833 | - protected function getContentValidator(): ContentValidator |
|
834 | - { |
|
835 | - return GeneralUtility::makeInstance(ContentValidator::class); |
|
836 | - } |
|
837 | - |
|
838 | - /** |
|
839 | - * @return LanguageValidator|object |
|
840 | - */ |
|
841 | - protected function getLanguageValidator(): LanguageValidator |
|
842 | - { |
|
843 | - return GeneralUtility::makeInstance(LanguageValidator::class); |
|
844 | - } |
|
845 | - |
|
846 | - /** |
|
847 | - * Signal that is called for post-processing the computed constraints object. |
|
848 | - * |
|
849 | - * @param Query $query |
|
850 | - * @param ConstraintInterface|null $constraints |
|
851 | - * @return ConstraintInterface|null $constraints |
|
852 | - */ |
|
853 | - protected function emitPostProcessConstraintsSignal(Query $query, $constraints): ?ConstraintInterface |
|
854 | - { |
|
855 | - /** @var ConstraintContainer $constraintContainer */ |
|
856 | - $constraintContainer = GeneralUtility::makeInstance(ConstraintContainer::class); |
|
857 | - $result = $this->getSignalSlotDispatcher()->dispatch( |
|
858 | - self::class, |
|
859 | - 'postProcessConstraintsObject', |
|
860 | - [ |
|
861 | - $query, |
|
862 | - $constraints, |
|
863 | - $constraintContainer |
|
864 | - ] |
|
865 | - ); |
|
866 | - |
|
867 | - // Backward compatibility. |
|
868 | - $processedConstraints = $result[1]; |
|
869 | - |
|
870 | - // New way to transmit the constraints. |
|
871 | - if ($constraintContainer->getConstraint()) { |
|
872 | - $processedConstraints = $constraintContainer->getConstraint(); |
|
873 | - } |
|
874 | - return $processedConstraints; |
|
875 | - } |
|
876 | - |
|
877 | - /** |
|
878 | - * @return Dispatcher |
|
879 | - */ |
|
880 | - protected function getSignalSlotDispatcher(): Dispatcher |
|
881 | - { |
|
882 | - return GeneralUtility::makeInstance(Dispatcher::class); |
|
883 | - } |
|
40 | + /** |
|
41 | + * Tell whether it is a raw result (array) or object being returned. |
|
42 | + * |
|
43 | + * @var bool |
|
44 | + */ |
|
45 | + protected $rawResult = false; |
|
46 | + |
|
47 | + /** |
|
48 | + * The data type to be returned, e.g fe_users, fe_groups, tt_content, etc... |
|
49 | + * |
|
50 | + * @var string |
|
51 | + */ |
|
52 | + protected $dataType; |
|
53 | + |
|
54 | + /** |
|
55 | + * The source field is useful in the context of MM relations to know who is the caller |
|
56 | + * e.g findByItems which eventually corresponds to a field name. |
|
57 | + * |
|
58 | + * @var string |
|
59 | + */ |
|
60 | + protected $sourceFieldName = ''; |
|
61 | + |
|
62 | + /** |
|
63 | + * @var array |
|
64 | + */ |
|
65 | + protected $errorMessages = []; |
|
66 | + |
|
67 | + /** |
|
68 | + * @var QuerySettingsInterface |
|
69 | + */ |
|
70 | + protected $defaultQuerySettings; |
|
71 | + |
|
72 | + /** |
|
73 | + * @var DataHandler |
|
74 | + */ |
|
75 | + protected $dataHandler; |
|
76 | + |
|
77 | + /** |
|
78 | + * Constructor |
|
79 | + * |
|
80 | + * @param string $dataType |
|
81 | + */ |
|
82 | + public function __construct($dataType) |
|
83 | + { |
|
84 | + $this->dataType = $dataType; |
|
85 | + } |
|
86 | + |
|
87 | + /** |
|
88 | + * Returns all objects of this repository. |
|
89 | + * |
|
90 | + * @return Content[] |
|
91 | + */ |
|
92 | + public function findAll() |
|
93 | + { |
|
94 | + $query = $this->createQuery(); |
|
95 | + return $query->execute(); |
|
96 | + } |
|
97 | + |
|
98 | + /** |
|
99 | + * Returns all "distinct" values for a given property. |
|
100 | + * |
|
101 | + * @param string $propertyName |
|
102 | + * @param Matcher $matcher |
|
103 | + * @param Order|null $order |
|
104 | + * @return Content[] |
|
105 | + */ |
|
106 | + public function findDistinctValues($propertyName, Matcher $matcher = null, Order $order = null): array |
|
107 | + { |
|
108 | + $query = $this->createQuery(); |
|
109 | + $query->setDistinct($propertyName); |
|
110 | + |
|
111 | + // Remove empty values from selection. |
|
112 | + $constraint = $query->logicalNot($query->equals($propertyName, '')); |
|
113 | + |
|
114 | + // Add some additional constraints from the Matcher object. |
|
115 | + $matcherConstraint = null; |
|
116 | + if ($matcher !== null) { |
|
117 | + $matcherConstraint = $this->computeConstraints($query, $matcher); |
|
118 | + } |
|
119 | + |
|
120 | + // Assemble the final constraints or not. |
|
121 | + if ($matcherConstraint) { |
|
122 | + $query->logicalAnd([$matcherConstraint, $constraint]); |
|
123 | + $query->matching($query->logicalAnd([$matcherConstraint, $constraint])); |
|
124 | + } else { |
|
125 | + $query->matching($constraint); |
|
126 | + } |
|
127 | + |
|
128 | + if ($order) { |
|
129 | + $query->setOrderings($order->getOrderings()); |
|
130 | + } |
|
131 | + |
|
132 | + return $query->execute(); |
|
133 | + } |
|
134 | + |
|
135 | + /** |
|
136 | + * Returns all "distinct" values for a given property. |
|
137 | + * |
|
138 | + * @param string $propertyName |
|
139 | + * @param Matcher $matcher |
|
140 | + * @return int |
|
141 | + */ |
|
142 | + public function countDistinctValues($propertyName, Matcher $matcher = null): int |
|
143 | + { |
|
144 | + $query = $this->createQuery(); |
|
145 | + $query->setDistinct($propertyName); |
|
146 | + |
|
147 | + // Remove empty values from selection. |
|
148 | + $constraint = $query->logicalNot($query->equals($propertyName, '')); |
|
149 | + |
|
150 | + // Add some additional constraints from the Matcher object. |
|
151 | + $matcherConstraint = null; |
|
152 | + if (!is_null($matcher)) { |
|
153 | + $matcherConstraint = $this->computeConstraints($query, $matcher); |
|
154 | + } |
|
155 | + |
|
156 | + // Assemble the final constraints or not. |
|
157 | + if ($matcherConstraint) { |
|
158 | + $query->logicalAnd([$matcherConstraint, $constraint]); |
|
159 | + $query->matching($query->logicalAnd([$matcherConstraint, $constraint])); |
|
160 | + } else { |
|
161 | + $query->matching($constraint); |
|
162 | + } |
|
163 | + |
|
164 | + return $query->count(); |
|
165 | + } |
|
166 | + |
|
167 | + /** |
|
168 | + * Finds an object matching the given identifier. |
|
169 | + * |
|
170 | + * @param int $uid The identifier of the object to find |
|
171 | + * @return Content|null |
|
172 | + * @api |
|
173 | + */ |
|
174 | + public function findByUid($uid) |
|
175 | + { |
|
176 | + return $this->findByIdentifier($uid); |
|
177 | + } |
|
178 | + |
|
179 | + /** |
|
180 | + * Finds all Contents given specified matches. |
|
181 | + * |
|
182 | + * @param string $propertyName |
|
183 | + * @param array $values |
|
184 | + * @return Content[] |
|
185 | + */ |
|
186 | + public function findIn($propertyName, array $values): array |
|
187 | + { |
|
188 | + $query = $this->createQuery(); |
|
189 | + $query->matching($query->in($propertyName, $values)); |
|
190 | + return $query->execute(); |
|
191 | + } |
|
192 | + |
|
193 | + /** |
|
194 | + * Finds all Contents given specified matches. |
|
195 | + * |
|
196 | + * @param Matcher $matcher |
|
197 | + * @param Order $order The order |
|
198 | + * @param int $limit |
|
199 | + * @param int $offset |
|
200 | + * @return Content[] |
|
201 | + */ |
|
202 | + public function findBy(Matcher $matcher, Order $order = null, $limit = null, $offset = null): array |
|
203 | + { |
|
204 | + |
|
205 | + $query = $this->createQuery(); |
|
206 | + |
|
207 | + $limit = (int)$limit; // make sure to cast |
|
208 | + if ($limit > 0) { |
|
209 | + $query->setLimit($limit); |
|
210 | + } |
|
211 | + |
|
212 | + if ($order) { |
|
213 | + $query->setOrderings($order->getOrderings()); |
|
214 | + |
|
215 | + // Loops around the orderings adding if necessary a dummy condition |
|
216 | + // to make sure the relations can be resolved when transforming the query to plain SQL. |
|
217 | + foreach ($order->getOrderings() as $ordering => $direction) { |
|
218 | + if ($this->hasForeignRelationIn($ordering)) { |
|
219 | + $relationalField = $this->getForeignRelationFrom($ordering); |
|
220 | + $matcher->like($relationalField . '.uid', ''); |
|
221 | + } |
|
222 | + } |
|
223 | + } |
|
224 | + |
|
225 | + if ($offset) { |
|
226 | + $query->setOffset($offset); |
|
227 | + } |
|
228 | + |
|
229 | + $constraints = $this->computeConstraints($query, $matcher); |
|
230 | + |
|
231 | + if ($constraints) { |
|
232 | + $query->matching($constraints); |
|
233 | + } |
|
234 | + |
|
235 | + return $query->execute(); |
|
236 | + } |
|
237 | + |
|
238 | + /** |
|
239 | + * Find one Content object given specified matches. |
|
240 | + * |
|
241 | + * @param Matcher $matcher |
|
242 | + * @return Content |
|
243 | + */ |
|
244 | + public function findOneBy(Matcher $matcher): Content |
|
245 | + { |
|
246 | + |
|
247 | + $query = $this->createQuery(); |
|
248 | + |
|
249 | + $constraints = $this->computeConstraints($query, $matcher); |
|
250 | + |
|
251 | + if ($constraints) { |
|
252 | + $query->matching($constraints); |
|
253 | + } |
|
254 | + |
|
255 | + $query->setLimit(1); // only take one! |
|
256 | + |
|
257 | + $resultSet = $query->execute(); |
|
258 | + if ($resultSet) { |
|
259 | + $resultSet = current($resultSet); |
|
260 | + } |
|
261 | + return $resultSet; |
|
262 | + } |
|
263 | + |
|
264 | + /** |
|
265 | + * Count all Contents given specified matches. |
|
266 | + * |
|
267 | + * @param Matcher $matcher |
|
268 | + * @return int |
|
269 | + */ |
|
270 | + public function countBy(Matcher $matcher): int |
|
271 | + { |
|
272 | + |
|
273 | + $query = $this->createQuery(); |
|
274 | + |
|
275 | + $constraints = $this->computeConstraints($query, $matcher); |
|
276 | + |
|
277 | + if ($constraints) { |
|
278 | + $query->matching($constraints); |
|
279 | + } |
|
280 | + |
|
281 | + return $query->count(); |
|
282 | + } |
|
283 | + |
|
284 | + /** |
|
285 | + * Update a content with new information. |
|
286 | + * |
|
287 | + * @param Content $content |
|
288 | + * @param $language |
|
289 | + * @return bool |
|
290 | + */ |
|
291 | + public function localize($content, $language): bool |
|
292 | + { |
|
293 | + |
|
294 | + // Security check |
|
295 | + $this->getContentValidator()->validate($content); |
|
296 | + $this->getLanguageValidator()->validate($language); |
|
297 | + |
|
298 | + $dataType = $content->getDataType(); |
|
299 | + $handler = $this->getDataHandlerFactory()->action(ProcessAction::LOCALIZE)->forType($dataType)->getDataHandler(); |
|
300 | + |
|
301 | + $handlerResult = $handler->processLocalize($content, $language); |
|
302 | + $this->errorMessages = $handler->getErrorMessages(); |
|
303 | + return $handlerResult; |
|
304 | + } |
|
305 | + |
|
306 | + /** |
|
307 | + * Update a content with new information. |
|
308 | + * |
|
309 | + * @param Content $content |
|
310 | + * @return bool |
|
311 | + */ |
|
312 | + public function update($content) |
|
313 | + { |
|
314 | + |
|
315 | + // Security check. |
|
316 | + $this->getContentValidator()->validate($content); |
|
317 | + |
|
318 | + $dataType = $content->getDataType(); |
|
319 | + $handler = $this->getDataHandlerFactory()->action(ProcessAction::UPDATE)->forType($dataType)->getDataHandler(); |
|
320 | + |
|
321 | + $handlerResult = $handler->processUpdate($content); |
|
322 | + $this->errorMessages = $handler->getErrorMessages(); |
|
323 | + return $handlerResult; |
|
324 | + } |
|
325 | + |
|
326 | + /** |
|
327 | + * Removes an object from this repository. |
|
328 | + * |
|
329 | + * @param Content $content |
|
330 | + * @return boolean |
|
331 | + */ |
|
332 | + public function remove($content) |
|
333 | + { |
|
334 | + $dataType = $content->getDataType(); |
|
335 | + $handler = $this->getDataHandlerFactory()->action(ProcessAction::REMOVE)->forType($dataType)->getDataHandler(); |
|
336 | + |
|
337 | + $handlerResult = $handler->processRemove($content); |
|
338 | + $this->errorMessages = $handler->getErrorMessages(); |
|
339 | + return $handlerResult; |
|
340 | + } |
|
341 | + |
|
342 | + /** |
|
343 | + * Move a content within this repository. |
|
344 | + * The $target corresponds to the pid to move the records to. |
|
345 | + * It can also be a negative value in case of sorting. The negative value would be the uid of its predecessor. |
|
346 | + * |
|
347 | + * @param Content $content |
|
348 | + * @param string $target |
|
349 | + * @return bool |
|
350 | + */ |
|
351 | + public function move($content, $target): bool |
|
352 | + { |
|
353 | + |
|
354 | + // Security check. |
|
355 | + $this->getContentValidator()->validate($content); |
|
356 | + |
|
357 | + $dataType = $content->getDataType(); |
|
358 | + $handler = $this->getDataHandlerFactory()->action(ProcessAction::MOVE)->forType($dataType)->getDataHandler(); |
|
359 | + |
|
360 | + $handlerResult = $handler->processMove($content, $target); |
|
361 | + $this->errorMessages = $handler->getErrorMessages(); |
|
362 | + return $handlerResult; |
|
363 | + } |
|
364 | + |
|
365 | + /** |
|
366 | + * Copy a content within this repository. |
|
367 | + * |
|
368 | + * @param Content $content |
|
369 | + * @return bool |
|
370 | + */ |
|
371 | + public function copy($content, $target): bool |
|
372 | + { |
|
373 | + |
|
374 | + // Security check. |
|
375 | + $this->getContentValidator()->validate($content); |
|
376 | + |
|
377 | + $dataType = $content->getDataType(); |
|
378 | + $handler = $this->getDataHandlerFactory()->action(ProcessAction::COPY)->forType($dataType)->getDataHandler(); |
|
379 | + |
|
380 | + $handlerResult = $handler->processCopy($content, $target); |
|
381 | + $this->errorMessages = $handler->getErrorMessages(); |
|
382 | + return $handlerResult; |
|
383 | + } |
|
384 | + |
|
385 | + /** |
|
386 | + * Adds an object to this repository. |
|
387 | + * |
|
388 | + * @param object $object The object to add |
|
389 | + * @return void |
|
390 | + * @api |
|
391 | + */ |
|
392 | + public function add($object) |
|
393 | + { |
|
394 | + throw new \BadMethodCallException('Repository does not support the add() method.', 1375805599); |
|
395 | + } |
|
396 | + |
|
397 | + /** |
|
398 | + * Returns the total number objects of this repository. |
|
399 | + * |
|
400 | + * @return integer The object count |
|
401 | + * @api |
|
402 | + */ |
|
403 | + public function countAll() |
|
404 | + { |
|
405 | + $query = $this->createQuery(); |
|
406 | + return $query->count(); |
|
407 | + } |
|
408 | + |
|
409 | + /** |
|
410 | + * Removes all objects of this repository as if remove() was called for |
|
411 | + * all of them. |
|
412 | + * |
|
413 | + * @return void |
|
414 | + * @api |
|
415 | + */ |
|
416 | + public function removeAll() |
|
417 | + { |
|
418 | + // TODO: Implement removeAll() method. |
|
419 | + } |
|
420 | + |
|
421 | + /** |
|
422 | + * Finds an object matching the given identifier. |
|
423 | + * |
|
424 | + * @param mixed $identifier The identifier of the object to find |
|
425 | + * @return Content|null |
|
426 | + * @api |
|
427 | + */ |
|
428 | + public function findByIdentifier($identifier) |
|
429 | + { |
|
430 | + $query = $this->createQuery(); |
|
431 | + |
|
432 | + $result = $query->matching( |
|
433 | + $query->equals('uid', $identifier) |
|
434 | + ) |
|
435 | + ->execute(); |
|
436 | + |
|
437 | + if (is_array($result)) { |
|
438 | + $result = current($result); |
|
439 | + } |
|
440 | + |
|
441 | + return $result; |
|
442 | + } |
|
443 | + |
|
444 | + /** |
|
445 | + * Dispatches magic methods (findBy[Property]()) |
|
446 | + * |
|
447 | + * @param string $methodName The name of the magic method |
|
448 | + * @param string $arguments The arguments of the magic method |
|
449 | + * @return mixed |
|
450 | + * @api |
|
451 | + */ |
|
452 | + public function __call($methodName, $arguments) |
|
453 | + { |
|
454 | + if (substr($methodName, 0, 6) === 'findBy' && strlen($methodName) > 7) { |
|
455 | + $propertyName = strtolower(substr(substr($methodName, 6), 0, 1)) . substr(substr($methodName, 6), 1); |
|
456 | + $result = $this->processMagicCall($propertyName, $arguments[0]); |
|
457 | + } elseif (substr($methodName, 0, 9) === 'findOneBy' && strlen($methodName) > 10) { |
|
458 | + $propertyName = strtolower(substr(substr($methodName, 9), 0, 1)) . substr(substr($methodName, 9), 1); |
|
459 | + $result = $this->processMagicCall($propertyName, $arguments[0], 'one'); |
|
460 | + } elseif (substr($methodName, 0, 7) === 'countBy' && strlen($methodName) > 8) { |
|
461 | + $propertyName = strtolower(substr(substr($methodName, 7), 0, 1)) . substr(substr($methodName, 7), 1); |
|
462 | + $result = $this->processMagicCall($propertyName, $arguments[0], 'count'); |
|
463 | + } else { |
|
464 | + throw new UnsupportedMethodException('The method "' . $methodName . '" is not supported by the repository.', 1360838010); |
|
465 | + } |
|
466 | + return $result; |
|
467 | + } |
|
468 | + |
|
469 | + /** |
|
470 | + * Returns a query for objects of this repository |
|
471 | + * |
|
472 | + * @return Query |
|
473 | + * @api |
|
474 | + */ |
|
475 | + public function createQuery() |
|
476 | + { |
|
477 | + /** @var Query $query */ |
|
478 | + $query = GeneralUtility::makeInstance(Query::class, $this->dataType); |
|
479 | + $query->setSourceFieldName($this->sourceFieldName); |
|
480 | + |
|
481 | + if ($this->defaultQuerySettings) { |
|
482 | + $query->setTypo3QuerySettings($this->defaultQuerySettings); |
|
483 | + } else { |
|
484 | + |
|
485 | + // Initialize and pass the query settings at this level. |
|
486 | + $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class); |
|
487 | + |
|
488 | + // Default choice for the BE. |
|
489 | + if ($this->isBackendMode()) { |
|
490 | + $querySettings->setIgnoreEnableFields(true); |
|
491 | + } |
|
492 | + |
|
493 | + $query->setTypo3QuerySettings($querySettings); |
|
494 | + } |
|
495 | + |
|
496 | + return $query; |
|
497 | + } |
|
498 | + |
|
499 | + /** |
|
500 | + * Sets the property names to order the result by per default. |
|
501 | + * Expected like this: |
|
502 | + * array( |
|
503 | + * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING, |
|
504 | + * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING |
|
505 | + * ) |
|
506 | + * |
|
507 | + * @param array $defaultOrderings The property names to order by |
|
508 | + * @return void |
|
509 | + * @api |
|
510 | + */ |
|
511 | + public function setDefaultOrderings(array $defaultOrderings) |
|
512 | + { |
|
513 | + throw new \BadMethodCallException('Repository does not support the setDefaultOrderings() method.', 1375805598); |
|
514 | + } |
|
515 | + |
|
516 | + /** |
|
517 | + * Sets the default query settings to be used in this repository |
|
518 | + * |
|
519 | + * @param QuerySettingsInterface $defaultQuerySettings The query settings to be used by default |
|
520 | + * @return void |
|
521 | + * @api |
|
522 | + */ |
|
523 | + public function setDefaultQuerySettings(QuerySettingsInterface $defaultQuerySettings) |
|
524 | + { |
|
525 | + $this->defaultQuerySettings = $defaultQuerySettings; |
|
526 | + } |
|
527 | + |
|
528 | + /** |
|
529 | + * @return void |
|
530 | + */ |
|
531 | + public function resetDefaultQuerySettings(): void |
|
532 | + { |
|
533 | + $this->defaultQuerySettings = null; |
|
534 | + } |
|
535 | + |
|
536 | + |
|
537 | + /** |
|
538 | + * @return array |
|
539 | + */ |
|
540 | + public function getErrorMessages(): array |
|
541 | + { |
|
542 | + return $this->errorMessages; |
|
543 | + } |
|
544 | + |
|
545 | + /** |
|
546 | + * @param string $sourceFieldName |
|
547 | + * @return $this |
|
548 | + */ |
|
549 | + public function setSourceFieldName($sourceFieldName): self |
|
550 | + { |
|
551 | + $this->sourceFieldName = $sourceFieldName; |
|
552 | + return $this; |
|
553 | + } |
|
554 | + |
|
555 | + /** |
|
556 | + * @return string |
|
557 | + */ |
|
558 | + public function getDataType(): string |
|
559 | + { |
|
560 | + return $this->dataType; |
|
561 | + } |
|
562 | + |
|
563 | + /** |
|
564 | + * Tell whether the order has a foreign table in its expression, e.g. "metadata.title". |
|
565 | + * |
|
566 | + * @param string $ordering |
|
567 | + * @return bool |
|
568 | + */ |
|
569 | + protected function hasForeignRelationIn($ordering): bool |
|
570 | + { |
|
571 | + return strpos($ordering, '.') !== false; |
|
572 | + } |
|
573 | + |
|
574 | + /** |
|
575 | + * Extract the foreign relation of the ordering "metadata.title" -> "metadata" |
|
576 | + * |
|
577 | + * @param string $ordering |
|
578 | + * @return string |
|
579 | + */ |
|
580 | + protected function getForeignRelationFrom($ordering): string |
|
581 | + { |
|
582 | + $parts = explode('.', $ordering); |
|
583 | + return $parts[0]; |
|
584 | + } |
|
585 | + |
|
586 | + /** |
|
587 | + * Get the constraints |
|
588 | + * |
|
589 | + * @param Query $query |
|
590 | + * @param Matcher $matcher |
|
591 | + * @return ConstraintInterface|null |
|
592 | + */ |
|
593 | + protected function computeConstraints(Query $query, Matcher $matcher): ?ConstraintInterface |
|
594 | + { |
|
595 | + $constraints = null; |
|
596 | + |
|
597 | + $collectedConstraints = []; |
|
598 | + |
|
599 | + // Search term |
|
600 | + $constraint = $this->computeSearchTermConstraint($query, $matcher); |
|
601 | + if ($constraint) { |
|
602 | + $collectedConstraints[] = $constraint; |
|
603 | + } |
|
604 | + |
|
605 | + foreach ($matcher->getSupportedOperators() as $operator) { |
|
606 | + $constraint = $this->computeConstraint($query, $matcher, $operator); |
|
607 | + if ($constraint) { |
|
608 | + $collectedConstraints[] = $constraint; |
|
609 | + } |
|
610 | + } |
|
611 | + |
|
612 | + if (count($collectedConstraints) > 1) { |
|
613 | + $logical = $matcher->getDefaultLogicalSeparator(); |
|
614 | + $constraints = $query->$logical($collectedConstraints); |
|
615 | + } elseif (!empty($collectedConstraints)) { |
|
616 | + |
|
617 | + // true means there is one constraint only and should become the result |
|
618 | + $constraints = current($collectedConstraints); |
|
619 | + } |
|
620 | + |
|
621 | + // Trigger signal for post processing the computed constraints object. |
|
622 | + $constraints = $this->emitPostProcessConstraintsSignal($query, $constraints); |
|
623 | + |
|
624 | + return $constraints; |
|
625 | + } |
|
626 | + |
|
627 | + /** |
|
628 | + * Computes the search constraint and returns it. |
|
629 | + * |
|
630 | + * @param Query $query |
|
631 | + * @param Matcher $matcher |
|
632 | + * @return ConstraintInterface|null |
|
633 | + */ |
|
634 | + protected function computeSearchTermConstraint(Query $query, Matcher $matcher): ?ConstraintInterface |
|
635 | + { |
|
636 | + |
|
637 | + $result = null; |
|
638 | + |
|
639 | + // Search term case |
|
640 | + if ($matcher->getSearchTerm()) { |
|
641 | + |
|
642 | + $fields = GeneralUtility::trimExplode(',', Tca::table($this->dataType)->getSearchFields(), true); |
|
643 | + |
|
644 | + $constraints = []; |
|
645 | + $likeClause = sprintf('%%%s%%', $matcher->getSearchTerm()); |
|
646 | + foreach ($fields as $fieldNameAndPath) { |
|
647 | + if ($this->isSuitableForLike($fieldNameAndPath, $matcher->getSearchTerm())) { |
|
648 | + |
|
649 | + $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->dataType); |
|
650 | + $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $this->dataType); |
|
651 | + |
|
652 | + if (Tca::table($dataType)->hasField($fieldName) && Tca::table($dataType)->field($fieldName)->hasRelation()) { |
|
653 | + $foreignTable = Tca::table($dataType)->field($fieldName)->getForeignTable(); |
|
654 | + $fieldNameAndPath = $fieldNameAndPath . '.' . Tca::table($foreignTable)->getLabelField(); |
|
655 | + } |
|
656 | + $constraints[] = $query->like($fieldNameAndPath, $likeClause); |
|
657 | + } |
|
658 | + } |
|
659 | + $logical = $matcher->getLogicalSeparatorForSearchTerm(); |
|
660 | + $result = $query->$logical($constraints); |
|
661 | + } |
|
662 | + |
|
663 | + return $result; |
|
664 | + } |
|
665 | + |
|
666 | + /** |
|
667 | + * It does not make sense to have a "like" in presence of numerical field, e.g "uid". |
|
668 | + * Tell whether the given value makes sense for a "like" clause. |
|
669 | + * |
|
670 | + * @param string $fieldNameAndPath |
|
671 | + * @param string $value |
|
672 | + * @return bool |
|
673 | + */ |
|
674 | + protected function isSuitableForLike($fieldNameAndPath, $value): bool |
|
675 | + { |
|
676 | + $isSuitable = true; |
|
677 | + |
|
678 | + // true means it is a string |
|
679 | + if (!MathUtility::canBeInterpretedAsInteger($value)) { |
|
680 | + |
|
681 | + $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->dataType); |
|
682 | + $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $this->dataType); |
|
683 | + |
|
684 | + if (Tca::table($dataType)->field($fieldName)->isNumerical() |
|
685 | + && !Tca::table($dataType)->field($fieldName)->hasRelation() |
|
686 | + ) { |
|
687 | + $isSuitable = false; |
|
688 | + } |
|
689 | + } |
|
690 | + |
|
691 | + return $isSuitable; |
|
692 | + } |
|
693 | + |
|
694 | + /** |
|
695 | + * Computes the constraint for matches and returns it. |
|
696 | + * |
|
697 | + * @param Query $query |
|
698 | + * @param Matcher $matcher |
|
699 | + * @param string $operator |
|
700 | + * @return ConstraintInterface|null |
|
701 | + */ |
|
702 | + protected function computeConstraint(Query $query, Matcher $matcher, $operator): ?ConstraintInterface |
|
703 | + { |
|
704 | + $result = null; |
|
705 | + |
|
706 | + $operatorName = ucfirst($operator); |
|
707 | + $getCriteria = sprintf('get%s', $operatorName); |
|
708 | + $criteria = $matcher->$getCriteria(); |
|
709 | + |
|
710 | + if (!empty($criteria)) { |
|
711 | + $constraints = []; |
|
712 | + |
|
713 | + foreach ($criteria as $criterion) { |
|
714 | + |
|
715 | + $fieldNameAndPath = $criterion['fieldNameAndPath']; |
|
716 | + $operand = $criterion['operand']; |
|
717 | + |
|
718 | + // Compute a few variables... |
|
719 | + // $dataType is generally equals to $this->dataType but not always... if fieldName is a path. |
|
720 | + $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->dataType); |
|
721 | + $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $this->dataType); |
|
722 | + $fieldPath = $this->getFieldPathResolver()->stripFieldName($fieldNameAndPath, $this->dataType); |
|
723 | + |
|
724 | + if (Tca::table($dataType)->field($fieldName)->hasRelation()) { |
|
725 | + if (MathUtility::canBeInterpretedAsInteger($operand)) { |
|
726 | + $fieldNameAndPath = $fieldName . '.uid'; |
|
727 | + } else { |
|
728 | + $foreignTableName = Tca::table($dataType)->field($fieldName)->getForeignTable(); |
|
729 | + $foreignTable = Tca::table($foreignTableName); |
|
730 | + $fieldNameAndPath = $fieldName . '.' . $foreignTable->getLabelField(); |
|
731 | + } |
|
732 | + |
|
733 | + // If different means we should restore the prepended path segment for proper SQL parser. |
|
734 | + // This is true for a composite field, e.g items.sys_file_metadata for categories. |
|
735 | + if ($fieldName !== $fieldPath) { |
|
736 | + $fieldNameAndPath = $fieldPath . '.' . $fieldNameAndPath; |
|
737 | + } |
|
738 | + } |
|
739 | + |
|
740 | + if (strpos($operator, 'not') === 0) { |
|
741 | + $strippedOperator = strtolower(substr($operator, 3)); |
|
742 | + $constraints[] = $query->logicalNot($query->$strippedOperator($fieldNameAndPath, $criterion['operand'])); |
|
743 | + } else { |
|
744 | + $constraints[] = $query->$operator($fieldNameAndPath, $criterion['operand']); |
|
745 | + } |
|
746 | + } |
|
747 | + |
|
748 | + $getLogicalSeparator = sprintf('getLogicalSeparatorFor%s', $operatorName); |
|
749 | + $logical = method_exists($matcher, $getLogicalSeparator) |
|
750 | + ? $matcher->$getLogicalSeparator() |
|
751 | + : $matcher->getDefaultLogicalSeparator(); |
|
752 | + |
|
753 | + $result = $query->$logical($constraints); |
|
754 | + } |
|
755 | + |
|
756 | + return $result; |
|
757 | + } |
|
758 | + |
|
759 | + /** |
|
760 | + * @return DataHandler |
|
761 | + */ |
|
762 | + protected function getDataHandler(): DataHandler |
|
763 | + { |
|
764 | + if (!$this->dataHandler) { |
|
765 | + $this->dataHandler = GeneralUtility::makeInstance(DataHandler::class); |
|
766 | + } |
|
767 | + return $this->dataHandler; |
|
768 | + } |
|
769 | + |
|
770 | + /** |
|
771 | + * Handle the magic call by properly creating a Query object and returning its result. |
|
772 | + * |
|
773 | + * @param string $propertyName |
|
774 | + * @param string $value |
|
775 | + * @param string $flag |
|
776 | + * @return mixed |
|
777 | + */ |
|
778 | + protected function processMagicCall($propertyName, $value, $flag = '') |
|
779 | + { |
|
780 | + |
|
781 | + $fieldName = Property::name($propertyName)->of($this->dataType)->toFieldName(); |
|
782 | + |
|
783 | + /** @var $matcher Matcher */ |
|
784 | + $matcher = GeneralUtility::makeInstance(Matcher::class, [], $this->getDataType()); |
|
785 | + |
|
786 | + $table = Tca::table($this->dataType); |
|
787 | + if ($table->field($fieldName)->isGroup()) { |
|
788 | + |
|
789 | + $valueParts = explode('.', $value, 2); |
|
790 | + $fieldName = $fieldName . '.' . $valueParts[0]; |
|
791 | + $value = $valueParts[1]; |
|
792 | + } |
|
793 | + |
|
794 | + $matcher->equals($fieldName, $value); |
|
795 | + |
|
796 | + if ($flag === 'count') { |
|
797 | + $result = $this->countBy($matcher); |
|
798 | + } else { |
|
799 | + $result = $this->findBy($matcher); |
|
800 | + } |
|
801 | + return $flag === 'one' && !empty($result) ? reset($result) : $result; |
|
802 | + } |
|
803 | + |
|
804 | + /** |
|
805 | + * @return DataHandlerFactory|object |
|
806 | + */ |
|
807 | + protected function getDataHandlerFactory() |
|
808 | + { |
|
809 | + return GeneralUtility::makeInstance(DataHandlerFactory::class); |
|
810 | + } |
|
811 | + |
|
812 | + /** |
|
813 | + * Returns whether the current mode is Backend |
|
814 | + * |
|
815 | + * @return bool |
|
816 | + */ |
|
817 | + protected function isBackendMode(): bool |
|
818 | + { |
|
819 | + return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); |
|
820 | + } |
|
821 | + |
|
822 | + /** |
|
823 | + * @return FieldPathResolver|object |
|
824 | + */ |
|
825 | + protected function getFieldPathResolver() |
|
826 | + { |
|
827 | + return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
828 | + } |
|
829 | + |
|
830 | + /** |
|
831 | + * @return ContentValidator|object |
|
832 | + */ |
|
833 | + protected function getContentValidator(): ContentValidator |
|
834 | + { |
|
835 | + return GeneralUtility::makeInstance(ContentValidator::class); |
|
836 | + } |
|
837 | + |
|
838 | + /** |
|
839 | + * @return LanguageValidator|object |
|
840 | + */ |
|
841 | + protected function getLanguageValidator(): LanguageValidator |
|
842 | + { |
|
843 | + return GeneralUtility::makeInstance(LanguageValidator::class); |
|
844 | + } |
|
845 | + |
|
846 | + /** |
|
847 | + * Signal that is called for post-processing the computed constraints object. |
|
848 | + * |
|
849 | + * @param Query $query |
|
850 | + * @param ConstraintInterface|null $constraints |
|
851 | + * @return ConstraintInterface|null $constraints |
|
852 | + */ |
|
853 | + protected function emitPostProcessConstraintsSignal(Query $query, $constraints): ?ConstraintInterface |
|
854 | + { |
|
855 | + /** @var ConstraintContainer $constraintContainer */ |
|
856 | + $constraintContainer = GeneralUtility::makeInstance(ConstraintContainer::class); |
|
857 | + $result = $this->getSignalSlotDispatcher()->dispatch( |
|
858 | + self::class, |
|
859 | + 'postProcessConstraintsObject', |
|
860 | + [ |
|
861 | + $query, |
|
862 | + $constraints, |
|
863 | + $constraintContainer |
|
864 | + ] |
|
865 | + ); |
|
866 | + |
|
867 | + // Backward compatibility. |
|
868 | + $processedConstraints = $result[1]; |
|
869 | + |
|
870 | + // New way to transmit the constraints. |
|
871 | + if ($constraintContainer->getConstraint()) { |
|
872 | + $processedConstraints = $constraintContainer->getConstraint(); |
|
873 | + } |
|
874 | + return $processedConstraints; |
|
875 | + } |
|
876 | + |
|
877 | + /** |
|
878 | + * @return Dispatcher |
|
879 | + */ |
|
880 | + protected function getSignalSlotDispatcher(): Dispatcher |
|
881 | + { |
|
882 | + return GeneralUtility::makeInstance(Dispatcher::class); |
|
883 | + } |
|
884 | 884 | |
885 | 885 | } |
@@ -44,750 +44,750 @@ |
||
44 | 44 | */ |
45 | 45 | class ContentController extends ActionController |
46 | 46 | { |
47 | - /** |
|
48 | - * Initialize every action. |
|
49 | - */ |
|
50 | - public function initializeAction() |
|
51 | - { |
|
52 | - $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); |
|
53 | - $pageRenderer->addInlineLanguageLabelFile('EXT:vidi/Resources/Private/Language/locallang.xlf'); |
|
54 | - |
|
55 | - // Configure property mapping to retrieve the file object. |
|
56 | - if ($this->arguments->hasArgument('columns')) { |
|
57 | - |
|
58 | - /** @var CsvToArrayConverter $typeConverter */ |
|
59 | - $typeConverter = GeneralUtility::makeInstance(CsvToArrayConverter::class); |
|
60 | - |
|
61 | - $propertyMappingConfiguration = $this->arguments->getArgument('columns')->getPropertyMappingConfiguration(); |
|
62 | - $propertyMappingConfiguration->setTypeConverter($typeConverter); |
|
63 | - } |
|
64 | - } |
|
65 | - |
|
66 | - /** |
|
67 | - * List action for this controller. |
|
68 | - * |
|
69 | - * @return void |
|
70 | - */ |
|
71 | - public function indexAction(): ResponseInterface |
|
72 | - { |
|
73 | - $dataType = $this->getModuleLoader()->getDataType(); |
|
74 | - $selections = $this->selectionRepository->findByDataTypeForCurrentBackendUser($dataType); |
|
75 | - $this->view->assign('selections', $selections); |
|
76 | - |
|
77 | - $columns = Tca::grid()->getFields(); |
|
78 | - $this->view->assign('columns', $columns); |
|
79 | - $this->view->assign('numberOfColumns', count($columns)); |
|
80 | - return $this->htmlResponse(); |
|
81 | - } |
|
82 | - |
|
83 | - /** |
|
84 | - * List Row action for this controller. Output a json list of contents |
|
85 | - * |
|
86 | - * @param array $columns corresponds to columns to be rendered. |
|
87 | - * @param array $matches |
|
88 | - * @Validate("Fab\Vidi\Domain\Validator\ColumnsValidator", param="columns") |
|
89 | - * @Validate("Fab\Vidi\Domain\Validator\MatchesValidator", param="matches") |
|
90 | - * @return void |
|
91 | - */ |
|
92 | - public function listAction(array $columns = [], $matches = []): ResponseInterface |
|
93 | - { |
|
94 | - // Initialize some objects related to the query. |
|
95 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
96 | - $order = OrderObjectFactory::getInstance()->getOrder(); |
|
97 | - $pager = PagerObjectFactory::getInstance()->getPager(); |
|
98 | - |
|
99 | - // Fetch objects via the Content Service. |
|
100 | - $contentService = $this->getContentService()->findBy($matcher, $order, $pager->getLimit(), $pager->getOffset()); |
|
101 | - $pager->setCount($contentService->getNumberOfObjects()); |
|
102 | - |
|
103 | - // Assign values. |
|
104 | - $this->view->assign('columns', $columns); |
|
105 | - $this->view->assign('objects', $contentService->getObjects()); |
|
106 | - $this->view->assign('numberOfObjects', $contentService->getNumberOfObjects()); |
|
107 | - $this->view->assign('pager', $pager); |
|
108 | - |
|
109 | - $this->view->assign('response', $this->responseFactory->createResponse()); |
|
110 | - return $this->htmlResponse(); |
|
111 | - } |
|
112 | - |
|
113 | - /** |
|
114 | - * Retrieve Content objects first according to matching criteria and then "update" them. |
|
115 | - * Important to notice the field name can contains a path, e.g. metadata.title and therefore must be analysed. |
|
116 | - * |
|
117 | - * Possible values for $matches: |
|
118 | - * ----------------------------- |
|
119 | - * |
|
120 | - * $matches = array(uid => 1), will be taken as $query->equals |
|
121 | - * $matches = array(uid => 1,2,3), will be taken as $query->in |
|
122 | - * $matches = array(field_name1 => bar, field_name2 => bax), will be separated by AND. |
|
123 | - * |
|
124 | - * Possible values for $content: |
|
125 | - * ----------------------------- |
|
126 | - * |
|
127 | - * $content = array(field_name => bar) |
|
128 | - * $content = array(field_name => array(value1, value2)) <-- will be CSV converted by "value1,value2" |
|
129 | - * |
|
130 | - * @param string $fieldNameAndPath |
|
131 | - * @param array $content |
|
132 | - * @param array $matches |
|
133 | - * @param string $savingBehavior |
|
134 | - * @param int $language |
|
135 | - * @param array $columns |
|
136 | - * @throws InvalidKeyInArrayException |
|
137 | - */ |
|
138 | - public function updateAction($fieldNameAndPath, array $content, array $matches = [], $savingBehavior = SavingBehavior::REPLACE, $language = 0, $columns = []) |
|
139 | - { |
|
140 | - |
|
141 | - // Instantiate the Matcher object according different rules. |
|
142 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
143 | - $order = OrderObjectFactory::getInstance()->getOrder(); |
|
144 | - |
|
145 | - // Fetch objects via the Content Service. |
|
146 | - $contentService = $this->getContentService()->findBy($matcher, $order); |
|
147 | - |
|
148 | - // Get the real field that is going to be updated. |
|
149 | - $updatedFieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath); |
|
150 | - |
|
151 | - // Get result object for storing data along the processing. |
|
152 | - $result = $this->getJsonResult(); |
|
153 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
154 | - |
|
155 | - foreach ($contentService->getObjects() as $index => $object) { |
|
156 | - |
|
157 | - $identifier = $this->getContentObjectResolver()->getValue($object, $fieldNameAndPath, 'uid', $language); |
|
158 | - |
|
159 | - // It could be the identifier is not found because the translation |
|
160 | - // of the record does not yet exist when mass-editing |
|
161 | - if ((int)$identifier <= 0) { |
|
162 | - continue; |
|
163 | - } |
|
164 | - |
|
165 | - $dataType = $this->getContentObjectResolver()->getDataType($object, $fieldNameAndPath); |
|
166 | - |
|
167 | - $signalResult = $this->emitProcessContentDataSignal($object, $fieldNameAndPath, $content, $index + 1, $savingBehavior, $language); |
|
168 | - $contentData = $signalResult->getContentData(); |
|
169 | - |
|
170 | - // Add identifier to content data, required by TCEMain. |
|
171 | - $contentData['uid'] = $identifier; |
|
172 | - |
|
173 | - /** @var Content $dataObject */ |
|
174 | - $dataObject = GeneralUtility::makeInstance(Content::class, $dataType, $contentData); |
|
175 | - |
|
176 | - // Properly update object. |
|
177 | - ContentRepositoryFactory::getInstance($dataType)->update($dataObject); |
|
178 | - |
|
179 | - // Get the possible error messages and store them. |
|
180 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
181 | - $result->addErrorMessages($errorMessages); |
|
182 | - |
|
183 | - // We only want to see the detail result if there is one object updated. |
|
184 | - // Required for inline editing + it will display some useful info on the GUI in the flash messages. |
|
185 | - if ($contentService->getNumberOfObjects() === 1) { |
|
186 | - |
|
187 | - // Fetch the updated object from repository. |
|
188 | - $updatedObject = ContentRepositoryFactory::getInstance()->findByUid($object->getUid()); |
|
189 | - |
|
190 | - // Re-fetch the updated result. |
|
191 | - $updatedResult = $this->getContentObjectResolver()->getValue($updatedObject, $fieldNameAndPath, $updatedFieldName, $language); |
|
192 | - if (is_array($updatedResult)) { |
|
193 | - $_updatedResult = []; // reset result set. |
|
194 | - |
|
195 | - /** @var Content $contentObject */ |
|
196 | - foreach ($updatedResult as $contentObject) { |
|
197 | - $labelField = Tca::table($contentObject)->getLabelField(); |
|
198 | - $values = array( |
|
199 | - 'uid' => $contentObject->getUid(), |
|
200 | - 'name' => $contentObject[$labelField], |
|
201 | - ); |
|
202 | - $_updatedResult[] = $values; |
|
203 | - } |
|
204 | - |
|
205 | - $updatedResult = $_updatedResult; |
|
206 | - } |
|
207 | - |
|
208 | - $labelField = Tca::table($object)->getLabelField(); |
|
209 | - |
|
210 | - $processedObjectData = array( |
|
211 | - 'uid' => $object->getUid(), |
|
212 | - 'name' => $object[$labelField], |
|
213 | - 'updatedField' => $fieldNameAndPath, |
|
214 | - 'updatedValue' => $updatedResult, |
|
215 | - ); |
|
216 | - $result->setProcessedObject($processedObjectData); |
|
217 | - |
|
218 | - if (!empty($columns)) { |
|
219 | - /** @var Row $row */ |
|
220 | - $row = GeneralUtility::makeInstance(Row::class, $columns); |
|
221 | - $result->setRow($row->render($updatedObject)); |
|
222 | - } |
|
223 | - } |
|
224 | - } |
|
225 | - |
|
226 | - $response = $this->responseFactory->createResponse() |
|
227 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
228 | - $response->getBody()->write(json_encode($result)); |
|
229 | - return $response; |
|
230 | - } |
|
231 | - |
|
232 | - /** |
|
233 | - * Set the sorting of a record giving the previous object. |
|
234 | - * |
|
235 | - * @param array $matches |
|
236 | - * @param int $previousIdentifier |
|
237 | - */ |
|
238 | - public function sortAction(array $matches = [], $previousIdentifier = null) |
|
239 | - { |
|
240 | - |
|
241 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
242 | - |
|
243 | - // Fetch objects via the Content Service. |
|
244 | - $contentService = $this->getContentService()->findBy($matcher); |
|
245 | - |
|
246 | - // Compute the label field name of the table. |
|
247 | - $tableTitleField = Tca::table()->getLabelField(); |
|
248 | - |
|
249 | - // Get result object for storing data along the processing. |
|
250 | - $result = $this->getJsonResult(); |
|
251 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
252 | - |
|
253 | - foreach ($contentService->getObjects() as $object) { |
|
254 | - |
|
255 | - // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
256 | - if ($contentService->getNumberOfObjects() === 1) { |
|
257 | - $tableTitleValue = $object[$tableTitleField]; |
|
258 | - $processedObjectData = array( |
|
259 | - 'uid' => $object->getUid(), |
|
260 | - 'name' => $tableTitleValue, |
|
261 | - ); |
|
262 | - $result->setProcessedObject($processedObjectData); |
|
263 | - } |
|
264 | - |
|
265 | - // The $target corresponds to the pid to move the records to. |
|
266 | - // It can also be a negative value in case of sorting. The negative value would be the uid of its predecessor. |
|
267 | - $target = is_null($previousIdentifier) ? $object->getPid() : (-(int)$previousIdentifier); |
|
268 | - |
|
269 | - // Work out the object. |
|
270 | - ContentRepositoryFactory::getInstance()->move($object, $target); |
|
271 | - |
|
272 | - // Get the possible error messages and store them. |
|
273 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
274 | - $result->addErrorMessages($errorMessages); |
|
275 | - } |
|
276 | - |
|
277 | - $response = $this->responseFactory->createResponse() |
|
278 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
279 | - $response->getBody()->write(json_encode($result)); |
|
280 | - return $response; |
|
281 | - } |
|
282 | - |
|
283 | - /** |
|
284 | - * Returns an editing form for a given field name of a Content object. |
|
285 | - * Argument $fieldNameAndPath corresponds to the field name to be edited. |
|
286 | - * Important to notice it can contains a path, e.g. metadata.title and therefore must be analysed. |
|
287 | - * |
|
288 | - * Possible values for $matches, refer to method "updateAction". |
|
289 | - * |
|
290 | - * @param string $fieldNameAndPath |
|
291 | - * @param array $matches |
|
292 | - * @param bool $hasRecursiveSelection |
|
293 | - * @throws \Exception |
|
294 | - */ |
|
295 | - public function editAction($fieldNameAndPath, array $matches = [], $hasRecursiveSelection = false): ResponseInterface |
|
296 | - { |
|
297 | - |
|
298 | - // Instantiate the Matcher object according different rules. |
|
299 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
300 | - |
|
301 | - // Fetch objects via the Content Service. |
|
302 | - $contentService = $this->getContentService()->findBy($matcher); |
|
303 | - |
|
304 | - $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath); |
|
305 | - $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath); |
|
306 | - |
|
307 | - $fieldType = Tca::table($dataType)->field($fieldName)->getType(); |
|
308 | - $this->view->assign('fieldType', ucfirst($fieldType)); |
|
309 | - $this->view->assign('dataType', $dataType); |
|
310 | - $this->view->assign('fieldName', $fieldName); |
|
311 | - $this->view->assign('matches', $matches); |
|
312 | - $this->view->assign('fieldNameAndPath', $fieldNameAndPath); |
|
313 | - $this->view->assign('numberOfObjects', $contentService->getNumberOfObjects()); |
|
314 | - $this->view->assign('hasRecursiveSelection', $hasRecursiveSelection); |
|
315 | - $this->view->assign('editWholeSelection', empty($matches['uid'])); // necessary?? |
|
316 | - |
|
317 | - // Fetch content and its relations. |
|
318 | - if ($fieldType === FieldType::MULTISELECT) { |
|
319 | - |
|
320 | - $object = ContentRepositoryFactory::getInstance()->findOneBy($matcher); |
|
321 | - $identifier = $this->getContentObjectResolver()->getValue($object, $fieldNameAndPath, 'uid'); |
|
322 | - $dataType = $this->getContentObjectResolver()->getDataType($object, $fieldNameAndPath); |
|
323 | - |
|
324 | - $content = ContentRepositoryFactory::getInstance($dataType)->findByUid($identifier); |
|
325 | - |
|
326 | - // Makes sure the object was retrieved. Security! |
|
327 | - if (!$content) { |
|
328 | - $message = sprintf('I could not retrieved content object of type "%s" with identifier %s.', $dataType, $identifier); |
|
329 | - throw new \Exception($message, 1402350182); |
|
330 | - } |
|
331 | - |
|
332 | - $relatedDataType = Tca::table($dataType)->field($fieldName)->getForeignTable(); |
|
333 | - |
|
334 | - // Initialize the matcher object. |
|
335 | - /** @var Matcher $matcher */ |
|
336 | - $matcher = GeneralUtility::makeInstance(Matcher::class, [], $relatedDataType); |
|
337 | - |
|
338 | - // Default ordering for related data type. |
|
339 | - $defaultOrderings = Tca::table($relatedDataType)->getDefaultOrderings(); |
|
340 | - /** @var Order $order */ |
|
341 | - $defaultOrder = GeneralUtility::makeInstance(Order::class, $defaultOrderings); |
|
342 | - |
|
343 | - // Fetch related contents |
|
344 | - $relatedContents = ContentRepositoryFactory::getInstance($relatedDataType)->findBy($matcher, $defaultOrder); |
|
345 | - |
|
346 | - if (Tca::table($dataType)->field($fieldName)->isRenderModeTree()) { |
|
347 | - |
|
348 | - $fieldConfiguration = Tca::table($dataType)->field($fieldName)->getConfiguration(); |
|
349 | - $parentField = $fieldConfiguration['treeConfig']['parentField']; |
|
350 | - |
|
351 | - $flatTree = []; |
|
352 | - foreach ($relatedContents as $node) { |
|
353 | - $flatTree[$node->getUid()] = array( |
|
354 | - 'item' => $node, |
|
355 | - 'parent' => $node[$parentField] ? $node[$parentField]['uid'] : null, |
|
356 | - ); |
|
357 | - } |
|
358 | - |
|
359 | - $tree = []; |
|
360 | - |
|
361 | - // If leaves are selected without its parents selected, those are shown as parent |
|
362 | - foreach ($flatTree as $id => &$flatNode) { |
|
363 | - if (!isset($flatTree[$flatNode['parent']])) { |
|
364 | - $flatNode['parent'] = null; |
|
365 | - } |
|
366 | - } |
|
367 | - |
|
368 | - foreach ($flatTree as $id => &$node) { |
|
369 | - if ($node['parent'] === null) { |
|
370 | - $tree[$id] = &$node; |
|
371 | - } else { |
|
372 | - $flatTree[$node['parent']]['children'][$id] = &$node; |
|
373 | - } |
|
374 | - } |
|
375 | - |
|
376 | - $relatedContents = $tree; |
|
377 | - } |
|
378 | - |
|
379 | - $this->view->assign('content', $content); |
|
380 | - $this->view->assign('relatedContents', $relatedContents); |
|
381 | - $this->view->assign('relatedDataType', $relatedDataType); |
|
382 | - $this->view->assign('relatedContentTitle', Tca::table($relatedDataType)->getTitle()); |
|
383 | - $this->view->assign( |
|
384 | - 'renderMode', |
|
385 | - Tca::table($dataType)->field($fieldName)->isRenderModeTree() ? FieldType::TREE : null |
|
386 | - ); |
|
387 | - } |
|
388 | - return $this->htmlResponse(); |
|
389 | - } |
|
390 | - |
|
391 | - /** |
|
392 | - * Retrieve Content objects first according to matching criteria and then "delete" them. |
|
393 | - * |
|
394 | - * Possible values for $matches, refer to method "updateAction". |
|
395 | - * |
|
396 | - * @param array $matches |
|
397 | - */ |
|
398 | - public function deleteAction(array $matches = []) |
|
399 | - { |
|
400 | - |
|
401 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
402 | - |
|
403 | - // Fetch objects via the Content Service. |
|
404 | - $contentService = $this->getContentService()->findBy($matcher); |
|
405 | - |
|
406 | - // Compute the label field name of the table. |
|
407 | - $tableTitleField = Tca::table()->getLabelField(); |
|
408 | - |
|
409 | - // Get result object for storing data along the processing. |
|
410 | - $result = $this->getJsonResult(); |
|
411 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
412 | - |
|
413 | - foreach ($contentService->getObjects() as $object) { |
|
414 | - |
|
415 | - // Store the first object, so that the delete message can be more explicit when deleting only one record. |
|
416 | - if ($contentService->getNumberOfObjects() === 1) { |
|
417 | - $tableTitleValue = $object[$tableTitleField]; |
|
418 | - $processedObjectData = array( |
|
419 | - 'uid' => $object->getUid(), |
|
420 | - 'name' => $tableTitleValue, |
|
421 | - ); |
|
422 | - $result->setProcessedObject($processedObjectData); |
|
423 | - } |
|
424 | - |
|
425 | - // Properly delete object. |
|
426 | - ContentRepositoryFactory::getInstance()->remove($object); |
|
427 | - |
|
428 | - // Get the possible error messages and store them. |
|
429 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
430 | - $result->addErrorMessages($errorMessages); |
|
431 | - } |
|
432 | - |
|
433 | - $response = $this->responseFactory->createResponse() |
|
434 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
435 | - $response->getBody()->write(json_encode($result)); |
|
436 | - return $response; |
|
437 | - } |
|
438 | - |
|
439 | - /** |
|
440 | - * Retrieve Content objects first according to matching criteria and then "copy" them. |
|
441 | - * |
|
442 | - * Possible values for $matches, refer to method "updateAction". |
|
443 | - * |
|
444 | - * @param string $target |
|
445 | - * @param array $matches |
|
446 | - * @throws \Exception |
|
447 | - * @return string |
|
448 | - */ |
|
449 | - public function copyAction($target, array $matches = []) |
|
450 | - { |
|
451 | - // @todo |
|
452 | - throw new \Exception('Not yet implemented', 1410192546); |
|
453 | - } |
|
454 | - |
|
455 | - /** |
|
456 | - * Retrieve Content objects from the Clipboard then "copy" them according to the target. |
|
457 | - * |
|
458 | - * @param string $target |
|
459 | - * @throws \Exception |
|
460 | - */ |
|
461 | - public function copyClipboardAction($target) |
|
462 | - { |
|
463 | - |
|
464 | - // Retrieve matcher object from clipboard. |
|
465 | - $matcher = $this->getClipboardService()->getMatcher(); |
|
466 | - |
|
467 | - // Fetch objects via the Content Service. |
|
468 | - $contentService = $this->getContentService()->findBy($matcher); |
|
469 | - |
|
470 | - // Compute the label field name of the table. |
|
471 | - $tableTitleField = Tca::table()->getLabelField(); |
|
472 | - |
|
473 | - // Get result object for storing data along the processing. |
|
474 | - $result = $this->getJsonResult(); |
|
475 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
476 | - |
|
477 | - foreach ($contentService->getObjects() as $object) { |
|
478 | - |
|
479 | - // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
480 | - if ($contentService->getNumberOfObjects() === 1) { |
|
481 | - $tableTitleValue = $object[$tableTitleField]; |
|
482 | - $processedObjectData = array( |
|
483 | - 'uid' => $object->getUid(), |
|
484 | - 'name' => $tableTitleValue, |
|
485 | - ); |
|
486 | - $result->setProcessedObject($processedObjectData); |
|
487 | - } |
|
488 | - |
|
489 | - // Work out the object. |
|
490 | - ContentRepositoryFactory::getInstance()->copy($object, $target); |
|
491 | - |
|
492 | - // Get the possible error messages and store them. |
|
493 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
494 | - $result->addErrorMessages($errorMessages); |
|
495 | - } |
|
496 | - |
|
497 | - // Flush Clipboard if told so. |
|
498 | - if (GeneralUtility::_GP('flushClipboard')) { |
|
499 | - $this->getClipboardService()->flush(); |
|
500 | - } |
|
501 | - |
|
502 | - $response = $this->responseFactory->createResponse() |
|
503 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
504 | - $response->getBody()->write(json_encode($result)); |
|
505 | - return $response; |
|
506 | - } |
|
507 | - |
|
508 | - /** |
|
509 | - * Retrieve Content objects first according to matching criteria and then "move" them. |
|
510 | - * |
|
511 | - * Possible values for $matches, refer to method "updateAction". |
|
512 | - * |
|
513 | - * @param string $target |
|
514 | - * @param array $matches |
|
515 | - */ |
|
516 | - public function moveAction($target, array $matches = []) |
|
517 | - { |
|
518 | - |
|
519 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
520 | - |
|
521 | - // Fetch objects via the Content Service. |
|
522 | - $contentService = $this->getContentService()->findBy($matcher); |
|
523 | - |
|
524 | - // Compute the label field name of the table. |
|
525 | - $tableTitleField = Tca::table()->getLabelField(); |
|
526 | - |
|
527 | - // Get result object for storing data along the processing. |
|
528 | - $result = $this->getJsonResult(); |
|
529 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
530 | - |
|
531 | - foreach ($contentService->getObjects() as $object) { |
|
532 | - |
|
533 | - // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
534 | - if ($contentService->getNumberOfObjects() === 1) { |
|
535 | - $tableTitleValue = $object[$tableTitleField]; |
|
536 | - $processedObjectData = array( |
|
537 | - 'uid' => $object->getUid(), |
|
538 | - 'name' => $tableTitleValue, |
|
539 | - ); |
|
540 | - $result->setProcessedObject($processedObjectData); |
|
541 | - } |
|
542 | - |
|
543 | - // Work out the object. |
|
544 | - ContentRepositoryFactory::getInstance()->move($object, $target); |
|
545 | - |
|
546 | - // Get the possible error messages and store them. |
|
547 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
548 | - $result->addErrorMessages($errorMessages); |
|
549 | - } |
|
550 | - |
|
551 | - $response = $this->responseFactory->createResponse() |
|
552 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
553 | - $response->getBody()->write(json_encode($result)); |
|
554 | - return $response; |
|
555 | - } |
|
556 | - |
|
557 | - /** |
|
558 | - * Retrieve Content objects from the Clipboard then "move" them according to the target. |
|
559 | - * |
|
560 | - * @param string $target |
|
561 | - */ |
|
562 | - public function moveClipboardAction($target) |
|
563 | - { |
|
564 | - |
|
565 | - // Retrieve matcher object from clipboard. |
|
566 | - $matcher = $this->getClipboardService()->getMatcher(); |
|
567 | - |
|
568 | - // Fetch objects via the Content Service. |
|
569 | - $contentService = $this->getContentService()->findBy($matcher); |
|
570 | - |
|
571 | - // Compute the label field name of the table. |
|
572 | - $tableTitleField = Tca::table()->getLabelField(); |
|
573 | - |
|
574 | - // Get result object for storing data along the processing. |
|
575 | - $result = $this->getJsonResult(); |
|
576 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
577 | - |
|
578 | - foreach ($contentService->getObjects() as $object) { |
|
579 | - |
|
580 | - // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
581 | - if ($contentService->getNumberOfObjects() === 1) { |
|
582 | - $tableTitleValue = $object[$tableTitleField]; |
|
583 | - $processedObjectData = array( |
|
584 | - 'uid' => $object->getUid(), |
|
585 | - 'name' => $tableTitleValue, |
|
586 | - ); |
|
587 | - $result->setProcessedObject($processedObjectData); |
|
588 | - } |
|
589 | - |
|
590 | - // Work out the object. |
|
591 | - ContentRepositoryFactory::getInstance()->move($object, $target); |
|
592 | - |
|
593 | - // Get the possible error messages and store them. |
|
594 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
595 | - $result->addErrorMessages($errorMessages); |
|
596 | - } |
|
597 | - |
|
598 | - // Flush Clipboard if told so. |
|
599 | - if (GeneralUtility::_GP('flushClipboard')) { |
|
600 | - $this->getClipboardService()->flush(); |
|
601 | - } |
|
602 | - |
|
603 | - $response = $this->responseFactory->createResponse() |
|
604 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
605 | - $response->getBody()->write(json_encode($result)); |
|
606 | - return $response; |
|
607 | - } |
|
608 | - |
|
609 | - /** |
|
610 | - * Retrieve Content objects first according to matching criteria and then "localize" them. |
|
611 | - * |
|
612 | - * Possible values for $matches, refer to method "updateAction". |
|
613 | - * |
|
614 | - * @param string $fieldNameAndPath |
|
615 | - * @param array $matches |
|
616 | - * @param int $language |
|
617 | - * @throws \Exception |
|
618 | - */ |
|
619 | - public function localizeAction($fieldNameAndPath, array $matches = [], $language = 0) |
|
620 | - { |
|
621 | - |
|
622 | - $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
623 | - |
|
624 | - // Fetch objects via the Content Service. |
|
625 | - $contentService = $this->getContentService()->findBy($matcher); |
|
626 | - |
|
627 | - // Get result object for storing data along the processing. |
|
628 | - $result = $this->getJsonResult(); |
|
629 | - $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
630 | - |
|
631 | - foreach ($contentService->getObjects() as $object) { |
|
632 | - |
|
633 | - $identifier = $this->getContentObjectResolver()->getValue($object, $fieldNameAndPath, 'uid'); |
|
634 | - $dataType = $this->getContentObjectResolver()->getDataType($object, $fieldNameAndPath); |
|
635 | - |
|
636 | - // Fetch the source object to be localized. |
|
637 | - /** @var Content $content */ |
|
638 | - $content = ContentRepositoryFactory::getInstance($dataType)->findByIdentifier($identifier); |
|
639 | - |
|
640 | - // Makes sure the object was retrieved. Security! |
|
641 | - if (!$content) { |
|
642 | - $message = sprintf('Something went wrong when retrieving content "%s" with identifier "%s".', $dataType, $identifier); |
|
643 | - throw new \Exception($message, 1412343097); |
|
644 | - } |
|
645 | - |
|
646 | - // Handover the localization to the Repository. |
|
647 | - ContentRepositoryFactory::getInstance($dataType)->localize($content, $language); |
|
648 | - |
|
649 | - // Get the possible error messages and store them. |
|
650 | - $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
651 | - |
|
652 | - // Redirect to TCEForm so that the BE User can do its job! |
|
653 | - if ($contentService->getNumberOfObjects() === 1) { |
|
654 | - |
|
655 | - if (!empty($errorMessages)) { |
|
656 | - $message = sprintf('Something went wrong when localizing content "%s" with identifier "%s". <br/>%s', |
|
657 | - $dataType, |
|
658 | - $identifier, |
|
659 | - implode('<br/>', $errorMessages) |
|
660 | - ); |
|
661 | - throw new \Exception($message, 1412343098); |
|
662 | - } |
|
663 | - |
|
664 | - $localizedContent = $this->getLanguageService()->getLocalizedContent($content, $language); |
|
665 | - if (empty($localizedContent)) { |
|
666 | - $message = sprintf('Oups! I could not retrieve localized content of type "%s" with identifier "%s"', |
|
667 | - $content->getDataType(), |
|
668 | - $content->getUid() |
|
669 | - ); |
|
670 | - throw new \Exception($message, 1412343099); |
|
671 | - } |
|
672 | - |
|
673 | - /** @var EditUri $uri */ |
|
674 | - $uriRenderer = GeneralUtility::makeInstance(EditUri::class); |
|
675 | - $uri = $uriRenderer->render($localizedContent); |
|
676 | - HttpUtility::redirect($uri); |
|
677 | - break; // no need to further continue |
|
678 | - } |
|
679 | - |
|
680 | - $result->addErrorMessages($errorMessages); |
|
681 | - } |
|
682 | - |
|
683 | - $response = $this->responseFactory->createResponse() |
|
684 | - ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
685 | - $response->getBody()->write(json_encode($result)); |
|
686 | - return $response; |
|
687 | - } |
|
688 | - |
|
689 | - /** |
|
690 | - * Get the Vidi Module Loader. |
|
691 | - * |
|
692 | - * @return ContentService |
|
693 | - */ |
|
694 | - protected function getContentService() |
|
695 | - { |
|
696 | - return GeneralUtility::makeInstance(ContentService::class); |
|
697 | - } |
|
698 | - |
|
699 | - /** |
|
700 | - * @return ContentObjectResolver |
|
701 | - */ |
|
702 | - protected function getContentObjectResolver() |
|
703 | - { |
|
704 | - return GeneralUtility::makeInstance(ContentObjectResolver::class); |
|
705 | - } |
|
706 | - |
|
707 | - /** |
|
708 | - * @return FieldPathResolver |
|
709 | - */ |
|
710 | - protected function getFieldPathResolver() |
|
711 | - { |
|
712 | - return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
713 | - } |
|
714 | - |
|
715 | - /** |
|
716 | - * @return JsonResult|object |
|
717 | - */ |
|
718 | - protected function getJsonResult() |
|
719 | - { |
|
720 | - return GeneralUtility::makeInstance(JsonResult::class); |
|
721 | - } |
|
722 | - |
|
723 | - /** |
|
724 | - * Signal that is called for post-processing content data send to the server for update. |
|
725 | - * |
|
726 | - * @param Content $contentObject |
|
727 | - * @param $fieldNameAndPath |
|
728 | - * @param $contentData |
|
729 | - * @param $counter |
|
730 | - * @param $savingBehavior |
|
731 | - * @param $language |
|
732 | - * @return ProcessContentDataSignalArguments |
|
733 | - */ |
|
734 | - protected function emitProcessContentDataSignal(Content $contentObject, $fieldNameAndPath, $contentData, $counter, $savingBehavior, $language) |
|
735 | - { |
|
736 | - |
|
737 | - /** @var ProcessContentDataSignalArguments $signalArguments */ |
|
738 | - $signalArguments = GeneralUtility::makeInstance(ProcessContentDataSignalArguments::class); |
|
739 | - $signalArguments->setContentObject($contentObject) |
|
740 | - ->setFieldNameAndPath($fieldNameAndPath) |
|
741 | - ->setContentData($contentData) |
|
742 | - ->setCounter($counter) |
|
743 | - ->setSavingBehavior($savingBehavior) |
|
744 | - ->setLanguage($language); |
|
745 | - |
|
746 | - $signalResult = $this->getSignalSlotDispatcher()->dispatch('Fab\Vidi\Controller\Backend\ContentController', 'processContentData', array($signalArguments)); |
|
747 | - return $signalResult[0]; |
|
748 | - } |
|
749 | - |
|
750 | - /** |
|
751 | - * Get the SignalSlot dispatcher. |
|
752 | - * |
|
753 | - * @return Dispatcher |
|
754 | - */ |
|
755 | - protected function getSignalSlotDispatcher() |
|
756 | - { |
|
757 | - return GeneralUtility::makeInstance(Dispatcher::class); |
|
758 | - } |
|
759 | - |
|
760 | - /** |
|
761 | - * Get the Clipboard service. |
|
762 | - * |
|
763 | - * @return ClipboardService |
|
764 | - */ |
|
765 | - protected function getClipboardService() |
|
766 | - { |
|
767 | - return GeneralUtility::makeInstance(ClipboardService::class); |
|
768 | - } |
|
769 | - |
|
770 | - /** |
|
771 | - * @return LanguageService |
|
772 | - */ |
|
773 | - protected function getLanguageService() |
|
774 | - { |
|
775 | - return GeneralUtility::makeInstance(LanguageService::class); |
|
776 | - } |
|
777 | - |
|
778 | - /** |
|
779 | - * Get the Vidi Module Loader. |
|
780 | - * |
|
781 | - * @return ModuleLoader |
|
782 | - */ |
|
783 | - protected function getModuleLoader() |
|
784 | - { |
|
785 | - return GeneralUtility::makeInstance(ModuleLoader::class); |
|
786 | - } |
|
787 | - |
|
788 | - public function injectSelectionRepository(SelectionRepository $selectionRepository): void |
|
789 | - { |
|
790 | - $this->selectionRepository = $selectionRepository; |
|
791 | - } |
|
47 | + /** |
|
48 | + * Initialize every action. |
|
49 | + */ |
|
50 | + public function initializeAction() |
|
51 | + { |
|
52 | + $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); |
|
53 | + $pageRenderer->addInlineLanguageLabelFile('EXT:vidi/Resources/Private/Language/locallang.xlf'); |
|
54 | + |
|
55 | + // Configure property mapping to retrieve the file object. |
|
56 | + if ($this->arguments->hasArgument('columns')) { |
|
57 | + |
|
58 | + /** @var CsvToArrayConverter $typeConverter */ |
|
59 | + $typeConverter = GeneralUtility::makeInstance(CsvToArrayConverter::class); |
|
60 | + |
|
61 | + $propertyMappingConfiguration = $this->arguments->getArgument('columns')->getPropertyMappingConfiguration(); |
|
62 | + $propertyMappingConfiguration->setTypeConverter($typeConverter); |
|
63 | + } |
|
64 | + } |
|
65 | + |
|
66 | + /** |
|
67 | + * List action for this controller. |
|
68 | + * |
|
69 | + * @return void |
|
70 | + */ |
|
71 | + public function indexAction(): ResponseInterface |
|
72 | + { |
|
73 | + $dataType = $this->getModuleLoader()->getDataType(); |
|
74 | + $selections = $this->selectionRepository->findByDataTypeForCurrentBackendUser($dataType); |
|
75 | + $this->view->assign('selections', $selections); |
|
76 | + |
|
77 | + $columns = Tca::grid()->getFields(); |
|
78 | + $this->view->assign('columns', $columns); |
|
79 | + $this->view->assign('numberOfColumns', count($columns)); |
|
80 | + return $this->htmlResponse(); |
|
81 | + } |
|
82 | + |
|
83 | + /** |
|
84 | + * List Row action for this controller. Output a json list of contents |
|
85 | + * |
|
86 | + * @param array $columns corresponds to columns to be rendered. |
|
87 | + * @param array $matches |
|
88 | + * @Validate("Fab\Vidi\Domain\Validator\ColumnsValidator", param="columns") |
|
89 | + * @Validate("Fab\Vidi\Domain\Validator\MatchesValidator", param="matches") |
|
90 | + * @return void |
|
91 | + */ |
|
92 | + public function listAction(array $columns = [], $matches = []): ResponseInterface |
|
93 | + { |
|
94 | + // Initialize some objects related to the query. |
|
95 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
96 | + $order = OrderObjectFactory::getInstance()->getOrder(); |
|
97 | + $pager = PagerObjectFactory::getInstance()->getPager(); |
|
98 | + |
|
99 | + // Fetch objects via the Content Service. |
|
100 | + $contentService = $this->getContentService()->findBy($matcher, $order, $pager->getLimit(), $pager->getOffset()); |
|
101 | + $pager->setCount($contentService->getNumberOfObjects()); |
|
102 | + |
|
103 | + // Assign values. |
|
104 | + $this->view->assign('columns', $columns); |
|
105 | + $this->view->assign('objects', $contentService->getObjects()); |
|
106 | + $this->view->assign('numberOfObjects', $contentService->getNumberOfObjects()); |
|
107 | + $this->view->assign('pager', $pager); |
|
108 | + |
|
109 | + $this->view->assign('response', $this->responseFactory->createResponse()); |
|
110 | + return $this->htmlResponse(); |
|
111 | + } |
|
112 | + |
|
113 | + /** |
|
114 | + * Retrieve Content objects first according to matching criteria and then "update" them. |
|
115 | + * Important to notice the field name can contains a path, e.g. metadata.title and therefore must be analysed. |
|
116 | + * |
|
117 | + * Possible values for $matches: |
|
118 | + * ----------------------------- |
|
119 | + * |
|
120 | + * $matches = array(uid => 1), will be taken as $query->equals |
|
121 | + * $matches = array(uid => 1,2,3), will be taken as $query->in |
|
122 | + * $matches = array(field_name1 => bar, field_name2 => bax), will be separated by AND. |
|
123 | + * |
|
124 | + * Possible values for $content: |
|
125 | + * ----------------------------- |
|
126 | + * |
|
127 | + * $content = array(field_name => bar) |
|
128 | + * $content = array(field_name => array(value1, value2)) <-- will be CSV converted by "value1,value2" |
|
129 | + * |
|
130 | + * @param string $fieldNameAndPath |
|
131 | + * @param array $content |
|
132 | + * @param array $matches |
|
133 | + * @param string $savingBehavior |
|
134 | + * @param int $language |
|
135 | + * @param array $columns |
|
136 | + * @throws InvalidKeyInArrayException |
|
137 | + */ |
|
138 | + public function updateAction($fieldNameAndPath, array $content, array $matches = [], $savingBehavior = SavingBehavior::REPLACE, $language = 0, $columns = []) |
|
139 | + { |
|
140 | + |
|
141 | + // Instantiate the Matcher object according different rules. |
|
142 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
143 | + $order = OrderObjectFactory::getInstance()->getOrder(); |
|
144 | + |
|
145 | + // Fetch objects via the Content Service. |
|
146 | + $contentService = $this->getContentService()->findBy($matcher, $order); |
|
147 | + |
|
148 | + // Get the real field that is going to be updated. |
|
149 | + $updatedFieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath); |
|
150 | + |
|
151 | + // Get result object for storing data along the processing. |
|
152 | + $result = $this->getJsonResult(); |
|
153 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
154 | + |
|
155 | + foreach ($contentService->getObjects() as $index => $object) { |
|
156 | + |
|
157 | + $identifier = $this->getContentObjectResolver()->getValue($object, $fieldNameAndPath, 'uid', $language); |
|
158 | + |
|
159 | + // It could be the identifier is not found because the translation |
|
160 | + // of the record does not yet exist when mass-editing |
|
161 | + if ((int)$identifier <= 0) { |
|
162 | + continue; |
|
163 | + } |
|
164 | + |
|
165 | + $dataType = $this->getContentObjectResolver()->getDataType($object, $fieldNameAndPath); |
|
166 | + |
|
167 | + $signalResult = $this->emitProcessContentDataSignal($object, $fieldNameAndPath, $content, $index + 1, $savingBehavior, $language); |
|
168 | + $contentData = $signalResult->getContentData(); |
|
169 | + |
|
170 | + // Add identifier to content data, required by TCEMain. |
|
171 | + $contentData['uid'] = $identifier; |
|
172 | + |
|
173 | + /** @var Content $dataObject */ |
|
174 | + $dataObject = GeneralUtility::makeInstance(Content::class, $dataType, $contentData); |
|
175 | + |
|
176 | + // Properly update object. |
|
177 | + ContentRepositoryFactory::getInstance($dataType)->update($dataObject); |
|
178 | + |
|
179 | + // Get the possible error messages and store them. |
|
180 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
181 | + $result->addErrorMessages($errorMessages); |
|
182 | + |
|
183 | + // We only want to see the detail result if there is one object updated. |
|
184 | + // Required for inline editing + it will display some useful info on the GUI in the flash messages. |
|
185 | + if ($contentService->getNumberOfObjects() === 1) { |
|
186 | + |
|
187 | + // Fetch the updated object from repository. |
|
188 | + $updatedObject = ContentRepositoryFactory::getInstance()->findByUid($object->getUid()); |
|
189 | + |
|
190 | + // Re-fetch the updated result. |
|
191 | + $updatedResult = $this->getContentObjectResolver()->getValue($updatedObject, $fieldNameAndPath, $updatedFieldName, $language); |
|
192 | + if (is_array($updatedResult)) { |
|
193 | + $_updatedResult = []; // reset result set. |
|
194 | + |
|
195 | + /** @var Content $contentObject */ |
|
196 | + foreach ($updatedResult as $contentObject) { |
|
197 | + $labelField = Tca::table($contentObject)->getLabelField(); |
|
198 | + $values = array( |
|
199 | + 'uid' => $contentObject->getUid(), |
|
200 | + 'name' => $contentObject[$labelField], |
|
201 | + ); |
|
202 | + $_updatedResult[] = $values; |
|
203 | + } |
|
204 | + |
|
205 | + $updatedResult = $_updatedResult; |
|
206 | + } |
|
207 | + |
|
208 | + $labelField = Tca::table($object)->getLabelField(); |
|
209 | + |
|
210 | + $processedObjectData = array( |
|
211 | + 'uid' => $object->getUid(), |
|
212 | + 'name' => $object[$labelField], |
|
213 | + 'updatedField' => $fieldNameAndPath, |
|
214 | + 'updatedValue' => $updatedResult, |
|
215 | + ); |
|
216 | + $result->setProcessedObject($processedObjectData); |
|
217 | + |
|
218 | + if (!empty($columns)) { |
|
219 | + /** @var Row $row */ |
|
220 | + $row = GeneralUtility::makeInstance(Row::class, $columns); |
|
221 | + $result->setRow($row->render($updatedObject)); |
|
222 | + } |
|
223 | + } |
|
224 | + } |
|
225 | + |
|
226 | + $response = $this->responseFactory->createResponse() |
|
227 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
228 | + $response->getBody()->write(json_encode($result)); |
|
229 | + return $response; |
|
230 | + } |
|
231 | + |
|
232 | + /** |
|
233 | + * Set the sorting of a record giving the previous object. |
|
234 | + * |
|
235 | + * @param array $matches |
|
236 | + * @param int $previousIdentifier |
|
237 | + */ |
|
238 | + public function sortAction(array $matches = [], $previousIdentifier = null) |
|
239 | + { |
|
240 | + |
|
241 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
242 | + |
|
243 | + // Fetch objects via the Content Service. |
|
244 | + $contentService = $this->getContentService()->findBy($matcher); |
|
245 | + |
|
246 | + // Compute the label field name of the table. |
|
247 | + $tableTitleField = Tca::table()->getLabelField(); |
|
248 | + |
|
249 | + // Get result object for storing data along the processing. |
|
250 | + $result = $this->getJsonResult(); |
|
251 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
252 | + |
|
253 | + foreach ($contentService->getObjects() as $object) { |
|
254 | + |
|
255 | + // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
256 | + if ($contentService->getNumberOfObjects() === 1) { |
|
257 | + $tableTitleValue = $object[$tableTitleField]; |
|
258 | + $processedObjectData = array( |
|
259 | + 'uid' => $object->getUid(), |
|
260 | + 'name' => $tableTitleValue, |
|
261 | + ); |
|
262 | + $result->setProcessedObject($processedObjectData); |
|
263 | + } |
|
264 | + |
|
265 | + // The $target corresponds to the pid to move the records to. |
|
266 | + // It can also be a negative value in case of sorting. The negative value would be the uid of its predecessor. |
|
267 | + $target = is_null($previousIdentifier) ? $object->getPid() : (-(int)$previousIdentifier); |
|
268 | + |
|
269 | + // Work out the object. |
|
270 | + ContentRepositoryFactory::getInstance()->move($object, $target); |
|
271 | + |
|
272 | + // Get the possible error messages and store them. |
|
273 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
274 | + $result->addErrorMessages($errorMessages); |
|
275 | + } |
|
276 | + |
|
277 | + $response = $this->responseFactory->createResponse() |
|
278 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
279 | + $response->getBody()->write(json_encode($result)); |
|
280 | + return $response; |
|
281 | + } |
|
282 | + |
|
283 | + /** |
|
284 | + * Returns an editing form for a given field name of a Content object. |
|
285 | + * Argument $fieldNameAndPath corresponds to the field name to be edited. |
|
286 | + * Important to notice it can contains a path, e.g. metadata.title and therefore must be analysed. |
|
287 | + * |
|
288 | + * Possible values for $matches, refer to method "updateAction". |
|
289 | + * |
|
290 | + * @param string $fieldNameAndPath |
|
291 | + * @param array $matches |
|
292 | + * @param bool $hasRecursiveSelection |
|
293 | + * @throws \Exception |
|
294 | + */ |
|
295 | + public function editAction($fieldNameAndPath, array $matches = [], $hasRecursiveSelection = false): ResponseInterface |
|
296 | + { |
|
297 | + |
|
298 | + // Instantiate the Matcher object according different rules. |
|
299 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
300 | + |
|
301 | + // Fetch objects via the Content Service. |
|
302 | + $contentService = $this->getContentService()->findBy($matcher); |
|
303 | + |
|
304 | + $dataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath); |
|
305 | + $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath); |
|
306 | + |
|
307 | + $fieldType = Tca::table($dataType)->field($fieldName)->getType(); |
|
308 | + $this->view->assign('fieldType', ucfirst($fieldType)); |
|
309 | + $this->view->assign('dataType', $dataType); |
|
310 | + $this->view->assign('fieldName', $fieldName); |
|
311 | + $this->view->assign('matches', $matches); |
|
312 | + $this->view->assign('fieldNameAndPath', $fieldNameAndPath); |
|
313 | + $this->view->assign('numberOfObjects', $contentService->getNumberOfObjects()); |
|
314 | + $this->view->assign('hasRecursiveSelection', $hasRecursiveSelection); |
|
315 | + $this->view->assign('editWholeSelection', empty($matches['uid'])); // necessary?? |
|
316 | + |
|
317 | + // Fetch content and its relations. |
|
318 | + if ($fieldType === FieldType::MULTISELECT) { |
|
319 | + |
|
320 | + $object = ContentRepositoryFactory::getInstance()->findOneBy($matcher); |
|
321 | + $identifier = $this->getContentObjectResolver()->getValue($object, $fieldNameAndPath, 'uid'); |
|
322 | + $dataType = $this->getContentObjectResolver()->getDataType($object, $fieldNameAndPath); |
|
323 | + |
|
324 | + $content = ContentRepositoryFactory::getInstance($dataType)->findByUid($identifier); |
|
325 | + |
|
326 | + // Makes sure the object was retrieved. Security! |
|
327 | + if (!$content) { |
|
328 | + $message = sprintf('I could not retrieved content object of type "%s" with identifier %s.', $dataType, $identifier); |
|
329 | + throw new \Exception($message, 1402350182); |
|
330 | + } |
|
331 | + |
|
332 | + $relatedDataType = Tca::table($dataType)->field($fieldName)->getForeignTable(); |
|
333 | + |
|
334 | + // Initialize the matcher object. |
|
335 | + /** @var Matcher $matcher */ |
|
336 | + $matcher = GeneralUtility::makeInstance(Matcher::class, [], $relatedDataType); |
|
337 | + |
|
338 | + // Default ordering for related data type. |
|
339 | + $defaultOrderings = Tca::table($relatedDataType)->getDefaultOrderings(); |
|
340 | + /** @var Order $order */ |
|
341 | + $defaultOrder = GeneralUtility::makeInstance(Order::class, $defaultOrderings); |
|
342 | + |
|
343 | + // Fetch related contents |
|
344 | + $relatedContents = ContentRepositoryFactory::getInstance($relatedDataType)->findBy($matcher, $defaultOrder); |
|
345 | + |
|
346 | + if (Tca::table($dataType)->field($fieldName)->isRenderModeTree()) { |
|
347 | + |
|
348 | + $fieldConfiguration = Tca::table($dataType)->field($fieldName)->getConfiguration(); |
|
349 | + $parentField = $fieldConfiguration['treeConfig']['parentField']; |
|
350 | + |
|
351 | + $flatTree = []; |
|
352 | + foreach ($relatedContents as $node) { |
|
353 | + $flatTree[$node->getUid()] = array( |
|
354 | + 'item' => $node, |
|
355 | + 'parent' => $node[$parentField] ? $node[$parentField]['uid'] : null, |
|
356 | + ); |
|
357 | + } |
|
358 | + |
|
359 | + $tree = []; |
|
360 | + |
|
361 | + // If leaves are selected without its parents selected, those are shown as parent |
|
362 | + foreach ($flatTree as $id => &$flatNode) { |
|
363 | + if (!isset($flatTree[$flatNode['parent']])) { |
|
364 | + $flatNode['parent'] = null; |
|
365 | + } |
|
366 | + } |
|
367 | + |
|
368 | + foreach ($flatTree as $id => &$node) { |
|
369 | + if ($node['parent'] === null) { |
|
370 | + $tree[$id] = &$node; |
|
371 | + } else { |
|
372 | + $flatTree[$node['parent']]['children'][$id] = &$node; |
|
373 | + } |
|
374 | + } |
|
375 | + |
|
376 | + $relatedContents = $tree; |
|
377 | + } |
|
378 | + |
|
379 | + $this->view->assign('content', $content); |
|
380 | + $this->view->assign('relatedContents', $relatedContents); |
|
381 | + $this->view->assign('relatedDataType', $relatedDataType); |
|
382 | + $this->view->assign('relatedContentTitle', Tca::table($relatedDataType)->getTitle()); |
|
383 | + $this->view->assign( |
|
384 | + 'renderMode', |
|
385 | + Tca::table($dataType)->field($fieldName)->isRenderModeTree() ? FieldType::TREE : null |
|
386 | + ); |
|
387 | + } |
|
388 | + return $this->htmlResponse(); |
|
389 | + } |
|
390 | + |
|
391 | + /** |
|
392 | + * Retrieve Content objects first according to matching criteria and then "delete" them. |
|
393 | + * |
|
394 | + * Possible values for $matches, refer to method "updateAction". |
|
395 | + * |
|
396 | + * @param array $matches |
|
397 | + */ |
|
398 | + public function deleteAction(array $matches = []) |
|
399 | + { |
|
400 | + |
|
401 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
402 | + |
|
403 | + // Fetch objects via the Content Service. |
|
404 | + $contentService = $this->getContentService()->findBy($matcher); |
|
405 | + |
|
406 | + // Compute the label field name of the table. |
|
407 | + $tableTitleField = Tca::table()->getLabelField(); |
|
408 | + |
|
409 | + // Get result object for storing data along the processing. |
|
410 | + $result = $this->getJsonResult(); |
|
411 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
412 | + |
|
413 | + foreach ($contentService->getObjects() as $object) { |
|
414 | + |
|
415 | + // Store the first object, so that the delete message can be more explicit when deleting only one record. |
|
416 | + if ($contentService->getNumberOfObjects() === 1) { |
|
417 | + $tableTitleValue = $object[$tableTitleField]; |
|
418 | + $processedObjectData = array( |
|
419 | + 'uid' => $object->getUid(), |
|
420 | + 'name' => $tableTitleValue, |
|
421 | + ); |
|
422 | + $result->setProcessedObject($processedObjectData); |
|
423 | + } |
|
424 | + |
|
425 | + // Properly delete object. |
|
426 | + ContentRepositoryFactory::getInstance()->remove($object); |
|
427 | + |
|
428 | + // Get the possible error messages and store them. |
|
429 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
430 | + $result->addErrorMessages($errorMessages); |
|
431 | + } |
|
432 | + |
|
433 | + $response = $this->responseFactory->createResponse() |
|
434 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
435 | + $response->getBody()->write(json_encode($result)); |
|
436 | + return $response; |
|
437 | + } |
|
438 | + |
|
439 | + /** |
|
440 | + * Retrieve Content objects first according to matching criteria and then "copy" them. |
|
441 | + * |
|
442 | + * Possible values for $matches, refer to method "updateAction". |
|
443 | + * |
|
444 | + * @param string $target |
|
445 | + * @param array $matches |
|
446 | + * @throws \Exception |
|
447 | + * @return string |
|
448 | + */ |
|
449 | + public function copyAction($target, array $matches = []) |
|
450 | + { |
|
451 | + // @todo |
|
452 | + throw new \Exception('Not yet implemented', 1410192546); |
|
453 | + } |
|
454 | + |
|
455 | + /** |
|
456 | + * Retrieve Content objects from the Clipboard then "copy" them according to the target. |
|
457 | + * |
|
458 | + * @param string $target |
|
459 | + * @throws \Exception |
|
460 | + */ |
|
461 | + public function copyClipboardAction($target) |
|
462 | + { |
|
463 | + |
|
464 | + // Retrieve matcher object from clipboard. |
|
465 | + $matcher = $this->getClipboardService()->getMatcher(); |
|
466 | + |
|
467 | + // Fetch objects via the Content Service. |
|
468 | + $contentService = $this->getContentService()->findBy($matcher); |
|
469 | + |
|
470 | + // Compute the label field name of the table. |
|
471 | + $tableTitleField = Tca::table()->getLabelField(); |
|
472 | + |
|
473 | + // Get result object for storing data along the processing. |
|
474 | + $result = $this->getJsonResult(); |
|
475 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
476 | + |
|
477 | + foreach ($contentService->getObjects() as $object) { |
|
478 | + |
|
479 | + // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
480 | + if ($contentService->getNumberOfObjects() === 1) { |
|
481 | + $tableTitleValue = $object[$tableTitleField]; |
|
482 | + $processedObjectData = array( |
|
483 | + 'uid' => $object->getUid(), |
|
484 | + 'name' => $tableTitleValue, |
|
485 | + ); |
|
486 | + $result->setProcessedObject($processedObjectData); |
|
487 | + } |
|
488 | + |
|
489 | + // Work out the object. |
|
490 | + ContentRepositoryFactory::getInstance()->copy($object, $target); |
|
491 | + |
|
492 | + // Get the possible error messages and store them. |
|
493 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
494 | + $result->addErrorMessages($errorMessages); |
|
495 | + } |
|
496 | + |
|
497 | + // Flush Clipboard if told so. |
|
498 | + if (GeneralUtility::_GP('flushClipboard')) { |
|
499 | + $this->getClipboardService()->flush(); |
|
500 | + } |
|
501 | + |
|
502 | + $response = $this->responseFactory->createResponse() |
|
503 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
504 | + $response->getBody()->write(json_encode($result)); |
|
505 | + return $response; |
|
506 | + } |
|
507 | + |
|
508 | + /** |
|
509 | + * Retrieve Content objects first according to matching criteria and then "move" them. |
|
510 | + * |
|
511 | + * Possible values for $matches, refer to method "updateAction". |
|
512 | + * |
|
513 | + * @param string $target |
|
514 | + * @param array $matches |
|
515 | + */ |
|
516 | + public function moveAction($target, array $matches = []) |
|
517 | + { |
|
518 | + |
|
519 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
520 | + |
|
521 | + // Fetch objects via the Content Service. |
|
522 | + $contentService = $this->getContentService()->findBy($matcher); |
|
523 | + |
|
524 | + // Compute the label field name of the table. |
|
525 | + $tableTitleField = Tca::table()->getLabelField(); |
|
526 | + |
|
527 | + // Get result object for storing data along the processing. |
|
528 | + $result = $this->getJsonResult(); |
|
529 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
530 | + |
|
531 | + foreach ($contentService->getObjects() as $object) { |
|
532 | + |
|
533 | + // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
534 | + if ($contentService->getNumberOfObjects() === 1) { |
|
535 | + $tableTitleValue = $object[$tableTitleField]; |
|
536 | + $processedObjectData = array( |
|
537 | + 'uid' => $object->getUid(), |
|
538 | + 'name' => $tableTitleValue, |
|
539 | + ); |
|
540 | + $result->setProcessedObject($processedObjectData); |
|
541 | + } |
|
542 | + |
|
543 | + // Work out the object. |
|
544 | + ContentRepositoryFactory::getInstance()->move($object, $target); |
|
545 | + |
|
546 | + // Get the possible error messages and store them. |
|
547 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
548 | + $result->addErrorMessages($errorMessages); |
|
549 | + } |
|
550 | + |
|
551 | + $response = $this->responseFactory->createResponse() |
|
552 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
553 | + $response->getBody()->write(json_encode($result)); |
|
554 | + return $response; |
|
555 | + } |
|
556 | + |
|
557 | + /** |
|
558 | + * Retrieve Content objects from the Clipboard then "move" them according to the target. |
|
559 | + * |
|
560 | + * @param string $target |
|
561 | + */ |
|
562 | + public function moveClipboardAction($target) |
|
563 | + { |
|
564 | + |
|
565 | + // Retrieve matcher object from clipboard. |
|
566 | + $matcher = $this->getClipboardService()->getMatcher(); |
|
567 | + |
|
568 | + // Fetch objects via the Content Service. |
|
569 | + $contentService = $this->getContentService()->findBy($matcher); |
|
570 | + |
|
571 | + // Compute the label field name of the table. |
|
572 | + $tableTitleField = Tca::table()->getLabelField(); |
|
573 | + |
|
574 | + // Get result object for storing data along the processing. |
|
575 | + $result = $this->getJsonResult(); |
|
576 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
577 | + |
|
578 | + foreach ($contentService->getObjects() as $object) { |
|
579 | + |
|
580 | + // Store the first object, so that the "action" message can be more explicit when deleting only one record. |
|
581 | + if ($contentService->getNumberOfObjects() === 1) { |
|
582 | + $tableTitleValue = $object[$tableTitleField]; |
|
583 | + $processedObjectData = array( |
|
584 | + 'uid' => $object->getUid(), |
|
585 | + 'name' => $tableTitleValue, |
|
586 | + ); |
|
587 | + $result->setProcessedObject($processedObjectData); |
|
588 | + } |
|
589 | + |
|
590 | + // Work out the object. |
|
591 | + ContentRepositoryFactory::getInstance()->move($object, $target); |
|
592 | + |
|
593 | + // Get the possible error messages and store them. |
|
594 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
595 | + $result->addErrorMessages($errorMessages); |
|
596 | + } |
|
597 | + |
|
598 | + // Flush Clipboard if told so. |
|
599 | + if (GeneralUtility::_GP('flushClipboard')) { |
|
600 | + $this->getClipboardService()->flush(); |
|
601 | + } |
|
602 | + |
|
603 | + $response = $this->responseFactory->createResponse() |
|
604 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
605 | + $response->getBody()->write(json_encode($result)); |
|
606 | + return $response; |
|
607 | + } |
|
608 | + |
|
609 | + /** |
|
610 | + * Retrieve Content objects first according to matching criteria and then "localize" them. |
|
611 | + * |
|
612 | + * Possible values for $matches, refer to method "updateAction". |
|
613 | + * |
|
614 | + * @param string $fieldNameAndPath |
|
615 | + * @param array $matches |
|
616 | + * @param int $language |
|
617 | + * @throws \Exception |
|
618 | + */ |
|
619 | + public function localizeAction($fieldNameAndPath, array $matches = [], $language = 0) |
|
620 | + { |
|
621 | + |
|
622 | + $matcher = MatcherObjectFactory::getInstance()->getMatcher($matches); |
|
623 | + |
|
624 | + // Fetch objects via the Content Service. |
|
625 | + $contentService = $this->getContentService()->findBy($matcher); |
|
626 | + |
|
627 | + // Get result object for storing data along the processing. |
|
628 | + $result = $this->getJsonResult(); |
|
629 | + $result->setNumberOfObjects($contentService->getNumberOfObjects()); |
|
630 | + |
|
631 | + foreach ($contentService->getObjects() as $object) { |
|
632 | + |
|
633 | + $identifier = $this->getContentObjectResolver()->getValue($object, $fieldNameAndPath, 'uid'); |
|
634 | + $dataType = $this->getContentObjectResolver()->getDataType($object, $fieldNameAndPath); |
|
635 | + |
|
636 | + // Fetch the source object to be localized. |
|
637 | + /** @var Content $content */ |
|
638 | + $content = ContentRepositoryFactory::getInstance($dataType)->findByIdentifier($identifier); |
|
639 | + |
|
640 | + // Makes sure the object was retrieved. Security! |
|
641 | + if (!$content) { |
|
642 | + $message = sprintf('Something went wrong when retrieving content "%s" with identifier "%s".', $dataType, $identifier); |
|
643 | + throw new \Exception($message, 1412343097); |
|
644 | + } |
|
645 | + |
|
646 | + // Handover the localization to the Repository. |
|
647 | + ContentRepositoryFactory::getInstance($dataType)->localize($content, $language); |
|
648 | + |
|
649 | + // Get the possible error messages and store them. |
|
650 | + $errorMessages = ContentRepositoryFactory::getInstance()->getErrorMessages(); |
|
651 | + |
|
652 | + // Redirect to TCEForm so that the BE User can do its job! |
|
653 | + if ($contentService->getNumberOfObjects() === 1) { |
|
654 | + |
|
655 | + if (!empty($errorMessages)) { |
|
656 | + $message = sprintf('Something went wrong when localizing content "%s" with identifier "%s". <br/>%s', |
|
657 | + $dataType, |
|
658 | + $identifier, |
|
659 | + implode('<br/>', $errorMessages) |
|
660 | + ); |
|
661 | + throw new \Exception($message, 1412343098); |
|
662 | + } |
|
663 | + |
|
664 | + $localizedContent = $this->getLanguageService()->getLocalizedContent($content, $language); |
|
665 | + if (empty($localizedContent)) { |
|
666 | + $message = sprintf('Oups! I could not retrieve localized content of type "%s" with identifier "%s"', |
|
667 | + $content->getDataType(), |
|
668 | + $content->getUid() |
|
669 | + ); |
|
670 | + throw new \Exception($message, 1412343099); |
|
671 | + } |
|
672 | + |
|
673 | + /** @var EditUri $uri */ |
|
674 | + $uriRenderer = GeneralUtility::makeInstance(EditUri::class); |
|
675 | + $uri = $uriRenderer->render($localizedContent); |
|
676 | + HttpUtility::redirect($uri); |
|
677 | + break; // no need to further continue |
|
678 | + } |
|
679 | + |
|
680 | + $result->addErrorMessages($errorMessages); |
|
681 | + } |
|
682 | + |
|
683 | + $response = $this->responseFactory->createResponse() |
|
684 | + ->withHeader('Content-Type', 'application/json; charset=utf-8'); |
|
685 | + $response->getBody()->write(json_encode($result)); |
|
686 | + return $response; |
|
687 | + } |
|
688 | + |
|
689 | + /** |
|
690 | + * Get the Vidi Module Loader. |
|
691 | + * |
|
692 | + * @return ContentService |
|
693 | + */ |
|
694 | + protected function getContentService() |
|
695 | + { |
|
696 | + return GeneralUtility::makeInstance(ContentService::class); |
|
697 | + } |
|
698 | + |
|
699 | + /** |
|
700 | + * @return ContentObjectResolver |
|
701 | + */ |
|
702 | + protected function getContentObjectResolver() |
|
703 | + { |
|
704 | + return GeneralUtility::makeInstance(ContentObjectResolver::class); |
|
705 | + } |
|
706 | + |
|
707 | + /** |
|
708 | + * @return FieldPathResolver |
|
709 | + */ |
|
710 | + protected function getFieldPathResolver() |
|
711 | + { |
|
712 | + return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
713 | + } |
|
714 | + |
|
715 | + /** |
|
716 | + * @return JsonResult|object |
|
717 | + */ |
|
718 | + protected function getJsonResult() |
|
719 | + { |
|
720 | + return GeneralUtility::makeInstance(JsonResult::class); |
|
721 | + } |
|
722 | + |
|
723 | + /** |
|
724 | + * Signal that is called for post-processing content data send to the server for update. |
|
725 | + * |
|
726 | + * @param Content $contentObject |
|
727 | + * @param $fieldNameAndPath |
|
728 | + * @param $contentData |
|
729 | + * @param $counter |
|
730 | + * @param $savingBehavior |
|
731 | + * @param $language |
|
732 | + * @return ProcessContentDataSignalArguments |
|
733 | + */ |
|
734 | + protected function emitProcessContentDataSignal(Content $contentObject, $fieldNameAndPath, $contentData, $counter, $savingBehavior, $language) |
|
735 | + { |
|
736 | + |
|
737 | + /** @var ProcessContentDataSignalArguments $signalArguments */ |
|
738 | + $signalArguments = GeneralUtility::makeInstance(ProcessContentDataSignalArguments::class); |
|
739 | + $signalArguments->setContentObject($contentObject) |
|
740 | + ->setFieldNameAndPath($fieldNameAndPath) |
|
741 | + ->setContentData($contentData) |
|
742 | + ->setCounter($counter) |
|
743 | + ->setSavingBehavior($savingBehavior) |
|
744 | + ->setLanguage($language); |
|
745 | + |
|
746 | + $signalResult = $this->getSignalSlotDispatcher()->dispatch('Fab\Vidi\Controller\Backend\ContentController', 'processContentData', array($signalArguments)); |
|
747 | + return $signalResult[0]; |
|
748 | + } |
|
749 | + |
|
750 | + /** |
|
751 | + * Get the SignalSlot dispatcher. |
|
752 | + * |
|
753 | + * @return Dispatcher |
|
754 | + */ |
|
755 | + protected function getSignalSlotDispatcher() |
|
756 | + { |
|
757 | + return GeneralUtility::makeInstance(Dispatcher::class); |
|
758 | + } |
|
759 | + |
|
760 | + /** |
|
761 | + * Get the Clipboard service. |
|
762 | + * |
|
763 | + * @return ClipboardService |
|
764 | + */ |
|
765 | + protected function getClipboardService() |
|
766 | + { |
|
767 | + return GeneralUtility::makeInstance(ClipboardService::class); |
|
768 | + } |
|
769 | + |
|
770 | + /** |
|
771 | + * @return LanguageService |
|
772 | + */ |
|
773 | + protected function getLanguageService() |
|
774 | + { |
|
775 | + return GeneralUtility::makeInstance(LanguageService::class); |
|
776 | + } |
|
777 | + |
|
778 | + /** |
|
779 | + * Get the Vidi Module Loader. |
|
780 | + * |
|
781 | + * @return ModuleLoader |
|
782 | + */ |
|
783 | + protected function getModuleLoader() |
|
784 | + { |
|
785 | + return GeneralUtility::makeInstance(ModuleLoader::class); |
|
786 | + } |
|
787 | + |
|
788 | + public function injectSelectionRepository(SelectionRepository $selectionRepository): void |
|
789 | + { |
|
790 | + $this->selectionRepository = $selectionRepository; |
|
791 | + } |
|
792 | 792 | |
793 | 793 | } |
@@ -24,88 +24,88 @@ |
||
24 | 24 | class SelectionController extends ActionController |
25 | 25 | { |
26 | 26 | |
27 | - /** |
|
28 | - * @param Selection $selection |
|
29 | - */ |
|
30 | - public function createAction(Selection $selection = null) |
|
31 | - { |
|
32 | - $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
33 | - $selection->setDataType($this->getModuleLoader()->getDataType()); |
|
27 | + /** |
|
28 | + * @param Selection $selection |
|
29 | + */ |
|
30 | + public function createAction(Selection $selection = null) |
|
31 | + { |
|
32 | + $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
33 | + $selection->setDataType($this->getModuleLoader()->getDataType()); |
|
34 | 34 | |
35 | - $selection->setOwner($this->getBackendUser()->user['uid']); |
|
36 | - $selectionRepository->add($selection); |
|
37 | - $this->redirect('edit', 'Selection', 'vidi', array('dataType' => $selection->getDataType())); |
|
38 | - } |
|
35 | + $selection->setOwner($this->getBackendUser()->user['uid']); |
|
36 | + $selectionRepository->add($selection); |
|
37 | + $this->redirect('edit', 'Selection', 'vidi', array('dataType' => $selection->getDataType())); |
|
38 | + } |
|
39 | 39 | |
40 | - /** |
|
41 | - * @param Selection $selection |
|
42 | - * @return string |
|
43 | - */ |
|
44 | - public function deleteAction(Selection $selection) |
|
45 | - { |
|
46 | - $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
47 | - $selectionRepository->remove($selection); |
|
48 | - return 'ok'; |
|
49 | - } |
|
40 | + /** |
|
41 | + * @param Selection $selection |
|
42 | + * @return string |
|
43 | + */ |
|
44 | + public function deleteAction(Selection $selection) |
|
45 | + { |
|
46 | + $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
47 | + $selectionRepository->remove($selection); |
|
48 | + return 'ok'; |
|
49 | + } |
|
50 | 50 | |
51 | - /** |
|
52 | - * @param Selection $selection |
|
53 | - */ |
|
54 | - public function updateAction(Selection $selection) |
|
55 | - { |
|
56 | - $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
57 | - $selectionRepository->update($selection); |
|
58 | - $this->redirect('show', 'Selection', 'vidi', array('selection' => $selection->getUid())); |
|
59 | - } |
|
51 | + /** |
|
52 | + * @param Selection $selection |
|
53 | + */ |
|
54 | + public function updateAction(Selection $selection) |
|
55 | + { |
|
56 | + $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
57 | + $selectionRepository->update($selection); |
|
58 | + $this->redirect('show', 'Selection', 'vidi', array('selection' => $selection->getUid())); |
|
59 | + } |
|
60 | 60 | |
61 | - /** |
|
62 | - * @param Selection $selection |
|
63 | - */ |
|
64 | - public function showAction(Selection $selection) |
|
65 | - { |
|
66 | - $this->view->assign('selection', $selection); |
|
67 | - } |
|
61 | + /** |
|
62 | + * @param Selection $selection |
|
63 | + */ |
|
64 | + public function showAction(Selection $selection) |
|
65 | + { |
|
66 | + $this->view->assign('selection', $selection); |
|
67 | + } |
|
68 | 68 | |
69 | - /** |
|
70 | - * Returns an editing form for a given data type. |
|
71 | - * |
|
72 | - * @param string $dataType |
|
73 | - */ |
|
74 | - public function editAction($dataType) |
|
75 | - { |
|
76 | - $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
77 | - $selections = $selectionRepository->findByDataTypeForCurrentBackendUser($dataType); |
|
78 | - $this->view->assign('selections', $selections); |
|
79 | - } |
|
69 | + /** |
|
70 | + * Returns an editing form for a given data type. |
|
71 | + * |
|
72 | + * @param string $dataType |
|
73 | + */ |
|
74 | + public function editAction($dataType) |
|
75 | + { |
|
76 | + $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
77 | + $selections = $selectionRepository->findByDataTypeForCurrentBackendUser($dataType); |
|
78 | + $this->view->assign('selections', $selections); |
|
79 | + } |
|
80 | 80 | |
81 | - /** |
|
82 | - * @param string $dataType |
|
83 | - */ |
|
84 | - public function listAction($dataType) |
|
85 | - { |
|
86 | - $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
87 | - $selections = $selectionRepository->findByDataTypeForCurrentBackendUser($dataType); |
|
88 | - $this->view->assign('selections', $selections); |
|
89 | - } |
|
81 | + /** |
|
82 | + * @param string $dataType |
|
83 | + */ |
|
84 | + public function listAction($dataType) |
|
85 | + { |
|
86 | + $selectionRepository = GeneralUtility::makeInstance(SelectionRepository::class); |
|
87 | + $selections = $selectionRepository->findByDataTypeForCurrentBackendUser($dataType); |
|
88 | + $this->view->assign('selections', $selections); |
|
89 | + } |
|
90 | 90 | |
91 | - /** |
|
92 | - * Get the Vidi Module Loader. |
|
93 | - * |
|
94 | - * @return ModuleLoader |
|
95 | - */ |
|
96 | - protected function getModuleLoader() |
|
97 | - { |
|
98 | - return GeneralUtility::makeInstance(ModuleLoader::class); |
|
99 | - } |
|
91 | + /** |
|
92 | + * Get the Vidi Module Loader. |
|
93 | + * |
|
94 | + * @return ModuleLoader |
|
95 | + */ |
|
96 | + protected function getModuleLoader() |
|
97 | + { |
|
98 | + return GeneralUtility::makeInstance(ModuleLoader::class); |
|
99 | + } |
|
100 | 100 | |
101 | - /** |
|
102 | - * Returns an instance of the current Backend User. |
|
103 | - * |
|
104 | - * @return BackendUserAuthentication |
|
105 | - */ |
|
106 | - protected function getBackendUser() |
|
107 | - { |
|
108 | - return $GLOBALS['BE_USER']; |
|
109 | - } |
|
101 | + /** |
|
102 | + * Returns an instance of the current Backend User. |
|
103 | + * |
|
104 | + * @return BackendUserAuthentication |
|
105 | + */ |
|
106 | + protected function getBackendUser() |
|
107 | + { |
|
108 | + return $GLOBALS['BE_USER']; |
|
109 | + } |
|
110 | 110 | |
111 | 111 | } |
@@ -18,42 +18,42 @@ |
||
18 | 18 | abstract class AbstractTool implements ToolInterface |
19 | 19 | { |
20 | 20 | |
21 | - /** |
|
22 | - * @param string $templateNameAndPath |
|
23 | - * @return StandaloneView |
|
24 | - * @throws \InvalidArgumentException |
|
25 | - */ |
|
26 | - protected function initializeStandaloneView($templateNameAndPath) |
|
27 | - { |
|
28 | - |
|
29 | - $templateNameAndPath = GeneralUtility::getFileAbsFileName($templateNameAndPath); |
|
30 | - |
|
31 | - /** @var StandaloneView $view */ |
|
32 | - $view = GeneralUtility::makeInstance(StandaloneView::class); |
|
33 | - |
|
34 | - $view->setTemplatePathAndFilename($templateNameAndPath); |
|
35 | - return $view; |
|
36 | - } |
|
37 | - |
|
38 | - /** |
|
39 | - * Returns an instance of the current Backend User. |
|
40 | - * |
|
41 | - * @return BackendUserAuthentication |
|
42 | - */ |
|
43 | - protected function getBackendUser() |
|
44 | - { |
|
45 | - return $GLOBALS['BE_USER']; |
|
46 | - } |
|
47 | - |
|
48 | - /** |
|
49 | - * Get the Vidi Module Loader. |
|
50 | - * |
|
51 | - * @return ModuleLoader |
|
52 | - * @throws \InvalidArgumentException |
|
53 | - */ |
|
54 | - protected function getModuleLoader() |
|
55 | - { |
|
56 | - return GeneralUtility::makeInstance(ModuleLoader::class); |
|
57 | - } |
|
21 | + /** |
|
22 | + * @param string $templateNameAndPath |
|
23 | + * @return StandaloneView |
|
24 | + * @throws \InvalidArgumentException |
|
25 | + */ |
|
26 | + protected function initializeStandaloneView($templateNameAndPath) |
|
27 | + { |
|
28 | + |
|
29 | + $templateNameAndPath = GeneralUtility::getFileAbsFileName($templateNameAndPath); |
|
30 | + |
|
31 | + /** @var StandaloneView $view */ |
|
32 | + $view = GeneralUtility::makeInstance(StandaloneView::class); |
|
33 | + |
|
34 | + $view->setTemplatePathAndFilename($templateNameAndPath); |
|
35 | + return $view; |
|
36 | + } |
|
37 | + |
|
38 | + /** |
|
39 | + * Returns an instance of the current Backend User. |
|
40 | + * |
|
41 | + * @return BackendUserAuthentication |
|
42 | + */ |
|
43 | + protected function getBackendUser() |
|
44 | + { |
|
45 | + return $GLOBALS['BE_USER']; |
|
46 | + } |
|
47 | + |
|
48 | + /** |
|
49 | + * Get the Vidi Module Loader. |
|
50 | + * |
|
51 | + * @return ModuleLoader |
|
52 | + * @throws \InvalidArgumentException |
|
53 | + */ |
|
54 | + protected function getModuleLoader() |
|
55 | + { |
|
56 | + return GeneralUtility::makeInstance(ModuleLoader::class); |
|
57 | + } |
|
58 | 58 | |
59 | 59 | } |
@@ -18,220 +18,220 @@ |
||
18 | 18 | class ModulePreferences implements SingletonInterface |
19 | 19 | { |
20 | 20 | |
21 | - /** |
|
22 | - * @var array |
|
23 | - */ |
|
24 | - protected $preferences; |
|
25 | - |
|
26 | - /** |
|
27 | - * @var string |
|
28 | - */ |
|
29 | - protected $tableName = 'tx_vidi_preference'; |
|
30 | - |
|
31 | - /** |
|
32 | - * @param string $key |
|
33 | - * @param string $dataType |
|
34 | - * @return mixed |
|
35 | - */ |
|
36 | - public function get($key, $dataType = '') |
|
37 | - { |
|
38 | - |
|
39 | - if (empty($dataType)) { |
|
40 | - $dataType = $this->getModuleLoader()->getDataType(); |
|
41 | - } |
|
42 | - |
|
43 | - if (!$this->isLoaded($dataType)) { |
|
44 | - $this->load($dataType); |
|
45 | - } |
|
46 | - |
|
47 | - $value = empty($this->preferences[$dataType][$key]) ? null : $this->preferences[$dataType][$key]; |
|
48 | - return $value; |
|
49 | - } |
|
50 | - |
|
51 | - /** |
|
52 | - * Tell whether the module is loaded. |
|
53 | - * |
|
54 | - * @param string $dataType |
|
55 | - * @return bool |
|
56 | - */ |
|
57 | - public function isLoaded($dataType) |
|
58 | - { |
|
59 | - return !empty($this->preferences[$dataType]); |
|
60 | - } |
|
61 | - |
|
62 | - /** |
|
63 | - * @param string $dataType |
|
64 | - * @return array |
|
65 | - */ |
|
66 | - public function getAll($dataType = '') |
|
67 | - { |
|
68 | - |
|
69 | - if (empty($dataType)) { |
|
70 | - $dataType = $this->getModuleLoader()->getDataType(); |
|
71 | - } |
|
72 | - $this->load($dataType); |
|
73 | - return $this->preferences[$dataType]; |
|
74 | - } |
|
75 | - |
|
76 | - /** |
|
77 | - * Get the md5 signature of the preferences. |
|
78 | - * |
|
79 | - * @param string $dataType |
|
80 | - * @return bool |
|
81 | - */ |
|
82 | - public function getSignature($dataType = '') |
|
83 | - { |
|
84 | - $preferences = $this->getAll($dataType); |
|
85 | - return md5(serialize($preferences)); |
|
86 | - } |
|
87 | - |
|
88 | - /** |
|
89 | - * Load preferences. |
|
90 | - * |
|
91 | - * @param string $dataType |
|
92 | - * @return void |
|
93 | - */ |
|
94 | - public function load($dataType) |
|
95 | - { |
|
96 | - |
|
97 | - // Fetch preferences from different sources and overlay them |
|
98 | - $databasePreferences = $this->fetchPreferencesFromDatabase($dataType); |
|
99 | - $generalPreferences = $this->fetchGlobalPreferencesFromTypoScript(); |
|
100 | - $specificPreferences = $this->fetchExtraPreferencesFromTypoScript($dataType); |
|
101 | - |
|
102 | - $preferences = array_merge($generalPreferences, $specificPreferences, $databasePreferences); |
|
103 | - $this->preferences[$dataType] = $preferences; |
|
104 | - } |
|
105 | - |
|
106 | - /** |
|
107 | - * Save preferences |
|
108 | - * |
|
109 | - * @param array $preferences |
|
110 | - * @return void |
|
111 | - */ |
|
112 | - public function save($preferences) |
|
113 | - { |
|
114 | - $configurableParts = ConfigurablePart::getParts(); |
|
115 | - |
|
116 | - $dataType = $this->getModuleLoader()->getDataType(); |
|
117 | - $this->getDataService()->delete( |
|
118 | - $this->tableName, |
|
119 | - [ |
|
120 | - 'data_type' => $dataType |
|
121 | - ] |
|
122 | - ); |
|
123 | - |
|
124 | - $sanitizedPreferences = []; |
|
125 | - foreach ($preferences as $key => $value) { |
|
126 | - if (in_array($key, $configurableParts)) { |
|
127 | - $sanitizedPreferences[$key] = $value; |
|
128 | - } |
|
129 | - } |
|
130 | - |
|
131 | - $this->getDataService()->insert( |
|
132 | - $this->tableName, |
|
133 | - [ |
|
134 | - 'data_type' => $dataType, |
|
135 | - 'preferences' => serialize($sanitizedPreferences), |
|
136 | - ] |
|
137 | - ); |
|
138 | - } |
|
139 | - |
|
140 | - /** |
|
141 | - * @param $dataType |
|
142 | - * @return array |
|
143 | - */ |
|
144 | - public function fetchPreferencesFromDatabase($dataType) |
|
145 | - { |
|
146 | - $preferences = []; |
|
147 | - $record = $this->getDataService()->getRecord( |
|
148 | - $this->tableName, |
|
149 | - [ |
|
150 | - 'data_type' => $dataType |
|
151 | - ] |
|
152 | - ); |
|
153 | - |
|
154 | - if (!empty($record)) { |
|
155 | - $preferences = unserialize($record['preferences']); |
|
156 | - } |
|
157 | - |
|
158 | - return $preferences; |
|
159 | - } |
|
160 | - |
|
161 | - /** |
|
162 | - * Returns the module settings. |
|
163 | - * |
|
164 | - * @return array |
|
165 | - */ |
|
166 | - protected function fetchGlobalPreferencesFromTypoScript() |
|
167 | - { |
|
168 | - $settings = $this->getSettings(); |
|
169 | - |
|
170 | - $configurableParts = ConfigurablePart::getParts(); |
|
171 | - $preferences = []; |
|
172 | - foreach ($settings as $key => $value) { |
|
173 | - if (in_array($key, $configurableParts)) { |
|
174 | - $preferences[$key] = $value; |
|
175 | - } |
|
176 | - } |
|
177 | - |
|
178 | - return $preferences; |
|
179 | - } |
|
180 | - |
|
181 | - /** |
|
182 | - * Returns the module settings. |
|
183 | - * |
|
184 | - * @param string $dataType |
|
185 | - * @return array |
|
186 | - */ |
|
187 | - protected function fetchExtraPreferencesFromTypoScript($dataType) |
|
188 | - { |
|
189 | - $generalSettings = $this->getSettings(); |
|
190 | - |
|
191 | - $preferences = []; |
|
192 | - if (isset($generalSettings[$dataType . '.'])) { |
|
193 | - $settings = $generalSettings[$dataType . '.']; |
|
194 | - |
|
195 | - $configurableParts = ConfigurablePart::getParts(); |
|
196 | - foreach ($settings as $key => $value) { |
|
197 | - if (in_array($key, $configurableParts)) { |
|
198 | - $preferences[$key] = $value; |
|
199 | - } |
|
200 | - } |
|
201 | - } |
|
202 | - |
|
203 | - return $preferences; |
|
204 | - } |
|
205 | - |
|
206 | - /** |
|
207 | - * Returns the module settings. |
|
208 | - * |
|
209 | - * @return array |
|
210 | - */ |
|
211 | - protected function getSettings() |
|
212 | - { |
|
213 | - /** @var BackendConfigurationManager $backendConfigurationManager */ |
|
214 | - $backendConfigurationManager = GeneralUtility::makeInstance(BackendConfigurationManager::class); |
|
215 | - $configuration = $backendConfigurationManager->getTypoScriptSetup(); |
|
216 | - return $configuration['module.']['tx_vidi.']['settings.']; |
|
217 | - } |
|
218 | - |
|
219 | - /** |
|
220 | - * @return object|DataService |
|
221 | - */ |
|
222 | - protected function getDataService(): DataService |
|
223 | - { |
|
224 | - return GeneralUtility::makeInstance(DataService::class); |
|
225 | - } |
|
226 | - |
|
227 | - /** |
|
228 | - * Get the Vidi Module Loader. |
|
229 | - * |
|
230 | - * @return ModuleLoader|object |
|
231 | - */ |
|
232 | - protected function getModuleLoader() |
|
233 | - { |
|
234 | - return GeneralUtility::makeInstance(ModuleLoader::class); |
|
235 | - } |
|
21 | + /** |
|
22 | + * @var array |
|
23 | + */ |
|
24 | + protected $preferences; |
|
25 | + |
|
26 | + /** |
|
27 | + * @var string |
|
28 | + */ |
|
29 | + protected $tableName = 'tx_vidi_preference'; |
|
30 | + |
|
31 | + /** |
|
32 | + * @param string $key |
|
33 | + * @param string $dataType |
|
34 | + * @return mixed |
|
35 | + */ |
|
36 | + public function get($key, $dataType = '') |
|
37 | + { |
|
38 | + |
|
39 | + if (empty($dataType)) { |
|
40 | + $dataType = $this->getModuleLoader()->getDataType(); |
|
41 | + } |
|
42 | + |
|
43 | + if (!$this->isLoaded($dataType)) { |
|
44 | + $this->load($dataType); |
|
45 | + } |
|
46 | + |
|
47 | + $value = empty($this->preferences[$dataType][$key]) ? null : $this->preferences[$dataType][$key]; |
|
48 | + return $value; |
|
49 | + } |
|
50 | + |
|
51 | + /** |
|
52 | + * Tell whether the module is loaded. |
|
53 | + * |
|
54 | + * @param string $dataType |
|
55 | + * @return bool |
|
56 | + */ |
|
57 | + public function isLoaded($dataType) |
|
58 | + { |
|
59 | + return !empty($this->preferences[$dataType]); |
|
60 | + } |
|
61 | + |
|
62 | + /** |
|
63 | + * @param string $dataType |
|
64 | + * @return array |
|
65 | + */ |
|
66 | + public function getAll($dataType = '') |
|
67 | + { |
|
68 | + |
|
69 | + if (empty($dataType)) { |
|
70 | + $dataType = $this->getModuleLoader()->getDataType(); |
|
71 | + } |
|
72 | + $this->load($dataType); |
|
73 | + return $this->preferences[$dataType]; |
|
74 | + } |
|
75 | + |
|
76 | + /** |
|
77 | + * Get the md5 signature of the preferences. |
|
78 | + * |
|
79 | + * @param string $dataType |
|
80 | + * @return bool |
|
81 | + */ |
|
82 | + public function getSignature($dataType = '') |
|
83 | + { |
|
84 | + $preferences = $this->getAll($dataType); |
|
85 | + return md5(serialize($preferences)); |
|
86 | + } |
|
87 | + |
|
88 | + /** |
|
89 | + * Load preferences. |
|
90 | + * |
|
91 | + * @param string $dataType |
|
92 | + * @return void |
|
93 | + */ |
|
94 | + public function load($dataType) |
|
95 | + { |
|
96 | + |
|
97 | + // Fetch preferences from different sources and overlay them |
|
98 | + $databasePreferences = $this->fetchPreferencesFromDatabase($dataType); |
|
99 | + $generalPreferences = $this->fetchGlobalPreferencesFromTypoScript(); |
|
100 | + $specificPreferences = $this->fetchExtraPreferencesFromTypoScript($dataType); |
|
101 | + |
|
102 | + $preferences = array_merge($generalPreferences, $specificPreferences, $databasePreferences); |
|
103 | + $this->preferences[$dataType] = $preferences; |
|
104 | + } |
|
105 | + |
|
106 | + /** |
|
107 | + * Save preferences |
|
108 | + * |
|
109 | + * @param array $preferences |
|
110 | + * @return void |
|
111 | + */ |
|
112 | + public function save($preferences) |
|
113 | + { |
|
114 | + $configurableParts = ConfigurablePart::getParts(); |
|
115 | + |
|
116 | + $dataType = $this->getModuleLoader()->getDataType(); |
|
117 | + $this->getDataService()->delete( |
|
118 | + $this->tableName, |
|
119 | + [ |
|
120 | + 'data_type' => $dataType |
|
121 | + ] |
|
122 | + ); |
|
123 | + |
|
124 | + $sanitizedPreferences = []; |
|
125 | + foreach ($preferences as $key => $value) { |
|
126 | + if (in_array($key, $configurableParts)) { |
|
127 | + $sanitizedPreferences[$key] = $value; |
|
128 | + } |
|
129 | + } |
|
130 | + |
|
131 | + $this->getDataService()->insert( |
|
132 | + $this->tableName, |
|
133 | + [ |
|
134 | + 'data_type' => $dataType, |
|
135 | + 'preferences' => serialize($sanitizedPreferences), |
|
136 | + ] |
|
137 | + ); |
|
138 | + } |
|
139 | + |
|
140 | + /** |
|
141 | + * @param $dataType |
|
142 | + * @return array |
|
143 | + */ |
|
144 | + public function fetchPreferencesFromDatabase($dataType) |
|
145 | + { |
|
146 | + $preferences = []; |
|
147 | + $record = $this->getDataService()->getRecord( |
|
148 | + $this->tableName, |
|
149 | + [ |
|
150 | + 'data_type' => $dataType |
|
151 | + ] |
|
152 | + ); |
|
153 | + |
|
154 | + if (!empty($record)) { |
|
155 | + $preferences = unserialize($record['preferences']); |
|
156 | + } |
|
157 | + |
|
158 | + return $preferences; |
|
159 | + } |
|
160 | + |
|
161 | + /** |
|
162 | + * Returns the module settings. |
|
163 | + * |
|
164 | + * @return array |
|
165 | + */ |
|
166 | + protected function fetchGlobalPreferencesFromTypoScript() |
|
167 | + { |
|
168 | + $settings = $this->getSettings(); |
|
169 | + |
|
170 | + $configurableParts = ConfigurablePart::getParts(); |
|
171 | + $preferences = []; |
|
172 | + foreach ($settings as $key => $value) { |
|
173 | + if (in_array($key, $configurableParts)) { |
|
174 | + $preferences[$key] = $value; |
|
175 | + } |
|
176 | + } |
|
177 | + |
|
178 | + return $preferences; |
|
179 | + } |
|
180 | + |
|
181 | + /** |
|
182 | + * Returns the module settings. |
|
183 | + * |
|
184 | + * @param string $dataType |
|
185 | + * @return array |
|
186 | + */ |
|
187 | + protected function fetchExtraPreferencesFromTypoScript($dataType) |
|
188 | + { |
|
189 | + $generalSettings = $this->getSettings(); |
|
190 | + |
|
191 | + $preferences = []; |
|
192 | + if (isset($generalSettings[$dataType . '.'])) { |
|
193 | + $settings = $generalSettings[$dataType . '.']; |
|
194 | + |
|
195 | + $configurableParts = ConfigurablePart::getParts(); |
|
196 | + foreach ($settings as $key => $value) { |
|
197 | + if (in_array($key, $configurableParts)) { |
|
198 | + $preferences[$key] = $value; |
|
199 | + } |
|
200 | + } |
|
201 | + } |
|
202 | + |
|
203 | + return $preferences; |
|
204 | + } |
|
205 | + |
|
206 | + /** |
|
207 | + * Returns the module settings. |
|
208 | + * |
|
209 | + * @return array |
|
210 | + */ |
|
211 | + protected function getSettings() |
|
212 | + { |
|
213 | + /** @var BackendConfigurationManager $backendConfigurationManager */ |
|
214 | + $backendConfigurationManager = GeneralUtility::makeInstance(BackendConfigurationManager::class); |
|
215 | + $configuration = $backendConfigurationManager->getTypoScriptSetup(); |
|
216 | + return $configuration['module.']['tx_vidi.']['settings.']; |
|
217 | + } |
|
218 | + |
|
219 | + /** |
|
220 | + * @return object|DataService |
|
221 | + */ |
|
222 | + protected function getDataService(): DataService |
|
223 | + { |
|
224 | + return GeneralUtility::makeInstance(DataService::class); |
|
225 | + } |
|
226 | + |
|
227 | + /** |
|
228 | + * Get the Vidi Module Loader. |
|
229 | + * |
|
230 | + * @return ModuleLoader|object |
|
231 | + */ |
|
232 | + protected function getModuleLoader() |
|
233 | + { |
|
234 | + return GeneralUtility::makeInstance(ModuleLoader::class); |
|
235 | + } |
|
236 | 236 | |
237 | 237 | } |
@@ -22,124 +22,124 @@ |
||
22 | 22 | class ContentService |
23 | 23 | { |
24 | 24 | |
25 | - /** |
|
26 | - * @var string |
|
27 | - */ |
|
28 | - protected $dataType; |
|
29 | - |
|
30 | - /** |
|
31 | - * @var Content[] |
|
32 | - */ |
|
33 | - protected $objects = []; |
|
34 | - |
|
35 | - /** |
|
36 | - * @var int |
|
37 | - */ |
|
38 | - protected $numberOfObjects = 0; |
|
39 | - |
|
40 | - /** |
|
41 | - * Constructor |
|
42 | - * |
|
43 | - * @param string $dataType |
|
44 | - */ |
|
45 | - public function __construct($dataType = '') |
|
46 | - { |
|
47 | - if (empty($dataType)) { |
|
48 | - $dataType = $this->getModuleLoader()->getDataType(); |
|
49 | - } |
|
50 | - $this->dataType = $dataType; |
|
51 | - } |
|
52 | - |
|
53 | - /** |
|
54 | - * Fetch the files given an object assuming |
|
55 | - * |
|
56 | - * @param Matcher $matcher |
|
57 | - * @param Order $order The order |
|
58 | - * @param int $limit |
|
59 | - * @param int $offset |
|
60 | - * @return $this |
|
61 | - */ |
|
62 | - public function findBy(Matcher $matcher, Order $order = null, $limit = null, $offset = null) |
|
63 | - { |
|
64 | - |
|
65 | - // Query the repository. |
|
66 | - $objects = ContentRepositoryFactory::getInstance($this->dataType)->findBy($matcher, $order, $limit, $offset); |
|
67 | - $signalResult = $this->emitAfterFindContentObjectsSignal($objects, $matcher, $order, $limit, $offset); |
|
68 | - |
|
69 | - // Reset objects variable after possible signal / slot processing. |
|
70 | - $this->objects = $signalResult->getContentObjects(); |
|
71 | - |
|
72 | - // Count number of content objects. |
|
73 | - if ($signalResult->getHasBeenProcessed()) { |
|
74 | - $this->numberOfObjects = $signalResult->getNumberOfObjects(); |
|
75 | - } else { |
|
76 | - $this->numberOfObjects = ContentRepositoryFactory::getInstance($this->dataType)->countBy($matcher); |
|
77 | - } |
|
78 | - |
|
79 | - return $this; |
|
80 | - } |
|
81 | - |
|
82 | - /** |
|
83 | - * Signal that is called after the content objects have been found. |
|
84 | - * |
|
85 | - * @param array $contentObjects |
|
86 | - * @param Matcher $matcher |
|
87 | - * @param Order $order |
|
88 | - * @param int $limit |
|
89 | - * @param int $offset |
|
90 | - * @return AfterFindContentObjectsSignalArguments |
|
91 | - */ |
|
92 | - protected function emitAfterFindContentObjectsSignal($contentObjects, Matcher $matcher, Order $order = null, $limit = 0, $offset = 0) |
|
93 | - { |
|
94 | - |
|
95 | - /** @var AfterFindContentObjectsSignalArguments $signalArguments */ |
|
96 | - $signalArguments = GeneralUtility::makeInstance(AfterFindContentObjectsSignalArguments::class); |
|
97 | - $signalArguments->setDataType($this->dataType) |
|
98 | - ->setContentObjects($contentObjects) |
|
99 | - ->setMatcher($matcher) |
|
100 | - ->setOrder($order) |
|
101 | - ->setLimit($limit) |
|
102 | - ->setOffset($offset) |
|
103 | - ->setHasBeenProcessed(false); |
|
104 | - |
|
105 | - $signalResult = $this->getSignalSlotDispatcher()->dispatch(ContentService::class, 'afterFindContentObjects', array($signalArguments)); |
|
106 | - return $signalResult[0]; |
|
107 | - } |
|
108 | - |
|
109 | - /** |
|
110 | - * Get the Vidi Module Loader. |
|
111 | - * |
|
112 | - * @return ModuleLoader|object |
|
113 | - */ |
|
114 | - protected function getModuleLoader() |
|
115 | - { |
|
116 | - return GeneralUtility::makeInstance(ModuleLoader::class); |
|
117 | - } |
|
118 | - |
|
119 | - /** |
|
120 | - * Get the SignalSlot dispatcher. |
|
121 | - * |
|
122 | - * @return Dispatcher|object |
|
123 | - */ |
|
124 | - protected function getSignalSlotDispatcher() |
|
125 | - { |
|
126 | - return GeneralUtility::makeInstance(Dispatcher::class); |
|
127 | - } |
|
128 | - |
|
129 | - /** |
|
130 | - * @return Content[] |
|
131 | - */ |
|
132 | - public function getObjects() |
|
133 | - { |
|
134 | - return $this->objects; |
|
135 | - } |
|
136 | - |
|
137 | - /** |
|
138 | - * @return int |
|
139 | - */ |
|
140 | - public function getNumberOfObjects() |
|
141 | - { |
|
142 | - return $this->numberOfObjects; |
|
143 | - } |
|
25 | + /** |
|
26 | + * @var string |
|
27 | + */ |
|
28 | + protected $dataType; |
|
29 | + |
|
30 | + /** |
|
31 | + * @var Content[] |
|
32 | + */ |
|
33 | + protected $objects = []; |
|
34 | + |
|
35 | + /** |
|
36 | + * @var int |
|
37 | + */ |
|
38 | + protected $numberOfObjects = 0; |
|
39 | + |
|
40 | + /** |
|
41 | + * Constructor |
|
42 | + * |
|
43 | + * @param string $dataType |
|
44 | + */ |
|
45 | + public function __construct($dataType = '') |
|
46 | + { |
|
47 | + if (empty($dataType)) { |
|
48 | + $dataType = $this->getModuleLoader()->getDataType(); |
|
49 | + } |
|
50 | + $this->dataType = $dataType; |
|
51 | + } |
|
52 | + |
|
53 | + /** |
|
54 | + * Fetch the files given an object assuming |
|
55 | + * |
|
56 | + * @param Matcher $matcher |
|
57 | + * @param Order $order The order |
|
58 | + * @param int $limit |
|
59 | + * @param int $offset |
|
60 | + * @return $this |
|
61 | + */ |
|
62 | + public function findBy(Matcher $matcher, Order $order = null, $limit = null, $offset = null) |
|
63 | + { |
|
64 | + |
|
65 | + // Query the repository. |
|
66 | + $objects = ContentRepositoryFactory::getInstance($this->dataType)->findBy($matcher, $order, $limit, $offset); |
|
67 | + $signalResult = $this->emitAfterFindContentObjectsSignal($objects, $matcher, $order, $limit, $offset); |
|
68 | + |
|
69 | + // Reset objects variable after possible signal / slot processing. |
|
70 | + $this->objects = $signalResult->getContentObjects(); |
|
71 | + |
|
72 | + // Count number of content objects. |
|
73 | + if ($signalResult->getHasBeenProcessed()) { |
|
74 | + $this->numberOfObjects = $signalResult->getNumberOfObjects(); |
|
75 | + } else { |
|
76 | + $this->numberOfObjects = ContentRepositoryFactory::getInstance($this->dataType)->countBy($matcher); |
|
77 | + } |
|
78 | + |
|
79 | + return $this; |
|
80 | + } |
|
81 | + |
|
82 | + /** |
|
83 | + * Signal that is called after the content objects have been found. |
|
84 | + * |
|
85 | + * @param array $contentObjects |
|
86 | + * @param Matcher $matcher |
|
87 | + * @param Order $order |
|
88 | + * @param int $limit |
|
89 | + * @param int $offset |
|
90 | + * @return AfterFindContentObjectsSignalArguments |
|
91 | + */ |
|
92 | + protected function emitAfterFindContentObjectsSignal($contentObjects, Matcher $matcher, Order $order = null, $limit = 0, $offset = 0) |
|
93 | + { |
|
94 | + |
|
95 | + /** @var AfterFindContentObjectsSignalArguments $signalArguments */ |
|
96 | + $signalArguments = GeneralUtility::makeInstance(AfterFindContentObjectsSignalArguments::class); |
|
97 | + $signalArguments->setDataType($this->dataType) |
|
98 | + ->setContentObjects($contentObjects) |
|
99 | + ->setMatcher($matcher) |
|
100 | + ->setOrder($order) |
|
101 | + ->setLimit($limit) |
|
102 | + ->setOffset($offset) |
|
103 | + ->setHasBeenProcessed(false); |
|
104 | + |
|
105 | + $signalResult = $this->getSignalSlotDispatcher()->dispatch(ContentService::class, 'afterFindContentObjects', array($signalArguments)); |
|
106 | + return $signalResult[0]; |
|
107 | + } |
|
108 | + |
|
109 | + /** |
|
110 | + * Get the Vidi Module Loader. |
|
111 | + * |
|
112 | + * @return ModuleLoader|object |
|
113 | + */ |
|
114 | + protected function getModuleLoader() |
|
115 | + { |
|
116 | + return GeneralUtility::makeInstance(ModuleLoader::class); |
|
117 | + } |
|
118 | + |
|
119 | + /** |
|
120 | + * Get the SignalSlot dispatcher. |
|
121 | + * |
|
122 | + * @return Dispatcher|object |
|
123 | + */ |
|
124 | + protected function getSignalSlotDispatcher() |
|
125 | + { |
|
126 | + return GeneralUtility::makeInstance(Dispatcher::class); |
|
127 | + } |
|
128 | + |
|
129 | + /** |
|
130 | + * @return Content[] |
|
131 | + */ |
|
132 | + public function getObjects() |
|
133 | + { |
|
134 | + return $this->objects; |
|
135 | + } |
|
136 | + |
|
137 | + /** |
|
138 | + * @return int |
|
139 | + */ |
|
140 | + public function getNumberOfObjects() |
|
141 | + { |
|
142 | + return $this->numberOfObjects; |
|
143 | + } |
|
144 | 144 | |
145 | 145 | } |
@@ -25,267 +25,267 @@ |
||
25 | 25 | class MatcherObjectFactory implements SingletonInterface |
26 | 26 | { |
27 | 27 | |
28 | - /** |
|
29 | - * Gets a singleton instance of this class. |
|
30 | - * |
|
31 | - * @return $this |
|
32 | - */ |
|
33 | - static public function getInstance(): self |
|
34 | - { |
|
35 | - return GeneralUtility::makeInstance(self::class); |
|
36 | - } |
|
37 | - |
|
38 | - /** |
|
39 | - * Returns a matcher object. |
|
40 | - * |
|
41 | - * @param array $matches |
|
42 | - * @param string $dataType |
|
43 | - * @return Matcher |
|
44 | - */ |
|
45 | - public function getMatcher(array $matches = [], $dataType = ''): Matcher |
|
46 | - { |
|
47 | - if ($dataType === '') { |
|
48 | - $dataType = $this->getModuleLoader()->getDataType(); |
|
49 | - } |
|
50 | - |
|
51 | - /** @var $matcher Matcher */ |
|
52 | - $matcher = GeneralUtility::makeInstance(Matcher::class, [], $dataType); |
|
53 | - |
|
54 | - $matcher = $this->applyCriteriaFromDataTables($matcher); |
|
55 | - $matcher = $this->applyCriteriaFromMatchesArgument($matcher, $matches); |
|
56 | - |
|
57 | - if ($this->isBackendMode()) { |
|
58 | - $matcher = $this->applyCriteriaFromUrl($matcher); |
|
59 | - $matcher = $this->applyCriteriaFromTSConfig($matcher); |
|
60 | - } |
|
61 | - |
|
62 | - // Trigger signal for post processing Matcher Object. |
|
63 | - $this->emitPostProcessMatcherObjectSignal($matcher); |
|
64 | - |
|
65 | - return $matcher; |
|
66 | - } |
|
67 | - |
|
68 | - /** |
|
69 | - * Get a possible id from the URL and apply as filter criteria. |
|
70 | - * Except if the main module belongs to the File. The id would be a combined identifier |
|
71 | - * including the storage and a mount point. |
|
72 | - * |
|
73 | - * @param Matcher $matcher |
|
74 | - * @return Matcher $matcher |
|
75 | - */ |
|
76 | - protected function applyCriteriaFromUrl(Matcher $matcher): Matcher |
|
77 | - { |
|
78 | - if (GeneralUtility::_GP('id') |
|
79 | - && !$this->getModuleLoader()->isPidIgnored() |
|
80 | - && $this->getModuleLoader()->getMainModule() !== ModuleName::FILE) { |
|
81 | - $matcher->equals('pid', GeneralUtility::_GP('id')); |
|
82 | - } |
|
83 | - |
|
84 | - return $matcher; |
|
85 | - } |
|
86 | - |
|
87 | - /** |
|
88 | - * @param Matcher $matcher |
|
89 | - * @return Matcher $matcher |
|
90 | - */ |
|
91 | - protected function applyCriteriaFromTSConfig(Matcher $matcher): Matcher |
|
92 | - { |
|
93 | - $dataType = $matcher->getDataType(); |
|
94 | - $tsConfigPath = sprintf('tx_vidi.dataType.%s.constraints', $dataType); |
|
95 | - $tsConfig = $this->getBackendUser()->getTSConfig($tsConfigPath); |
|
96 | - |
|
97 | - if (is_array($tsConfig['properties']) && !empty($tsConfig['properties'])) { |
|
98 | - |
|
99 | - foreach ($tsConfig['properties'] as $constraint) { |
|
100 | - |
|
101 | - if (preg_match('/(.+) (>=|>|<|<=|=|like) (.+)/is', $constraint, $matches) && count($matches) === 4) { |
|
102 | - |
|
103 | - $operator = $matcher->getSupportedOperators()[strtolower(trim($matches[2]))]; |
|
104 | - $operand = trim($matches[1]); |
|
105 | - $value = trim($matches[3]); |
|
106 | - |
|
107 | - $matcher->$operator($operand, $value); |
|
108 | - } elseif (preg_match('/(.+) (in) (.+)/is', $constraint, $matches) && count($matches) === 4) { |
|
109 | - |
|
110 | - $operator = $matcher->getSupportedOperators()[trim($matches[2])]; |
|
111 | - $operand = trim($matches[1]); |
|
112 | - $value = trim($matches[3]); |
|
113 | - $matcher->$operator($operand, GeneralUtility::trimExplode(',', $value, true)); |
|
114 | - } |
|
115 | - } |
|
116 | - } |
|
117 | - |
|
118 | - return $matcher; |
|
119 | - } |
|
120 | - |
|
121 | - /** |
|
122 | - * @param Matcher $matcher |
|
123 | - * @param array $matches |
|
124 | - * @return Matcher $matcher |
|
125 | - */ |
|
126 | - protected function applyCriteriaFromMatchesArgument(Matcher $matcher, $matches): Matcher |
|
127 | - { |
|
128 | - foreach ($matches as $fieldNameAndPath => $value) { |
|
129 | - // CSV values should be considered as "in" operator in the query, otherwise "equals". |
|
130 | - $explodedValues = GeneralUtility::trimExplode(',', $value, true); |
|
131 | - if (count($explodedValues) > 1) { |
|
132 | - $matcher->in($fieldNameAndPath, $explodedValues); |
|
133 | - } else { |
|
134 | - $matcher->equals($fieldNameAndPath, $explodedValues[0]); |
|
135 | - } |
|
136 | - } |
|
137 | - |
|
138 | - return $matcher; |
|
139 | - } |
|
140 | - |
|
141 | - /** |
|
142 | - * Apply criteria specific to jQuery plugin DataTable. |
|
143 | - * |
|
144 | - * @param Matcher $matcher |
|
145 | - * @return Matcher $matcher |
|
146 | - */ |
|
147 | - protected function applyCriteriaFromDataTables(Matcher $matcher): Matcher |
|
148 | - { |
|
149 | - |
|
150 | - // Special case for Grid in the BE using jQuery DataTables plugin. |
|
151 | - // Retrieve a possible search term from GP. |
|
152 | - $query = GeneralUtility::_GP('search'); |
|
153 | - if (is_array($query)) { |
|
154 | - if (!empty($query['value'])) { |
|
155 | - $query = $query['value']; |
|
156 | - } else { |
|
157 | - $query = ''; |
|
158 | - } |
|
159 | - } |
|
160 | - |
|
161 | - if (strlen($query) > 0) { |
|
162 | - |
|
163 | - // Parse the json query coming from the Visual Search. |
|
164 | - $query = rawurldecode($query); |
|
165 | - $queryParts = json_decode($query, true); |
|
166 | - |
|
167 | - if (is_array($queryParts)) { |
|
168 | - $matcher = $this->parseQuery($queryParts, $matcher); |
|
169 | - } else { |
|
170 | - $matcher->setSearchTerm($query); |
|
171 | - } |
|
172 | - } |
|
173 | - return $matcher; |
|
174 | - } |
|
175 | - |
|
176 | - /** |
|
177 | - * @param array $queryParts |
|
178 | - * @param Matcher $matcher |
|
179 | - * @return Matcher $matcher |
|
180 | - */ |
|
181 | - protected function parseQuery(array $queryParts, Matcher $matcher): Matcher |
|
182 | - { |
|
183 | - $dataType = $matcher->getDataType(); |
|
184 | - foreach ($queryParts as $term) { |
|
185 | - $fieldNameAndPath = key($term); |
|
186 | - |
|
187 | - $resolvedDataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $dataType); |
|
188 | - $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $dataType); |
|
189 | - |
|
190 | - // Retrieve the value. |
|
191 | - $value = current($term); |
|
192 | - |
|
193 | - if (Tca::grid($resolvedDataType)->hasFacet($fieldName) && Tca::grid($resolvedDataType)->facet($fieldName)->canModifyMatcher()) { |
|
194 | - $matcher = Tca::grid($resolvedDataType)->facet($fieldName)->modifyMatcher($matcher, $value); |
|
195 | - } elseif (Tca::table($resolvedDataType)->hasField($fieldName)) { |
|
196 | - // Check whether the field exists and set it as "equal" or "like". |
|
197 | - if ($this->isOperatorEquals($fieldNameAndPath, $dataType, $value)) { |
|
198 | - $matcher->equals($fieldNameAndPath, $value); |
|
199 | - } else { |
|
200 | - $matcher->like($fieldNameAndPath, $value); |
|
201 | - } |
|
202 | - } elseif ($fieldNameAndPath === 'text') { |
|
203 | - // Special case if field is "text" which is a pseudo field in this case. |
|
204 | - // Set the search term which means Vidi will |
|
205 | - // search in various fields with operator "like". The fields come from key "searchFields" in the TCA. |
|
206 | - $matcher->setSearchTerm($value); |
|
207 | - } |
|
208 | - } |
|
209 | - return $matcher; |
|
210 | - } |
|
211 | - |
|
212 | - /** |
|
213 | - * Tell whether the operator should be equals instead of like for a search, e.g. if the value is numerical. |
|
214 | - * |
|
215 | - * @param string $fieldName |
|
216 | - * @param string $dataType |
|
217 | - * @param string $value |
|
218 | - * @return bool |
|
219 | - */ |
|
220 | - protected function isOperatorEquals($fieldName, $dataType, $value): bool |
|
221 | - { |
|
222 | - return (Tca::table($dataType)->field($fieldName)->hasRelation() && MathUtility::canBeInterpretedAsInteger($value)) |
|
223 | - || Tca::table($dataType)->field($fieldName)->isNumerical(); |
|
224 | - } |
|
225 | - |
|
226 | - /** |
|
227 | - * Signal that is called for post-processing a matcher object. |
|
228 | - * |
|
229 | - * @param Matcher $matcher |
|
230 | - */ |
|
231 | - protected function emitPostProcessMatcherObjectSignal(Matcher $matcher): void |
|
232 | - { |
|
233 | - |
|
234 | - if (strlen($matcher->getDataType()) <= 0) { |
|
235 | - |
|
236 | - /** @var ModuleLoader $moduleLoader */ |
|
237 | - $moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class); |
|
238 | - $matcher->setDataType($moduleLoader->getDataType()); |
|
239 | - } |
|
240 | - |
|
241 | - $this->getSignalSlotDispatcher()->dispatch('Fab\Vidi\Controller\Backend\ContentController', 'postProcessMatcherObject', array($matcher, $matcher->getDataType())); |
|
242 | - } |
|
243 | - |
|
244 | - /** |
|
245 | - * Get the SignalSlot dispatcher |
|
246 | - * |
|
247 | - * @return Dispatcher|object |
|
248 | - */ |
|
249 | - protected function getSignalSlotDispatcher() |
|
250 | - { |
|
251 | - return GeneralUtility::makeInstance(Dispatcher::class); |
|
252 | - } |
|
253 | - |
|
254 | - /** |
|
255 | - * Get the Vidi Module Loader. |
|
256 | - * |
|
257 | - * @return ModuleLoader|object |
|
258 | - */ |
|
259 | - protected function getModuleLoader() |
|
260 | - { |
|
261 | - return GeneralUtility::makeInstance(ModuleLoader::class); |
|
262 | - } |
|
263 | - |
|
264 | - /** |
|
265 | - * @return FieldPathResolver|object |
|
266 | - */ |
|
267 | - protected function getFieldPathResolver() |
|
268 | - { |
|
269 | - return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
270 | - } |
|
271 | - |
|
272 | - /** |
|
273 | - * Returns an instance of the current Backend User. |
|
274 | - * |
|
275 | - * @return BackendUserAuthentication |
|
276 | - */ |
|
277 | - protected function getBackendUser(): BackendUserAuthentication |
|
278 | - { |
|
279 | - return $GLOBALS['BE_USER']; |
|
280 | - } |
|
281 | - |
|
282 | - /** |
|
283 | - * Returns whether the current mode is Backend |
|
284 | - * |
|
285 | - * @return bool |
|
286 | - */ |
|
287 | - protected function isBackendMode(): bool |
|
288 | - { |
|
289 | - return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); |
|
290 | - } |
|
28 | + /** |
|
29 | + * Gets a singleton instance of this class. |
|
30 | + * |
|
31 | + * @return $this |
|
32 | + */ |
|
33 | + static public function getInstance(): self |
|
34 | + { |
|
35 | + return GeneralUtility::makeInstance(self::class); |
|
36 | + } |
|
37 | + |
|
38 | + /** |
|
39 | + * Returns a matcher object. |
|
40 | + * |
|
41 | + * @param array $matches |
|
42 | + * @param string $dataType |
|
43 | + * @return Matcher |
|
44 | + */ |
|
45 | + public function getMatcher(array $matches = [], $dataType = ''): Matcher |
|
46 | + { |
|
47 | + if ($dataType === '') { |
|
48 | + $dataType = $this->getModuleLoader()->getDataType(); |
|
49 | + } |
|
50 | + |
|
51 | + /** @var $matcher Matcher */ |
|
52 | + $matcher = GeneralUtility::makeInstance(Matcher::class, [], $dataType); |
|
53 | + |
|
54 | + $matcher = $this->applyCriteriaFromDataTables($matcher); |
|
55 | + $matcher = $this->applyCriteriaFromMatchesArgument($matcher, $matches); |
|
56 | + |
|
57 | + if ($this->isBackendMode()) { |
|
58 | + $matcher = $this->applyCriteriaFromUrl($matcher); |
|
59 | + $matcher = $this->applyCriteriaFromTSConfig($matcher); |
|
60 | + } |
|
61 | + |
|
62 | + // Trigger signal for post processing Matcher Object. |
|
63 | + $this->emitPostProcessMatcherObjectSignal($matcher); |
|
64 | + |
|
65 | + return $matcher; |
|
66 | + } |
|
67 | + |
|
68 | + /** |
|
69 | + * Get a possible id from the URL and apply as filter criteria. |
|
70 | + * Except if the main module belongs to the File. The id would be a combined identifier |
|
71 | + * including the storage and a mount point. |
|
72 | + * |
|
73 | + * @param Matcher $matcher |
|
74 | + * @return Matcher $matcher |
|
75 | + */ |
|
76 | + protected function applyCriteriaFromUrl(Matcher $matcher): Matcher |
|
77 | + { |
|
78 | + if (GeneralUtility::_GP('id') |
|
79 | + && !$this->getModuleLoader()->isPidIgnored() |
|
80 | + && $this->getModuleLoader()->getMainModule() !== ModuleName::FILE) { |
|
81 | + $matcher->equals('pid', GeneralUtility::_GP('id')); |
|
82 | + } |
|
83 | + |
|
84 | + return $matcher; |
|
85 | + } |
|
86 | + |
|
87 | + /** |
|
88 | + * @param Matcher $matcher |
|
89 | + * @return Matcher $matcher |
|
90 | + */ |
|
91 | + protected function applyCriteriaFromTSConfig(Matcher $matcher): Matcher |
|
92 | + { |
|
93 | + $dataType = $matcher->getDataType(); |
|
94 | + $tsConfigPath = sprintf('tx_vidi.dataType.%s.constraints', $dataType); |
|
95 | + $tsConfig = $this->getBackendUser()->getTSConfig($tsConfigPath); |
|
96 | + |
|
97 | + if (is_array($tsConfig['properties']) && !empty($tsConfig['properties'])) { |
|
98 | + |
|
99 | + foreach ($tsConfig['properties'] as $constraint) { |
|
100 | + |
|
101 | + if (preg_match('/(.+) (>=|>|<|<=|=|like) (.+)/is', $constraint, $matches) && count($matches) === 4) { |
|
102 | + |
|
103 | + $operator = $matcher->getSupportedOperators()[strtolower(trim($matches[2]))]; |
|
104 | + $operand = trim($matches[1]); |
|
105 | + $value = trim($matches[3]); |
|
106 | + |
|
107 | + $matcher->$operator($operand, $value); |
|
108 | + } elseif (preg_match('/(.+) (in) (.+)/is', $constraint, $matches) && count($matches) === 4) { |
|
109 | + |
|
110 | + $operator = $matcher->getSupportedOperators()[trim($matches[2])]; |
|
111 | + $operand = trim($matches[1]); |
|
112 | + $value = trim($matches[3]); |
|
113 | + $matcher->$operator($operand, GeneralUtility::trimExplode(',', $value, true)); |
|
114 | + } |
|
115 | + } |
|
116 | + } |
|
117 | + |
|
118 | + return $matcher; |
|
119 | + } |
|
120 | + |
|
121 | + /** |
|
122 | + * @param Matcher $matcher |
|
123 | + * @param array $matches |
|
124 | + * @return Matcher $matcher |
|
125 | + */ |
|
126 | + protected function applyCriteriaFromMatchesArgument(Matcher $matcher, $matches): Matcher |
|
127 | + { |
|
128 | + foreach ($matches as $fieldNameAndPath => $value) { |
|
129 | + // CSV values should be considered as "in" operator in the query, otherwise "equals". |
|
130 | + $explodedValues = GeneralUtility::trimExplode(',', $value, true); |
|
131 | + if (count($explodedValues) > 1) { |
|
132 | + $matcher->in($fieldNameAndPath, $explodedValues); |
|
133 | + } else { |
|
134 | + $matcher->equals($fieldNameAndPath, $explodedValues[0]); |
|
135 | + } |
|
136 | + } |
|
137 | + |
|
138 | + return $matcher; |
|
139 | + } |
|
140 | + |
|
141 | + /** |
|
142 | + * Apply criteria specific to jQuery plugin DataTable. |
|
143 | + * |
|
144 | + * @param Matcher $matcher |
|
145 | + * @return Matcher $matcher |
|
146 | + */ |
|
147 | + protected function applyCriteriaFromDataTables(Matcher $matcher): Matcher |
|
148 | + { |
|
149 | + |
|
150 | + // Special case for Grid in the BE using jQuery DataTables plugin. |
|
151 | + // Retrieve a possible search term from GP. |
|
152 | + $query = GeneralUtility::_GP('search'); |
|
153 | + if (is_array($query)) { |
|
154 | + if (!empty($query['value'])) { |
|
155 | + $query = $query['value']; |
|
156 | + } else { |
|
157 | + $query = ''; |
|
158 | + } |
|
159 | + } |
|
160 | + |
|
161 | + if (strlen($query) > 0) { |
|
162 | + |
|
163 | + // Parse the json query coming from the Visual Search. |
|
164 | + $query = rawurldecode($query); |
|
165 | + $queryParts = json_decode($query, true); |
|
166 | + |
|
167 | + if (is_array($queryParts)) { |
|
168 | + $matcher = $this->parseQuery($queryParts, $matcher); |
|
169 | + } else { |
|
170 | + $matcher->setSearchTerm($query); |
|
171 | + } |
|
172 | + } |
|
173 | + return $matcher; |
|
174 | + } |
|
175 | + |
|
176 | + /** |
|
177 | + * @param array $queryParts |
|
178 | + * @param Matcher $matcher |
|
179 | + * @return Matcher $matcher |
|
180 | + */ |
|
181 | + protected function parseQuery(array $queryParts, Matcher $matcher): Matcher |
|
182 | + { |
|
183 | + $dataType = $matcher->getDataType(); |
|
184 | + foreach ($queryParts as $term) { |
|
185 | + $fieldNameAndPath = key($term); |
|
186 | + |
|
187 | + $resolvedDataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $dataType); |
|
188 | + $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $dataType); |
|
189 | + |
|
190 | + // Retrieve the value. |
|
191 | + $value = current($term); |
|
192 | + |
|
193 | + if (Tca::grid($resolvedDataType)->hasFacet($fieldName) && Tca::grid($resolvedDataType)->facet($fieldName)->canModifyMatcher()) { |
|
194 | + $matcher = Tca::grid($resolvedDataType)->facet($fieldName)->modifyMatcher($matcher, $value); |
|
195 | + } elseif (Tca::table($resolvedDataType)->hasField($fieldName)) { |
|
196 | + // Check whether the field exists and set it as "equal" or "like". |
|
197 | + if ($this->isOperatorEquals($fieldNameAndPath, $dataType, $value)) { |
|
198 | + $matcher->equals($fieldNameAndPath, $value); |
|
199 | + } else { |
|
200 | + $matcher->like($fieldNameAndPath, $value); |
|
201 | + } |
|
202 | + } elseif ($fieldNameAndPath === 'text') { |
|
203 | + // Special case if field is "text" which is a pseudo field in this case. |
|
204 | + // Set the search term which means Vidi will |
|
205 | + // search in various fields with operator "like". The fields come from key "searchFields" in the TCA. |
|
206 | + $matcher->setSearchTerm($value); |
|
207 | + } |
|
208 | + } |
|
209 | + return $matcher; |
|
210 | + } |
|
211 | + |
|
212 | + /** |
|
213 | + * Tell whether the operator should be equals instead of like for a search, e.g. if the value is numerical. |
|
214 | + * |
|
215 | + * @param string $fieldName |
|
216 | + * @param string $dataType |
|
217 | + * @param string $value |
|
218 | + * @return bool |
|
219 | + */ |
|
220 | + protected function isOperatorEquals($fieldName, $dataType, $value): bool |
|
221 | + { |
|
222 | + return (Tca::table($dataType)->field($fieldName)->hasRelation() && MathUtility::canBeInterpretedAsInteger($value)) |
|
223 | + || Tca::table($dataType)->field($fieldName)->isNumerical(); |
|
224 | + } |
|
225 | + |
|
226 | + /** |
|
227 | + * Signal that is called for post-processing a matcher object. |
|
228 | + * |
|
229 | + * @param Matcher $matcher |
|
230 | + */ |
|
231 | + protected function emitPostProcessMatcherObjectSignal(Matcher $matcher): void |
|
232 | + { |
|
233 | + |
|
234 | + if (strlen($matcher->getDataType()) <= 0) { |
|
235 | + |
|
236 | + /** @var ModuleLoader $moduleLoader */ |
|
237 | + $moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class); |
|
238 | + $matcher->setDataType($moduleLoader->getDataType()); |
|
239 | + } |
|
240 | + |
|
241 | + $this->getSignalSlotDispatcher()->dispatch('Fab\Vidi\Controller\Backend\ContentController', 'postProcessMatcherObject', array($matcher, $matcher->getDataType())); |
|
242 | + } |
|
243 | + |
|
244 | + /** |
|
245 | + * Get the SignalSlot dispatcher |
|
246 | + * |
|
247 | + * @return Dispatcher|object |
|
248 | + */ |
|
249 | + protected function getSignalSlotDispatcher() |
|
250 | + { |
|
251 | + return GeneralUtility::makeInstance(Dispatcher::class); |
|
252 | + } |
|
253 | + |
|
254 | + /** |
|
255 | + * Get the Vidi Module Loader. |
|
256 | + * |
|
257 | + * @return ModuleLoader|object |
|
258 | + */ |
|
259 | + protected function getModuleLoader() |
|
260 | + { |
|
261 | + return GeneralUtility::makeInstance(ModuleLoader::class); |
|
262 | + } |
|
263 | + |
|
264 | + /** |
|
265 | + * @return FieldPathResolver|object |
|
266 | + */ |
|
267 | + protected function getFieldPathResolver() |
|
268 | + { |
|
269 | + return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
270 | + } |
|
271 | + |
|
272 | + /** |
|
273 | + * Returns an instance of the current Backend User. |
|
274 | + * |
|
275 | + * @return BackendUserAuthentication |
|
276 | + */ |
|
277 | + protected function getBackendUser(): BackendUserAuthentication |
|
278 | + { |
|
279 | + return $GLOBALS['BE_USER']; |
|
280 | + } |
|
281 | + |
|
282 | + /** |
|
283 | + * Returns whether the current mode is Backend |
|
284 | + * |
|
285 | + * @return bool |
|
286 | + */ |
|
287 | + protected function isBackendMode(): bool |
|
288 | + { |
|
289 | + return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); |
|
290 | + } |
|
291 | 291 | } |
@@ -41,626 +41,626 @@ |
||
41 | 41 | class Query implements QueryInterface |
42 | 42 | { |
43 | 43 | |
44 | - /** |
|
45 | - * An inner join. |
|
46 | - */ |
|
47 | - const JCR_JOIN_TYPE_INNER = '{http://www.jcp.org/jcr/1.0}joinTypeInner'; |
|
48 | - |
|
49 | - /** |
|
50 | - * A left-outer join. |
|
51 | - */ |
|
52 | - const JCR_JOIN_TYPE_LEFT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeLeftOuter'; |
|
53 | - |
|
54 | - /** |
|
55 | - * A right-outer join. |
|
56 | - */ |
|
57 | - const JCR_JOIN_TYPE_RIGHT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeRightOuter'; |
|
58 | - |
|
59 | - /** |
|
60 | - * Charset of strings in QOM |
|
61 | - */ |
|
62 | - const CHARSET = 'utf-8'; |
|
63 | - |
|
64 | - /** |
|
65 | - * @var string |
|
66 | - */ |
|
67 | - protected $sourceFieldName; |
|
68 | - |
|
69 | - /** |
|
70 | - * @var string |
|
71 | - */ |
|
72 | - protected $type; |
|
73 | - |
|
74 | - /** |
|
75 | - * @var PersistenceManagerInterface |
|
76 | - */ |
|
77 | - protected $persistenceManager; |
|
78 | - |
|
79 | - /** |
|
80 | - * @var QueryObjectModelFactory |
|
81 | - */ |
|
82 | - protected $qomFactory; |
|
83 | - |
|
84 | - /** |
|
85 | - * @var SourceInterface |
|
86 | - */ |
|
87 | - protected $source; |
|
88 | - |
|
89 | - /** |
|
90 | - * @var ConstraintInterface |
|
91 | - */ |
|
92 | - protected $constraint; |
|
93 | - |
|
94 | - /** |
|
95 | - * @var Statement |
|
96 | - */ |
|
97 | - protected $statement; |
|
98 | - |
|
99 | - /** |
|
100 | - * @var array |
|
101 | - */ |
|
102 | - protected $orderings = []; |
|
103 | - |
|
104 | - /** |
|
105 | - * @var int |
|
106 | - */ |
|
107 | - protected $limit; |
|
108 | - |
|
109 | - /** |
|
110 | - * @var int |
|
111 | - */ |
|
112 | - protected $offset; |
|
113 | - |
|
114 | - /** |
|
115 | - * Apply DISTINCT upon property. |
|
116 | - * |
|
117 | - * @var string |
|
118 | - */ |
|
119 | - protected $distinct; |
|
120 | - |
|
121 | - /** |
|
122 | - * The query settings. |
|
123 | - * |
|
124 | - * @var Typo3QuerySettings |
|
125 | - */ |
|
126 | - public Typo3QuerySettings $typo3QuerySettings; |
|
127 | - |
|
128 | - /** |
|
129 | - * Constructs a query object working on the given class name |
|
130 | - * |
|
131 | - * @param string $type |
|
132 | - */ |
|
133 | - public function __construct($type) |
|
134 | - { |
|
135 | - $this->type = $type; |
|
136 | - $this->persistenceManager = GeneralUtility::makeInstance(PersistenceManagerInterface::class); |
|
137 | - $this->qomFactory = GeneralUtility::makeInstance(QueryObjectModelFactory::class); |
|
138 | - } |
|
139 | - |
|
140 | - public function injectTypo3QuerySettings(Typo3QuerySettings $querySettings): void |
|
141 | - { |
|
142 | - $this->typo3QuerySettings = $querySettings; |
|
143 | - } |
|
144 | - |
|
145 | - /** |
|
146 | - * Sets the Query Settings. These Query settings must match the settings expected by |
|
147 | - * the specific Storage Backend. |
|
148 | - * |
|
149 | - * @param QuerySettingsInterface $typo3QuerySettings The Query Settings |
|
150 | - * @return void |
|
151 | - */ |
|
152 | - public function setTypo3QuerySettings(QuerySettingsInterface $typo3QuerySettings) |
|
153 | - { |
|
154 | - $this->typo3QuerySettings = $typo3QuerySettings; |
|
155 | - } |
|
156 | - |
|
157 | - /** |
|
158 | - * Returns the Query Settings. |
|
159 | - * |
|
160 | - * @throws \Exception |
|
161 | - * @return Typo3QuerySettings $querySettings The Query Settings |
|
162 | - * @api This method is not part of FLOW3 API |
|
163 | - */ |
|
164 | - public function getTypo3QuerySettings() |
|
165 | - { |
|
166 | - if (!$this->typo3QuerySettings instanceof QuerySettingsInterface) { |
|
167 | - throw new Exception('Tried to get the query settings without setting them before.', 1248689115); |
|
168 | - } |
|
169 | - |
|
170 | - // Apply possible settings to the query. |
|
171 | - if ($this->isBackendMode()) { |
|
172 | - /** @var BackendConfigurationManager $backendConfigurationManager */ |
|
173 | - $backendConfigurationManager = GeneralUtility::makeInstance(BackendConfigurationManager::class); |
|
174 | - $configuration = $backendConfigurationManager->getTypoScriptSetup(); |
|
175 | - $querySettings = array('respectSysLanguage'); |
|
176 | - foreach ($querySettings as $setting) { |
|
177 | - if (isset($configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type . '.'][$setting])) { |
|
178 | - $value = (bool)$configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type . '.'][$setting]; |
|
179 | - ObjectAccess::setProperty($this->typo3QuerySettings, $setting, $value); |
|
180 | - } |
|
181 | - } |
|
182 | - } |
|
183 | - |
|
184 | - return $this->typo3QuerySettings; |
|
185 | - } |
|
186 | - |
|
187 | - /** |
|
188 | - * Returns the type this query cares for. |
|
189 | - * |
|
190 | - * @return string |
|
191 | - * @api |
|
192 | - */ |
|
193 | - public function getType() |
|
194 | - { |
|
195 | - return $this->type; |
|
196 | - } |
|
197 | - |
|
198 | - /** |
|
199 | - * Sets the source to fetch the result from |
|
200 | - * |
|
201 | - * @param SourceInterface $source |
|
202 | - */ |
|
203 | - public function setSource(SourceInterface $source) |
|
204 | - { |
|
205 | - $this->source = $source; |
|
206 | - } |
|
207 | - |
|
208 | - /** |
|
209 | - * Returns the selectorn name or an empty string, if the source is not a selector |
|
210 | - * TODO This has to be checked at another place |
|
211 | - * |
|
212 | - * @return string The selector name |
|
213 | - */ |
|
214 | - protected function getSelectorName() |
|
215 | - { |
|
216 | - if ($this->getSource() instanceof SelectorInterface) { |
|
217 | - return $this->source->getSelectorName(); |
|
218 | - } else { |
|
219 | - return ''; |
|
220 | - } |
|
221 | - } |
|
222 | - |
|
223 | - /** |
|
224 | - * Gets the node-tuple source for this query. |
|
225 | - * |
|
226 | - * @return SourceInterface the node-tuple source; non-null |
|
227 | - */ |
|
228 | - public function getSource() |
|
229 | - { |
|
230 | - if ($this->source === null) { |
|
231 | - $this->source = $this->qomFactory->selector($this->getType()); |
|
232 | - } |
|
233 | - return $this->source; |
|
234 | - } |
|
235 | - |
|
236 | - /** |
|
237 | - * Executes the query against the database and returns the result |
|
238 | - * |
|
239 | - * @return QueryResultInterface|array The query result object or an array if $this->getQuerySettings()->getReturnRawQueryResult() is true |
|
240 | - * @api |
|
241 | - */ |
|
242 | - public function execute($returnRawQueryResult = false) |
|
243 | - { |
|
244 | - /** @var VidiDbBackend $backend */ |
|
245 | - $backend = GeneralUtility::makeInstance(VidiDbBackend::class, $this); |
|
246 | - return $backend->fetchResult(); |
|
247 | - } |
|
248 | - |
|
249 | - /** |
|
250 | - * Sets the property names to order the result by. Expected like this: |
|
251 | - * array( |
|
252 | - * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING, |
|
253 | - * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING |
|
254 | - * ) |
|
255 | - * where 'foo' and 'bar' are property names. |
|
256 | - * |
|
257 | - * @param array $orderings The property names to order by |
|
258 | - * @return QueryInterface |
|
259 | - * @api |
|
260 | - */ |
|
261 | - public function setOrderings(array $orderings) |
|
262 | - { |
|
263 | - $this->orderings = $orderings; |
|
264 | - return $this; |
|
265 | - } |
|
266 | - |
|
267 | - /** |
|
268 | - * Returns the property names to order the result by. Like this: |
|
269 | - * array( |
|
270 | - * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING, |
|
271 | - * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING |
|
272 | - * ) |
|
273 | - * |
|
274 | - * @return array |
|
275 | - */ |
|
276 | - public function getOrderings() |
|
277 | - { |
|
278 | - return $this->orderings; |
|
279 | - } |
|
280 | - |
|
281 | - /** |
|
282 | - * Sets the maximum size of the result set to limit. Returns $this to allow |
|
283 | - * for chaining (fluid interface) |
|
284 | - * |
|
285 | - * @param integer $limit |
|
286 | - * @throws \InvalidArgumentException |
|
287 | - * @return QueryInterface |
|
288 | - * @api |
|
289 | - */ |
|
290 | - public function setLimit($limit) |
|
291 | - { |
|
292 | - if (!is_int($limit) || $limit < 1) { |
|
293 | - throw new \InvalidArgumentException('The limit must be an integer >= 1', 1245071870); |
|
294 | - } |
|
295 | - $this->limit = $limit; |
|
296 | - return $this; |
|
297 | - } |
|
298 | - |
|
299 | - /** |
|
300 | - * Resets a previously set maximum size of the result set. Returns $this to allow |
|
301 | - * for chaining (fluid interface) |
|
302 | - * |
|
303 | - * @return QueryInterface |
|
304 | - * @api |
|
305 | - */ |
|
306 | - public function unsetLimit() |
|
307 | - { |
|
308 | - unset($this->limit); |
|
309 | - return $this; |
|
310 | - } |
|
311 | - |
|
312 | - /** |
|
313 | - * Returns the maximum size of the result set to limit. |
|
314 | - * |
|
315 | - * @return integer |
|
316 | - * @api |
|
317 | - */ |
|
318 | - public function getLimit() |
|
319 | - { |
|
320 | - return $this->limit; |
|
321 | - } |
|
322 | - |
|
323 | - /** |
|
324 | - * Sets the start offset of the result set to offset. Returns $this to |
|
325 | - * allow for chaining (fluid interface) |
|
326 | - * |
|
327 | - * @param integer $offset |
|
328 | - * @throws \InvalidArgumentException |
|
329 | - * @return QueryInterface |
|
330 | - * @api |
|
331 | - */ |
|
332 | - public function setOffset($offset) |
|
333 | - { |
|
334 | - if (!is_int($offset) || $offset < 0) { |
|
335 | - throw new \InvalidArgumentException('The offset must be a positive integer', 1245071872); |
|
336 | - } |
|
337 | - $this->offset = $offset; |
|
338 | - return $this; |
|
339 | - } |
|
340 | - |
|
341 | - /** |
|
342 | - * Returns the start offset of the result set. |
|
343 | - * |
|
344 | - * @return integer |
|
345 | - * @api |
|
346 | - */ |
|
347 | - public function getOffset() |
|
348 | - { |
|
349 | - return $this->offset; |
|
350 | - } |
|
351 | - |
|
352 | - /** |
|
353 | - * The constraint used to limit the result set. Returns $this to allow |
|
354 | - * for chaining (fluid interface) |
|
355 | - * |
|
356 | - * @param ConstraintInterface $constraint |
|
357 | - * @return QueryInterface |
|
358 | - * @api |
|
359 | - */ |
|
360 | - public function matching($constraint) |
|
361 | - { |
|
362 | - $this->constraint = $constraint; |
|
363 | - return $this; |
|
364 | - } |
|
365 | - |
|
366 | - /** |
|
367 | - * Gets the constraint for this query. |
|
368 | - * |
|
369 | - * @return ConstraintInterface the constraint, or null if none |
|
370 | - * @api |
|
371 | - */ |
|
372 | - public function getConstraint() |
|
373 | - { |
|
374 | - return $this->constraint; |
|
375 | - } |
|
376 | - |
|
377 | - /** |
|
378 | - * Performs a logical conjunction of the given constraints. The method takes one or more contraints and concatenates them with a boolean AND. |
|
379 | - * It also scepts a single array of constraints to be concatenated. |
|
380 | - * |
|
381 | - * @param mixed $constraint1 The first of multiple constraints or an array of constraints. |
|
382 | - * @throws InvalidNumberOfConstraintsException |
|
383 | - * @return AndInterface |
|
384 | - * @api |
|
385 | - */ |
|
386 | - public function logicalAnd($constraint1) |
|
387 | - { |
|
388 | - if (is_array($constraint1)) { |
|
389 | - $resultingConstraint = array_shift($constraint1); |
|
390 | - $constraints = $constraint1; |
|
391 | - } else { |
|
392 | - $constraints = func_get_args(); |
|
393 | - $resultingConstraint = array_shift($constraints); |
|
394 | - } |
|
395 | - if ($resultingConstraint === null) { |
|
396 | - throw new InvalidNumberOfConstraintsException('There must be at least one constraint or a non-empty array of constraints given.', 1401289500); |
|
397 | - } |
|
398 | - foreach ($constraints as $constraint) { |
|
399 | - $resultingConstraint = $this->qomFactory->_and($resultingConstraint, $constraint); |
|
400 | - } |
|
401 | - return $resultingConstraint; |
|
402 | - } |
|
403 | - |
|
404 | - /** |
|
405 | - * Performs a logical disjunction of the two given constraints |
|
406 | - * |
|
407 | - * @param mixed $constraint1 The first of multiple constraints or an array of constraints. |
|
408 | - * @throws InvalidNumberOfConstraintsException |
|
409 | - * @return OrInterface |
|
410 | - * @api |
|
411 | - */ |
|
412 | - public function logicalOr($constraint1) |
|
413 | - { |
|
414 | - if (is_array($constraint1)) { |
|
415 | - $resultingConstraint = array_shift($constraint1); |
|
416 | - $constraints = $constraint1; |
|
417 | - } else { |
|
418 | - $constraints = func_get_args(); |
|
419 | - $resultingConstraint = array_shift($constraints); |
|
420 | - } |
|
421 | - if ($resultingConstraint === null) { |
|
422 | - throw new InvalidNumberOfConstraintsException('There must be at least one constraint or a non-empty array of constraints given.', 1401289501); |
|
423 | - } |
|
424 | - foreach ($constraints as $constraint) { |
|
425 | - $resultingConstraint = $this->qomFactory->_or($resultingConstraint, $constraint); |
|
426 | - } |
|
427 | - return $resultingConstraint; |
|
428 | - } |
|
429 | - |
|
430 | - /** |
|
431 | - * Performs a logical negation of the given constraint |
|
432 | - * |
|
433 | - * @param ConstraintInterface $constraint Constraint to negate |
|
434 | - * @throws \RuntimeException |
|
435 | - * @return NotInterface |
|
436 | - * @api |
|
437 | - */ |
|
438 | - public function logicalNot(ConstraintInterface $constraint) |
|
439 | - { |
|
440 | - return $this->qomFactory->not($constraint); |
|
441 | - } |
|
442 | - |
|
443 | - /** |
|
444 | - * Returns an equals criterion used for matching objects against a query |
|
445 | - * |
|
446 | - * @param string $propertyName The name of the property to compare against |
|
447 | - * @param mixed $operand The value to compare with |
|
448 | - * @param boolean $caseSensitive Whether the equality test should be done case-sensitive |
|
449 | - * @return ComparisonInterface |
|
450 | - * @api |
|
451 | - */ |
|
452 | - public function equals($propertyName, $operand, $caseSensitive = true) |
|
453 | - { |
|
454 | - if (is_object($operand) || $caseSensitive) { |
|
455 | - $comparison = $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_EQUAL_TO, $operand); |
|
456 | - } else { |
|
457 | - $comparison = $this->qomFactory->comparison($this->qomFactory->lowerCase($this->qomFactory->propertyValue($propertyName, $this->getSelectorName())), QueryInterface::OPERATOR_EQUAL_TO, mb_strtolower($operand, \TYPO3\CMS\Extbase\Persistence\Generic\Query::CHARSET)); |
|
458 | - } |
|
459 | - return $comparison; |
|
460 | - } |
|
461 | - |
|
462 | - /** |
|
463 | - * Returns a like criterion used for matching objects against a query |
|
464 | - * |
|
465 | - * @param string $propertyName The name of the property to compare against |
|
466 | - * @param mixed $operand The value to compare with |
|
467 | - * @param boolean $caseSensitive Whether the matching should be done case-sensitive |
|
468 | - * @return ComparisonInterface |
|
469 | - * @api |
|
470 | - */ |
|
471 | - public function like($propertyName, $operand, $caseSensitive = true) |
|
472 | - { |
|
473 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LIKE, $operand); |
|
474 | - } |
|
475 | - |
|
476 | - /** |
|
477 | - * Returns a "contains" criterion used for matching objects against a query. |
|
478 | - * It matches if the multivalued property contains the given operand. |
|
479 | - * |
|
480 | - * @param string $propertyName The name of the (multivalued) property to compare against |
|
481 | - * @param mixed $operand The value to compare with |
|
482 | - * @return ComparisonInterface |
|
483 | - * @api |
|
484 | - */ |
|
485 | - public function contains($propertyName, $operand) |
|
486 | - { |
|
487 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_CONTAINS, $operand); |
|
488 | - } |
|
489 | - |
|
490 | - /** |
|
491 | - * Returns an "in" criterion used for matching objects against a query. It |
|
492 | - * matches if the property's value is contained in the multivalued operand. |
|
493 | - * |
|
494 | - * @param string $propertyName The name of the property to compare against |
|
495 | - * @param mixed $operand The value to compare with, multivalued |
|
496 | - * @throws UnexpectedTypeException |
|
497 | - * @return ComparisonInterface |
|
498 | - * @api |
|
499 | - */ |
|
500 | - public function in($propertyName, $operand) |
|
501 | - { |
|
502 | - if (!is_array($operand) && !$operand instanceof \ArrayAccess && !$operand instanceof \Traversable) { |
|
503 | - throw new UnexpectedTypeException('The "in" operator must be given a mutlivalued operand (array, ArrayAccess, Traversable).', 1264678095); |
|
504 | - } |
|
505 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_IN, $operand); |
|
506 | - } |
|
507 | - |
|
508 | - /** |
|
509 | - * Returns a less than criterion used for matching objects against a query |
|
510 | - * |
|
511 | - * @param string $propertyName The name of the property to compare against |
|
512 | - * @param mixed $operand The value to compare with |
|
513 | - * @return ComparisonInterface |
|
514 | - * @api |
|
515 | - */ |
|
516 | - public function lessThan($propertyName, $operand) |
|
517 | - { |
|
518 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN, $operand); |
|
519 | - } |
|
520 | - |
|
521 | - /** |
|
522 | - * Returns a less or equal than criterion used for matching objects against a query |
|
523 | - * |
|
524 | - * @param string $propertyName The name of the property to compare against |
|
525 | - * @param mixed $operand The value to compare with |
|
526 | - * @return ComparisonInterface |
|
527 | - * @api |
|
528 | - */ |
|
529 | - public function lessThanOrEqual($propertyName, $operand) |
|
530 | - { |
|
531 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO, $operand); |
|
532 | - } |
|
533 | - |
|
534 | - /** |
|
535 | - * Returns a greater than criterion used for matching objects against a query |
|
536 | - * |
|
537 | - * @param string $propertyName The name of the property to compare against |
|
538 | - * @param mixed $operand The value to compare with |
|
539 | - * @return ComparisonInterface |
|
540 | - * @api |
|
541 | - */ |
|
542 | - public function greaterThan($propertyName, $operand) |
|
543 | - { |
|
544 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN, $operand); |
|
545 | - } |
|
546 | - |
|
547 | - /** |
|
548 | - * Returns a greater than or equal criterion used for matching objects against a query |
|
549 | - * |
|
550 | - * @param string $propertyName The name of the property to compare against |
|
551 | - * @param mixed $operand The value to compare with |
|
552 | - * @return ComparisonInterface |
|
553 | - * @api |
|
554 | - */ |
|
555 | - public function greaterThanOrEqual($propertyName, $operand) |
|
556 | - { |
|
557 | - return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO, $operand); |
|
558 | - } |
|
559 | - |
|
560 | - /** |
|
561 | - * Returns the query result count. |
|
562 | - * |
|
563 | - * @return integer The query result count |
|
564 | - * @api |
|
565 | - */ |
|
566 | - public function count() |
|
567 | - { |
|
568 | - /** @var VidiDbBackend $backend */ |
|
569 | - $backend = GeneralUtility::makeInstance(VidiDbBackend::class, $this); |
|
570 | - return $backend->countResult(); |
|
571 | - } |
|
572 | - |
|
573 | - /** |
|
574 | - * Returns an "isEmpty" criterion used for matching objects against a query. |
|
575 | - * It matches if the multivalued property contains no values or is null. |
|
576 | - * |
|
577 | - * @param string $propertyName The name of the multivalued property to compare against |
|
578 | - * @throws NotImplementedException |
|
579 | - * @throws InvalidQueryException if used on a single-valued property |
|
580 | - * @api |
|
581 | - */ |
|
582 | - public function isEmpty($propertyName) |
|
583 | - { |
|
584 | - throw new NotImplementedException(__METHOD__); |
|
585 | - } |
|
586 | - |
|
587 | - /** |
|
588 | - * @return string |
|
589 | - */ |
|
590 | - public function getDistinct() |
|
591 | - { |
|
592 | - return $this->distinct; |
|
593 | - } |
|
594 | - |
|
595 | - /** |
|
596 | - * @param string $distinct |
|
597 | - * @return $this |
|
598 | - */ |
|
599 | - public function setDistinct($distinct) |
|
600 | - { |
|
601 | - $this->distinct = $distinct; |
|
602 | - return $this; |
|
603 | - } |
|
604 | - |
|
605 | - /** |
|
606 | - * Sets the statement of this query. If you use this, you will lose the abstraction from a concrete storage |
|
607 | - * backend (database). |
|
608 | - * |
|
609 | - * @param string|\TYPO3\CMS\Core\Database\PreparedStatement $statement The statement |
|
610 | - * @param array $parameters An array of parameters. These will be bound to placeholders '?' in the $statement. |
|
611 | - * @return QueryInterface |
|
612 | - */ |
|
613 | - public function statement($statement, array $parameters = array()) |
|
614 | - { |
|
615 | - $this->statement = $this->qomFactory->statement($statement, $parameters); |
|
616 | - return $this; |
|
617 | - } |
|
618 | - |
|
619 | - /** |
|
620 | - * Returns the statement of this query. |
|
621 | - * |
|
622 | - * @return Statement |
|
623 | - */ |
|
624 | - public function getStatement() |
|
625 | - { |
|
626 | - return $this->statement; |
|
627 | - } |
|
628 | - |
|
629 | - /** |
|
630 | - * Returns whether the current mode is Backend. |
|
631 | - * |
|
632 | - * @return bool |
|
633 | - */ |
|
634 | - protected function isBackendMode() |
|
635 | - { |
|
636 | - return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); |
|
637 | - } |
|
638 | - |
|
639 | - /** |
|
640 | - * @return string |
|
641 | - */ |
|
642 | - public function getSourceFieldName() |
|
643 | - { |
|
644 | - return $this->sourceFieldName; |
|
645 | - } |
|
646 | - |
|
647 | - /** |
|
648 | - * @param string $sourceFieldName |
|
649 | - * @return $this |
|
650 | - */ |
|
651 | - public function setSourceFieldName($sourceFieldName) |
|
652 | - { |
|
653 | - $this->sourceFieldName = $sourceFieldName; |
|
654 | - return $this; |
|
655 | - } |
|
656 | - |
|
657 | - public function setQuerySettings(QuerySettingsInterface $querySettings) |
|
658 | - { |
|
659 | - $this->typo3QuerySettings = $querySettings; |
|
660 | - } |
|
661 | - |
|
662 | - public function getQuerySettings() |
|
663 | - { |
|
664 | - return $this->typo3QuerySettings; |
|
665 | - } |
|
44 | + /** |
|
45 | + * An inner join. |
|
46 | + */ |
|
47 | + const JCR_JOIN_TYPE_INNER = '{http://www.jcp.org/jcr/1.0}joinTypeInner'; |
|
48 | + |
|
49 | + /** |
|
50 | + * A left-outer join. |
|
51 | + */ |
|
52 | + const JCR_JOIN_TYPE_LEFT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeLeftOuter'; |
|
53 | + |
|
54 | + /** |
|
55 | + * A right-outer join. |
|
56 | + */ |
|
57 | + const JCR_JOIN_TYPE_RIGHT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeRightOuter'; |
|
58 | + |
|
59 | + /** |
|
60 | + * Charset of strings in QOM |
|
61 | + */ |
|
62 | + const CHARSET = 'utf-8'; |
|
63 | + |
|
64 | + /** |
|
65 | + * @var string |
|
66 | + */ |
|
67 | + protected $sourceFieldName; |
|
68 | + |
|
69 | + /** |
|
70 | + * @var string |
|
71 | + */ |
|
72 | + protected $type; |
|
73 | + |
|
74 | + /** |
|
75 | + * @var PersistenceManagerInterface |
|
76 | + */ |
|
77 | + protected $persistenceManager; |
|
78 | + |
|
79 | + /** |
|
80 | + * @var QueryObjectModelFactory |
|
81 | + */ |
|
82 | + protected $qomFactory; |
|
83 | + |
|
84 | + /** |
|
85 | + * @var SourceInterface |
|
86 | + */ |
|
87 | + protected $source; |
|
88 | + |
|
89 | + /** |
|
90 | + * @var ConstraintInterface |
|
91 | + */ |
|
92 | + protected $constraint; |
|
93 | + |
|
94 | + /** |
|
95 | + * @var Statement |
|
96 | + */ |
|
97 | + protected $statement; |
|
98 | + |
|
99 | + /** |
|
100 | + * @var array |
|
101 | + */ |
|
102 | + protected $orderings = []; |
|
103 | + |
|
104 | + /** |
|
105 | + * @var int |
|
106 | + */ |
|
107 | + protected $limit; |
|
108 | + |
|
109 | + /** |
|
110 | + * @var int |
|
111 | + */ |
|
112 | + protected $offset; |
|
113 | + |
|
114 | + /** |
|
115 | + * Apply DISTINCT upon property. |
|
116 | + * |
|
117 | + * @var string |
|
118 | + */ |
|
119 | + protected $distinct; |
|
120 | + |
|
121 | + /** |
|
122 | + * The query settings. |
|
123 | + * |
|
124 | + * @var Typo3QuerySettings |
|
125 | + */ |
|
126 | + public Typo3QuerySettings $typo3QuerySettings; |
|
127 | + |
|
128 | + /** |
|
129 | + * Constructs a query object working on the given class name |
|
130 | + * |
|
131 | + * @param string $type |
|
132 | + */ |
|
133 | + public function __construct($type) |
|
134 | + { |
|
135 | + $this->type = $type; |
|
136 | + $this->persistenceManager = GeneralUtility::makeInstance(PersistenceManagerInterface::class); |
|
137 | + $this->qomFactory = GeneralUtility::makeInstance(QueryObjectModelFactory::class); |
|
138 | + } |
|
139 | + |
|
140 | + public function injectTypo3QuerySettings(Typo3QuerySettings $querySettings): void |
|
141 | + { |
|
142 | + $this->typo3QuerySettings = $querySettings; |
|
143 | + } |
|
144 | + |
|
145 | + /** |
|
146 | + * Sets the Query Settings. These Query settings must match the settings expected by |
|
147 | + * the specific Storage Backend. |
|
148 | + * |
|
149 | + * @param QuerySettingsInterface $typo3QuerySettings The Query Settings |
|
150 | + * @return void |
|
151 | + */ |
|
152 | + public function setTypo3QuerySettings(QuerySettingsInterface $typo3QuerySettings) |
|
153 | + { |
|
154 | + $this->typo3QuerySettings = $typo3QuerySettings; |
|
155 | + } |
|
156 | + |
|
157 | + /** |
|
158 | + * Returns the Query Settings. |
|
159 | + * |
|
160 | + * @throws \Exception |
|
161 | + * @return Typo3QuerySettings $querySettings The Query Settings |
|
162 | + * @api This method is not part of FLOW3 API |
|
163 | + */ |
|
164 | + public function getTypo3QuerySettings() |
|
165 | + { |
|
166 | + if (!$this->typo3QuerySettings instanceof QuerySettingsInterface) { |
|
167 | + throw new Exception('Tried to get the query settings without setting them before.', 1248689115); |
|
168 | + } |
|
169 | + |
|
170 | + // Apply possible settings to the query. |
|
171 | + if ($this->isBackendMode()) { |
|
172 | + /** @var BackendConfigurationManager $backendConfigurationManager */ |
|
173 | + $backendConfigurationManager = GeneralUtility::makeInstance(BackendConfigurationManager::class); |
|
174 | + $configuration = $backendConfigurationManager->getTypoScriptSetup(); |
|
175 | + $querySettings = array('respectSysLanguage'); |
|
176 | + foreach ($querySettings as $setting) { |
|
177 | + if (isset($configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type . '.'][$setting])) { |
|
178 | + $value = (bool)$configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type . '.'][$setting]; |
|
179 | + ObjectAccess::setProperty($this->typo3QuerySettings, $setting, $value); |
|
180 | + } |
|
181 | + } |
|
182 | + } |
|
183 | + |
|
184 | + return $this->typo3QuerySettings; |
|
185 | + } |
|
186 | + |
|
187 | + /** |
|
188 | + * Returns the type this query cares for. |
|
189 | + * |
|
190 | + * @return string |
|
191 | + * @api |
|
192 | + */ |
|
193 | + public function getType() |
|
194 | + { |
|
195 | + return $this->type; |
|
196 | + } |
|
197 | + |
|
198 | + /** |
|
199 | + * Sets the source to fetch the result from |
|
200 | + * |
|
201 | + * @param SourceInterface $source |
|
202 | + */ |
|
203 | + public function setSource(SourceInterface $source) |
|
204 | + { |
|
205 | + $this->source = $source; |
|
206 | + } |
|
207 | + |
|
208 | + /** |
|
209 | + * Returns the selectorn name or an empty string, if the source is not a selector |
|
210 | + * TODO This has to be checked at another place |
|
211 | + * |
|
212 | + * @return string The selector name |
|
213 | + */ |
|
214 | + protected function getSelectorName() |
|
215 | + { |
|
216 | + if ($this->getSource() instanceof SelectorInterface) { |
|
217 | + return $this->source->getSelectorName(); |
|
218 | + } else { |
|
219 | + return ''; |
|
220 | + } |
|
221 | + } |
|
222 | + |
|
223 | + /** |
|
224 | + * Gets the node-tuple source for this query. |
|
225 | + * |
|
226 | + * @return SourceInterface the node-tuple source; non-null |
|
227 | + */ |
|
228 | + public function getSource() |
|
229 | + { |
|
230 | + if ($this->source === null) { |
|
231 | + $this->source = $this->qomFactory->selector($this->getType()); |
|
232 | + } |
|
233 | + return $this->source; |
|
234 | + } |
|
235 | + |
|
236 | + /** |
|
237 | + * Executes the query against the database and returns the result |
|
238 | + * |
|
239 | + * @return QueryResultInterface|array The query result object or an array if $this->getQuerySettings()->getReturnRawQueryResult() is true |
|
240 | + * @api |
|
241 | + */ |
|
242 | + public function execute($returnRawQueryResult = false) |
|
243 | + { |
|
244 | + /** @var VidiDbBackend $backend */ |
|
245 | + $backend = GeneralUtility::makeInstance(VidiDbBackend::class, $this); |
|
246 | + return $backend->fetchResult(); |
|
247 | + } |
|
248 | + |
|
249 | + /** |
|
250 | + * Sets the property names to order the result by. Expected like this: |
|
251 | + * array( |
|
252 | + * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING, |
|
253 | + * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING |
|
254 | + * ) |
|
255 | + * where 'foo' and 'bar' are property names. |
|
256 | + * |
|
257 | + * @param array $orderings The property names to order by |
|
258 | + * @return QueryInterface |
|
259 | + * @api |
|
260 | + */ |
|
261 | + public function setOrderings(array $orderings) |
|
262 | + { |
|
263 | + $this->orderings = $orderings; |
|
264 | + return $this; |
|
265 | + } |
|
266 | + |
|
267 | + /** |
|
268 | + * Returns the property names to order the result by. Like this: |
|
269 | + * array( |
|
270 | + * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING, |
|
271 | + * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING |
|
272 | + * ) |
|
273 | + * |
|
274 | + * @return array |
|
275 | + */ |
|
276 | + public function getOrderings() |
|
277 | + { |
|
278 | + return $this->orderings; |
|
279 | + } |
|
280 | + |
|
281 | + /** |
|
282 | + * Sets the maximum size of the result set to limit. Returns $this to allow |
|
283 | + * for chaining (fluid interface) |
|
284 | + * |
|
285 | + * @param integer $limit |
|
286 | + * @throws \InvalidArgumentException |
|
287 | + * @return QueryInterface |
|
288 | + * @api |
|
289 | + */ |
|
290 | + public function setLimit($limit) |
|
291 | + { |
|
292 | + if (!is_int($limit) || $limit < 1) { |
|
293 | + throw new \InvalidArgumentException('The limit must be an integer >= 1', 1245071870); |
|
294 | + } |
|
295 | + $this->limit = $limit; |
|
296 | + return $this; |
|
297 | + } |
|
298 | + |
|
299 | + /** |
|
300 | + * Resets a previously set maximum size of the result set. Returns $this to allow |
|
301 | + * for chaining (fluid interface) |
|
302 | + * |
|
303 | + * @return QueryInterface |
|
304 | + * @api |
|
305 | + */ |
|
306 | + public function unsetLimit() |
|
307 | + { |
|
308 | + unset($this->limit); |
|
309 | + return $this; |
|
310 | + } |
|
311 | + |
|
312 | + /** |
|
313 | + * Returns the maximum size of the result set to limit. |
|
314 | + * |
|
315 | + * @return integer |
|
316 | + * @api |
|
317 | + */ |
|
318 | + public function getLimit() |
|
319 | + { |
|
320 | + return $this->limit; |
|
321 | + } |
|
322 | + |
|
323 | + /** |
|
324 | + * Sets the start offset of the result set to offset. Returns $this to |
|
325 | + * allow for chaining (fluid interface) |
|
326 | + * |
|
327 | + * @param integer $offset |
|
328 | + * @throws \InvalidArgumentException |
|
329 | + * @return QueryInterface |
|
330 | + * @api |
|
331 | + */ |
|
332 | + public function setOffset($offset) |
|
333 | + { |
|
334 | + if (!is_int($offset) || $offset < 0) { |
|
335 | + throw new \InvalidArgumentException('The offset must be a positive integer', 1245071872); |
|
336 | + } |
|
337 | + $this->offset = $offset; |
|
338 | + return $this; |
|
339 | + } |
|
340 | + |
|
341 | + /** |
|
342 | + * Returns the start offset of the result set. |
|
343 | + * |
|
344 | + * @return integer |
|
345 | + * @api |
|
346 | + */ |
|
347 | + public function getOffset() |
|
348 | + { |
|
349 | + return $this->offset; |
|
350 | + } |
|
351 | + |
|
352 | + /** |
|
353 | + * The constraint used to limit the result set. Returns $this to allow |
|
354 | + * for chaining (fluid interface) |
|
355 | + * |
|
356 | + * @param ConstraintInterface $constraint |
|
357 | + * @return QueryInterface |
|
358 | + * @api |
|
359 | + */ |
|
360 | + public function matching($constraint) |
|
361 | + { |
|
362 | + $this->constraint = $constraint; |
|
363 | + return $this; |
|
364 | + } |
|
365 | + |
|
366 | + /** |
|
367 | + * Gets the constraint for this query. |
|
368 | + * |
|
369 | + * @return ConstraintInterface the constraint, or null if none |
|
370 | + * @api |
|
371 | + */ |
|
372 | + public function getConstraint() |
|
373 | + { |
|
374 | + return $this->constraint; |
|
375 | + } |
|
376 | + |
|
377 | + /** |
|
378 | + * Performs a logical conjunction of the given constraints. The method takes one or more contraints and concatenates them with a boolean AND. |
|
379 | + * It also scepts a single array of constraints to be concatenated. |
|
380 | + * |
|
381 | + * @param mixed $constraint1 The first of multiple constraints or an array of constraints. |
|
382 | + * @throws InvalidNumberOfConstraintsException |
|
383 | + * @return AndInterface |
|
384 | + * @api |
|
385 | + */ |
|
386 | + public function logicalAnd($constraint1) |
|
387 | + { |
|
388 | + if (is_array($constraint1)) { |
|
389 | + $resultingConstraint = array_shift($constraint1); |
|
390 | + $constraints = $constraint1; |
|
391 | + } else { |
|
392 | + $constraints = func_get_args(); |
|
393 | + $resultingConstraint = array_shift($constraints); |
|
394 | + } |
|
395 | + if ($resultingConstraint === null) { |
|
396 | + throw new InvalidNumberOfConstraintsException('There must be at least one constraint or a non-empty array of constraints given.', 1401289500); |
|
397 | + } |
|
398 | + foreach ($constraints as $constraint) { |
|
399 | + $resultingConstraint = $this->qomFactory->_and($resultingConstraint, $constraint); |
|
400 | + } |
|
401 | + return $resultingConstraint; |
|
402 | + } |
|
403 | + |
|
404 | + /** |
|
405 | + * Performs a logical disjunction of the two given constraints |
|
406 | + * |
|
407 | + * @param mixed $constraint1 The first of multiple constraints or an array of constraints. |
|
408 | + * @throws InvalidNumberOfConstraintsException |
|
409 | + * @return OrInterface |
|
410 | + * @api |
|
411 | + */ |
|
412 | + public function logicalOr($constraint1) |
|
413 | + { |
|
414 | + if (is_array($constraint1)) { |
|
415 | + $resultingConstraint = array_shift($constraint1); |
|
416 | + $constraints = $constraint1; |
|
417 | + } else { |
|
418 | + $constraints = func_get_args(); |
|
419 | + $resultingConstraint = array_shift($constraints); |
|
420 | + } |
|
421 | + if ($resultingConstraint === null) { |
|
422 | + throw new InvalidNumberOfConstraintsException('There must be at least one constraint or a non-empty array of constraints given.', 1401289501); |
|
423 | + } |
|
424 | + foreach ($constraints as $constraint) { |
|
425 | + $resultingConstraint = $this->qomFactory->_or($resultingConstraint, $constraint); |
|
426 | + } |
|
427 | + return $resultingConstraint; |
|
428 | + } |
|
429 | + |
|
430 | + /** |
|
431 | + * Performs a logical negation of the given constraint |
|
432 | + * |
|
433 | + * @param ConstraintInterface $constraint Constraint to negate |
|
434 | + * @throws \RuntimeException |
|
435 | + * @return NotInterface |
|
436 | + * @api |
|
437 | + */ |
|
438 | + public function logicalNot(ConstraintInterface $constraint) |
|
439 | + { |
|
440 | + return $this->qomFactory->not($constraint); |
|
441 | + } |
|
442 | + |
|
443 | + /** |
|
444 | + * Returns an equals criterion used for matching objects against a query |
|
445 | + * |
|
446 | + * @param string $propertyName The name of the property to compare against |
|
447 | + * @param mixed $operand The value to compare with |
|
448 | + * @param boolean $caseSensitive Whether the equality test should be done case-sensitive |
|
449 | + * @return ComparisonInterface |
|
450 | + * @api |
|
451 | + */ |
|
452 | + public function equals($propertyName, $operand, $caseSensitive = true) |
|
453 | + { |
|
454 | + if (is_object($operand) || $caseSensitive) { |
|
455 | + $comparison = $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_EQUAL_TO, $operand); |
|
456 | + } else { |
|
457 | + $comparison = $this->qomFactory->comparison($this->qomFactory->lowerCase($this->qomFactory->propertyValue($propertyName, $this->getSelectorName())), QueryInterface::OPERATOR_EQUAL_TO, mb_strtolower($operand, \TYPO3\CMS\Extbase\Persistence\Generic\Query::CHARSET)); |
|
458 | + } |
|
459 | + return $comparison; |
|
460 | + } |
|
461 | + |
|
462 | + /** |
|
463 | + * Returns a like criterion used for matching objects against a query |
|
464 | + * |
|
465 | + * @param string $propertyName The name of the property to compare against |
|
466 | + * @param mixed $operand The value to compare with |
|
467 | + * @param boolean $caseSensitive Whether the matching should be done case-sensitive |
|
468 | + * @return ComparisonInterface |
|
469 | + * @api |
|
470 | + */ |
|
471 | + public function like($propertyName, $operand, $caseSensitive = true) |
|
472 | + { |
|
473 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LIKE, $operand); |
|
474 | + } |
|
475 | + |
|
476 | + /** |
|
477 | + * Returns a "contains" criterion used for matching objects against a query. |
|
478 | + * It matches if the multivalued property contains the given operand. |
|
479 | + * |
|
480 | + * @param string $propertyName The name of the (multivalued) property to compare against |
|
481 | + * @param mixed $operand The value to compare with |
|
482 | + * @return ComparisonInterface |
|
483 | + * @api |
|
484 | + */ |
|
485 | + public function contains($propertyName, $operand) |
|
486 | + { |
|
487 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_CONTAINS, $operand); |
|
488 | + } |
|
489 | + |
|
490 | + /** |
|
491 | + * Returns an "in" criterion used for matching objects against a query. It |
|
492 | + * matches if the property's value is contained in the multivalued operand. |
|
493 | + * |
|
494 | + * @param string $propertyName The name of the property to compare against |
|
495 | + * @param mixed $operand The value to compare with, multivalued |
|
496 | + * @throws UnexpectedTypeException |
|
497 | + * @return ComparisonInterface |
|
498 | + * @api |
|
499 | + */ |
|
500 | + public function in($propertyName, $operand) |
|
501 | + { |
|
502 | + if (!is_array($operand) && !$operand instanceof \ArrayAccess && !$operand instanceof \Traversable) { |
|
503 | + throw new UnexpectedTypeException('The "in" operator must be given a mutlivalued operand (array, ArrayAccess, Traversable).', 1264678095); |
|
504 | + } |
|
505 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_IN, $operand); |
|
506 | + } |
|
507 | + |
|
508 | + /** |
|
509 | + * Returns a less than criterion used for matching objects against a query |
|
510 | + * |
|
511 | + * @param string $propertyName The name of the property to compare against |
|
512 | + * @param mixed $operand The value to compare with |
|
513 | + * @return ComparisonInterface |
|
514 | + * @api |
|
515 | + */ |
|
516 | + public function lessThan($propertyName, $operand) |
|
517 | + { |
|
518 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN, $operand); |
|
519 | + } |
|
520 | + |
|
521 | + /** |
|
522 | + * Returns a less or equal than criterion used for matching objects against a query |
|
523 | + * |
|
524 | + * @param string $propertyName The name of the property to compare against |
|
525 | + * @param mixed $operand The value to compare with |
|
526 | + * @return ComparisonInterface |
|
527 | + * @api |
|
528 | + */ |
|
529 | + public function lessThanOrEqual($propertyName, $operand) |
|
530 | + { |
|
531 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO, $operand); |
|
532 | + } |
|
533 | + |
|
534 | + /** |
|
535 | + * Returns a greater than criterion used for matching objects against a query |
|
536 | + * |
|
537 | + * @param string $propertyName The name of the property to compare against |
|
538 | + * @param mixed $operand The value to compare with |
|
539 | + * @return ComparisonInterface |
|
540 | + * @api |
|
541 | + */ |
|
542 | + public function greaterThan($propertyName, $operand) |
|
543 | + { |
|
544 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN, $operand); |
|
545 | + } |
|
546 | + |
|
547 | + /** |
|
548 | + * Returns a greater than or equal criterion used for matching objects against a query |
|
549 | + * |
|
550 | + * @param string $propertyName The name of the property to compare against |
|
551 | + * @param mixed $operand The value to compare with |
|
552 | + * @return ComparisonInterface |
|
553 | + * @api |
|
554 | + */ |
|
555 | + public function greaterThanOrEqual($propertyName, $operand) |
|
556 | + { |
|
557 | + return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO, $operand); |
|
558 | + } |
|
559 | + |
|
560 | + /** |
|
561 | + * Returns the query result count. |
|
562 | + * |
|
563 | + * @return integer The query result count |
|
564 | + * @api |
|
565 | + */ |
|
566 | + public function count() |
|
567 | + { |
|
568 | + /** @var VidiDbBackend $backend */ |
|
569 | + $backend = GeneralUtility::makeInstance(VidiDbBackend::class, $this); |
|
570 | + return $backend->countResult(); |
|
571 | + } |
|
572 | + |
|
573 | + /** |
|
574 | + * Returns an "isEmpty" criterion used for matching objects against a query. |
|
575 | + * It matches if the multivalued property contains no values or is null. |
|
576 | + * |
|
577 | + * @param string $propertyName The name of the multivalued property to compare against |
|
578 | + * @throws NotImplementedException |
|
579 | + * @throws InvalidQueryException if used on a single-valued property |
|
580 | + * @api |
|
581 | + */ |
|
582 | + public function isEmpty($propertyName) |
|
583 | + { |
|
584 | + throw new NotImplementedException(__METHOD__); |
|
585 | + } |
|
586 | + |
|
587 | + /** |
|
588 | + * @return string |
|
589 | + */ |
|
590 | + public function getDistinct() |
|
591 | + { |
|
592 | + return $this->distinct; |
|
593 | + } |
|
594 | + |
|
595 | + /** |
|
596 | + * @param string $distinct |
|
597 | + * @return $this |
|
598 | + */ |
|
599 | + public function setDistinct($distinct) |
|
600 | + { |
|
601 | + $this->distinct = $distinct; |
|
602 | + return $this; |
|
603 | + } |
|
604 | + |
|
605 | + /** |
|
606 | + * Sets the statement of this query. If you use this, you will lose the abstraction from a concrete storage |
|
607 | + * backend (database). |
|
608 | + * |
|
609 | + * @param string|\TYPO3\CMS\Core\Database\PreparedStatement $statement The statement |
|
610 | + * @param array $parameters An array of parameters. These will be bound to placeholders '?' in the $statement. |
|
611 | + * @return QueryInterface |
|
612 | + */ |
|
613 | + public function statement($statement, array $parameters = array()) |
|
614 | + { |
|
615 | + $this->statement = $this->qomFactory->statement($statement, $parameters); |
|
616 | + return $this; |
|
617 | + } |
|
618 | + |
|
619 | + /** |
|
620 | + * Returns the statement of this query. |
|
621 | + * |
|
622 | + * @return Statement |
|
623 | + */ |
|
624 | + public function getStatement() |
|
625 | + { |
|
626 | + return $this->statement; |
|
627 | + } |
|
628 | + |
|
629 | + /** |
|
630 | + * Returns whether the current mode is Backend. |
|
631 | + * |
|
632 | + * @return bool |
|
633 | + */ |
|
634 | + protected function isBackendMode() |
|
635 | + { |
|
636 | + return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); |
|
637 | + } |
|
638 | + |
|
639 | + /** |
|
640 | + * @return string |
|
641 | + */ |
|
642 | + public function getSourceFieldName() |
|
643 | + { |
|
644 | + return $this->sourceFieldName; |
|
645 | + } |
|
646 | + |
|
647 | + /** |
|
648 | + * @param string $sourceFieldName |
|
649 | + * @return $this |
|
650 | + */ |
|
651 | + public function setSourceFieldName($sourceFieldName) |
|
652 | + { |
|
653 | + $this->sourceFieldName = $sourceFieldName; |
|
654 | + return $this; |
|
655 | + } |
|
656 | + |
|
657 | + public function setQuerySettings(QuerySettingsInterface $querySettings) |
|
658 | + { |
|
659 | + $this->typo3QuerySettings = $querySettings; |
|
660 | + } |
|
661 | + |
|
662 | + public function getQuerySettings() |
|
663 | + { |
|
664 | + return $this->typo3QuerySettings; |
|
665 | + } |
|
666 | 666 | } |
@@ -174,8 +174,8 @@ |
||
174 | 174 | $configuration = $backendConfigurationManager->getTypoScriptSetup(); |
175 | 175 | $querySettings = array('respectSysLanguage'); |
176 | 176 | foreach ($querySettings as $setting) { |
177 | - if (isset($configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type . '.'][$setting])) { |
|
178 | - $value = (bool)$configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type . '.'][$setting]; |
|
177 | + if (isset($configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type.'.'][$setting])) { |
|
178 | + $value = (bool)$configuration['config.']['tx_vidi.']['persistence.']['backend.'][$this->type.'.'][$setting]; |
|
179 | 179 | ObjectAccess::setProperty($this->typo3QuerySettings, $setting, $value); |
180 | 180 | } |
181 | 181 | } |
@@ -51,1052 +51,1052 @@ |
||
51 | 51 | class VidiDbBackend |
52 | 52 | { |
53 | 53 | |
54 | - const OPERATOR_EQUAL_TO_NULL = 'operatorEqualToNull'; |
|
55 | - const OPERATOR_NOT_EQUAL_TO_NULL = 'operatorNotEqualToNull'; |
|
56 | - |
|
57 | - /** |
|
58 | - * The TYPO3 page repository. Used for language and workspace overlay |
|
59 | - * |
|
60 | - * @var PageRepository |
|
61 | - */ |
|
62 | - protected $pageRepository; |
|
63 | - |
|
64 | - /** |
|
65 | - * @var Query |
|
66 | - */ |
|
67 | - protected $query; |
|
68 | - |
|
69 | - /** |
|
70 | - * Store some info related to table name and its aliases. |
|
71 | - * |
|
72 | - * @var array |
|
73 | - */ |
|
74 | - protected $tableNameAliases = array( |
|
75 | - 'aliases' => [], |
|
76 | - 'aliasIncrement' => [], |
|
77 | - ); |
|
78 | - |
|
79 | - /** |
|
80 | - * Use to store the current foreign table name alias. |
|
81 | - * |
|
82 | - * @var string |
|
83 | - */ |
|
84 | - protected $currentChildTableNameAlias = ''; |
|
85 | - |
|
86 | - /** |
|
87 | - * @param Query $query |
|
88 | - */ |
|
89 | - public function __construct(Query $query) |
|
90 | - { |
|
91 | - $this->query = $query; |
|
92 | - } |
|
93 | - |
|
94 | - /** |
|
95 | - * @param $parameters |
|
96 | - * @return array |
|
97 | - */ |
|
98 | - protected static function getTypes($parameters) |
|
99 | - { |
|
100 | - $types = []; |
|
101 | - foreach ($parameters as $parameter) { |
|
102 | - if (is_array($parameter)) { |
|
103 | - if (MathUtility::canBeInterpretedAsInteger($parameter[0])) { |
|
104 | - $types[] = \Doctrine\DBAL\Connection::PARAM_INT_ARRAY; |
|
105 | - } else { |
|
106 | - $types[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; |
|
107 | - } |
|
108 | - } else { |
|
109 | - if (MathUtility::canBeInterpretedAsInteger($parameter)) { |
|
110 | - $types[] = ParameterType::INTEGER; |
|
111 | - } else { |
|
112 | - $types[] = ParameterType::STRING; |
|
113 | - } |
|
114 | - } |
|
115 | - } |
|
116 | - return $types; |
|
117 | - } |
|
118 | - |
|
119 | - /** |
|
120 | - * Returns the result of the query |
|
121 | - */ |
|
122 | - public function fetchResult() |
|
123 | - { |
|
124 | - $parameters = []; |
|
125 | - $statementParts = $this->parseQuery($parameters); |
|
126 | - $statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); |
|
127 | - $sql = $this->buildQuery($statementParts); |
|
128 | - //print $sql; exit(); |
|
129 | - |
|
130 | - $rows = $this->getConnection() |
|
131 | - ->executeQuery($sql, $parameters, self::getTypes($parameters)) |
|
132 | - ->fetchAll(); |
|
133 | - |
|
134 | - return $this->getContentObjects($rows); |
|
135 | - } |
|
136 | - |
|
137 | - /** |
|
138 | - * Returns the number of tuples matching the query. |
|
139 | - * |
|
140 | - * @return int The number of matching tuples |
|
141 | - */ |
|
142 | - public function countResult() |
|
143 | - { |
|
144 | - $parameters = []; |
|
145 | - $statementParts = $this->parseQuery($parameters); |
|
146 | - $statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); |
|
147 | - $types = self::getTypes($parameters); |
|
148 | - |
|
149 | - // if limit is set, we need to count the rows "manually" as COUNT(*) ignores LIMIT constraints |
|
150 | - if (!empty($statementParts['limit'])) { |
|
151 | - $sql = $this->buildQuery($statementParts); |
|
152 | - |
|
153 | - $count = $this |
|
154 | - ->getConnection() |
|
155 | - ->executeQuery($sql, $parameters, $types) |
|
156 | - ->rowCount(); |
|
157 | - } else { |
|
158 | - $statementParts['fields'] = array('COUNT(*)'); |
|
159 | - // having orderings without grouping is not compatible with non-MySQL DBMS |
|
160 | - $statementParts['orderings'] = []; |
|
161 | - if (isset($statementParts['keywords']['distinct'])) { |
|
162 | - unset($statementParts['keywords']['distinct']); |
|
163 | - $distinctField = $this->query->getDistinct() ? $this->query->getDistinct() : 'uid'; |
|
164 | - $statementParts['fields'] = array('COUNT(DISTINCT ' . $statementParts['mainTable'] . '.' . $distinctField . ')'); |
|
165 | - } |
|
166 | - |
|
167 | - $sql = $this->buildQuery($statementParts); |
|
168 | - $count = $this |
|
169 | - ->getConnection() |
|
170 | - ->executeQuery($sql, $parameters, $types) |
|
171 | - ->fetchColumn(0); |
|
172 | - } |
|
173 | - return (int)$count; |
|
174 | - } |
|
175 | - |
|
176 | - /** |
|
177 | - * Parses the query and returns the SQL statement parts. |
|
178 | - * |
|
179 | - * @param array &$parameters |
|
180 | - * @return array |
|
181 | - */ |
|
182 | - public function parseQuery(array &$parameters) |
|
183 | - { |
|
184 | - $statementParts = []; |
|
185 | - $statementParts['keywords'] = []; |
|
186 | - $statementParts['tables'] = []; |
|
187 | - $statementParts['unions'] = []; |
|
188 | - $statementParts['fields'] = []; |
|
189 | - $statementParts['where'] = []; |
|
190 | - $statementParts['additionalWhereClause'] = []; |
|
191 | - $statementParts['orderings'] = []; |
|
192 | - $statementParts['limit'] = []; |
|
193 | - $query = $this->query; |
|
194 | - $source = $query->getSource(); |
|
195 | - $this->parseSource($source, $statementParts); |
|
196 | - $this->parseConstraint($query->getConstraint(), $source, $statementParts, $parameters); |
|
197 | - $this->parseOrderings($query->getOrderings(), $source, $statementParts); |
|
198 | - $this->parseLimitAndOffset($query->getLimit(), $query->getOffset(), $statementParts); |
|
199 | - $tableNames = array_unique(array_keys($statementParts['tables'] + $statementParts['unions'])); |
|
200 | - foreach ($tableNames as $tableNameOrAlias) { |
|
201 | - if (is_string($tableNameOrAlias) && strlen($tableNameOrAlias) > 0) { |
|
202 | - $this->addAdditionalWhereClause($query->getTypo3QuerySettings(), $tableNameOrAlias, $statementParts); |
|
203 | - } |
|
204 | - } |
|
205 | - |
|
206 | - return $statementParts; |
|
207 | - } |
|
208 | - |
|
209 | - /** |
|
210 | - * Fiddle with the statement structure to handle recursive MM relations. |
|
211 | - * For the recursive MM query to work, we must invert some values. |
|
212 | - * Let see if that is the best way of doing that... |
|
213 | - * |
|
214 | - * @param array $statementParts |
|
215 | - * @return array |
|
216 | - */ |
|
217 | - public function processStatementStructureForRecursiveMMRelation(array $statementParts) |
|
218 | - { |
|
219 | - |
|
220 | - if ($this->hasRecursiveMMRelation()) { |
|
221 | - $tableName = $this->query->getType(); |
|
222 | - |
|
223 | - // In order the MM query to work for a recursive MM query, we must invert some values. |
|
224 | - // tx_domain_model_foo0 (the alias) <--> tx_domain_model_foo (the origin table name) |
|
225 | - $values = []; |
|
226 | - foreach ($statementParts['fields'] as $key => $value) { |
|
227 | - $values[$key] = str_replace($tableName, $tableName . '0', $value); |
|
228 | - } |
|
229 | - $statementParts['fields'] = $values; |
|
230 | - |
|
231 | - // Same comment as above. |
|
232 | - $values = []; |
|
233 | - foreach ($statementParts['where'] as $key => $value) { |
|
234 | - $values[$key] = str_replace($tableName . '0', $tableName, $value); |
|
235 | - } |
|
236 | - $statementParts['where'] = $values; |
|
237 | - |
|
238 | - // We must be more restrictive by transforming the "left" union by "inner" |
|
239 | - $values = []; |
|
240 | - foreach ($statementParts['unions'] as $key => $value) { |
|
241 | - $values[$key] = str_replace('LEFT JOIN', 'INNER JOIN', $value); |
|
242 | - } |
|
243 | - $statementParts['unions'] = $values; |
|
244 | - } |
|
245 | - |
|
246 | - return $statementParts; |
|
247 | - } |
|
248 | - |
|
249 | - /** |
|
250 | - * Tell whether there is a recursive MM relation. |
|
251 | - * |
|
252 | - * @return bool |
|
253 | - */ |
|
254 | - public function hasRecursiveMMRelation() |
|
255 | - { |
|
256 | - return isset($this->tableNameAliases['aliasIncrement'][$this->query->getType()]); |
|
257 | - |
|
258 | - } |
|
259 | - |
|
260 | - /** |
|
261 | - * Returns the statement, ready to be executed. |
|
262 | - * |
|
263 | - * @param array $statementParts The SQL statement parts |
|
264 | - * @return string The SQL statement |
|
265 | - */ |
|
266 | - public function buildQuery(array $statementParts) |
|
267 | - { |
|
268 | - |
|
269 | - // Add more statement to the UNION part. |
|
270 | - if (!empty($statementParts['unions'])) { |
|
271 | - foreach ($statementParts['unions'] as $tableName => $unionPart) { |
|
272 | - if (!empty($statementParts['additionalWhereClause'][$tableName])) { |
|
273 | - $statementParts['unions'][$tableName] .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$tableName]); |
|
274 | - } |
|
275 | - } |
|
276 | - } |
|
277 | - |
|
278 | - $statement = 'SELECT ' . implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']) . ' FROM ' . implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']); |
|
279 | - if (!empty($statementParts['where'])) { |
|
280 | - $statement .= ' WHERE ' . implode('', $statementParts['where']); |
|
281 | - if (!empty($statementParts['additionalWhereClause'][$this->query->getType()])) { |
|
282 | - $statement .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]); |
|
283 | - } |
|
284 | - } elseif (!empty($statementParts['additionalWhereClause'])) { |
|
285 | - $statement .= ' WHERE ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]); |
|
286 | - } |
|
287 | - if (!empty($statementParts['orderings'])) { |
|
288 | - $statement .= ' ORDER BY ' . implode(', ', $statementParts['orderings']); |
|
289 | - } |
|
290 | - if (!empty($statementParts['limit'])) { |
|
291 | - $statement .= ' LIMIT ' . $statementParts['limit']; |
|
292 | - } |
|
293 | - |
|
294 | - return $statement; |
|
295 | - } |
|
296 | - |
|
297 | - /** |
|
298 | - * Transforms a Query Source into SQL and parameter arrays |
|
299 | - * |
|
300 | - * @param SourceInterface $source The source |
|
301 | - * @param array &$sql |
|
302 | - * @return void |
|
303 | - */ |
|
304 | - protected function parseSource(SourceInterface $source, array &$sql) |
|
305 | - { |
|
306 | - $tableName = $this->getTableName(); |
|
307 | - $sql['fields'][$tableName] = $tableName . '.*'; |
|
308 | - if ($this->query->getDistinct()) { |
|
309 | - $sql['fields'][$tableName] = $tableName . '.' . $this->query->getDistinct(); |
|
310 | - $sql['keywords']['distinct'] = 'DISTINCT'; |
|
311 | - } |
|
312 | - $sql['tables'][$tableName] = $tableName; |
|
313 | - $sql['mainTable'] = $tableName; |
|
314 | - } |
|
315 | - |
|
316 | - /** |
|
317 | - * Transforms a constraint into SQL and parameter arrays |
|
318 | - * |
|
319 | - * @param ConstraintInterface $constraint The constraint |
|
320 | - * @param SourceInterface $source The source |
|
321 | - * @param array &$statementParts The query parts |
|
322 | - * @param array &$parameters The parameters that will replace the markers |
|
323 | - * @return void |
|
324 | - */ |
|
325 | - protected function parseConstraint(ConstraintInterface $constraint = null, SourceInterface $source, array &$statementParts, array &$parameters) |
|
326 | - { |
|
327 | - if ($constraint instanceof AndInterface) { |
|
328 | - $statementParts['where'][] = '('; |
|
329 | - $this->parseConstraint($constraint->getConstraint1(), $source, $statementParts, $parameters); |
|
330 | - $statementParts['where'][] = ' AND '; |
|
331 | - $this->parseConstraint($constraint->getConstraint2(), $source, $statementParts, $parameters); |
|
332 | - $statementParts['where'][] = ')'; |
|
333 | - } elseif ($constraint instanceof OrInterface) { |
|
334 | - $statementParts['where'][] = '('; |
|
335 | - $this->parseConstraint($constraint->getConstraint1(), $source, $statementParts, $parameters); |
|
336 | - $statementParts['where'][] = ' OR '; |
|
337 | - $this->parseConstraint($constraint->getConstraint2(), $source, $statementParts, $parameters); |
|
338 | - $statementParts['where'][] = ')'; |
|
339 | - } elseif ($constraint instanceof NotInterface) { |
|
340 | - $statementParts['where'][] = 'NOT ('; |
|
341 | - $this->parseConstraint($constraint->getConstraint(), $source, $statementParts, $parameters); |
|
342 | - $statementParts['where'][] = ')'; |
|
343 | - } elseif ($constraint instanceof ComparisonInterface) { |
|
344 | - $this->parseComparison($constraint, $source, $statementParts, $parameters); |
|
345 | - } |
|
346 | - } |
|
347 | - |
|
348 | - /** |
|
349 | - * Parse a Comparison into SQL and parameter arrays. |
|
350 | - * |
|
351 | - * @param ComparisonInterface $comparison The comparison to parse |
|
352 | - * @param SourceInterface $source The source |
|
353 | - * @param array &$statementParts SQL query parts to add to |
|
354 | - * @param array &$parameters Parameters to bind to the SQL |
|
355 | - * @return void |
|
356 | - * @throws Exception\RepositoryException |
|
357 | - */ |
|
358 | - protected function parseComparison(ComparisonInterface $comparison, SourceInterface $source, array &$statementParts, array &$parameters) |
|
359 | - { |
|
360 | - $operand1 = $comparison->getOperand1(); |
|
361 | - $operator = $comparison->getOperator(); |
|
362 | - $operand2 = $comparison->getOperand2(); |
|
363 | - if ($operator === QueryInterface::OPERATOR_IN) { |
|
364 | - $items = []; |
|
365 | - $hasValue = false; |
|
366 | - foreach ($operand2 as $value) { |
|
367 | - $value = $this->getPlainValue($value); |
|
368 | - if ($value !== null) { |
|
369 | - $items[] = $value; |
|
370 | - $hasValue = true; |
|
371 | - } |
|
372 | - } |
|
373 | - if ($hasValue === false) { |
|
374 | - $statementParts['where'][] = '1<>1'; |
|
375 | - } else { |
|
376 | - $this->parseDynamicOperand($operand1, $operator, $source, $statementParts, $parameters, null); |
|
377 | - $parameters[] = $items; |
|
378 | - } |
|
379 | - } elseif ($operator === QueryInterface::OPERATOR_CONTAINS) { |
|
380 | - if ($operand2 === null) { |
|
381 | - $statementParts['where'][] = '1<>1'; |
|
382 | - } else { |
|
383 | - throw new \Exception('Not implemented! Contact extension author.', 1412931227); |
|
384 | - # @todo re-implement me if necessary. |
|
385 | - #$tableName = $this->query->getType(); |
|
386 | - #$propertyName = $operand1->getPropertyName(); |
|
387 | - #while (strpos($propertyName, '.') !== false) { |
|
388 | - # $this->addUnionStatement($tableName, $propertyName, $statementParts); |
|
389 | - #} |
|
390 | - #$columnName = $propertyName; |
|
391 | - #$columnMap = $propertyName; |
|
392 | - #$typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : null; |
|
393 | - #if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) { |
|
394 | - # $relationTableName = $columnMap->getRelationTableName(); |
|
395 | - # $statementParts['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=?)'; |
|
396 | - # $parameters[] = intval($this->getPlainValue($operand2)); |
|
397 | - #} elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) { |
|
398 | - # $parentKeyFieldName = $columnMap->getParentKeyFieldName(); |
|
399 | - # if (isset($parentKeyFieldName)) { |
|
400 | - # $childTableName = $columnMap->getChildTableName(); |
|
401 | - # $statementParts['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=?)'; |
|
402 | - # $parameters[] = intval($this->getPlainValue($operand2)); |
|
403 | - # } else { |
|
404 | - # $statementParts['where'][] = 'FIND_IN_SET(?,' . $tableName . '.' . $columnName . ')'; |
|
405 | - # $parameters[] = intval($this->getPlainValue($operand2)); |
|
406 | - # } |
|
407 | - #} else { |
|
408 | - # throw new Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745); |
|
409 | - #} |
|
410 | - } |
|
411 | - } else { |
|
412 | - if ($operand2 === null) { |
|
413 | - if ($operator === QueryInterface::OPERATOR_EQUAL_TO) { |
|
414 | - $operator = self::OPERATOR_EQUAL_TO_NULL; |
|
415 | - } elseif ($operator === QueryInterface::OPERATOR_NOT_EQUAL_TO) { |
|
416 | - $operator = self::OPERATOR_NOT_EQUAL_TO_NULL; |
|
417 | - } |
|
418 | - } |
|
419 | - $this->parseDynamicOperand($operand1, $operator, $source, $statementParts, $parameters); |
|
420 | - $parameters[] = $this->getPlainValue($operand2); |
|
421 | - } |
|
422 | - } |
|
423 | - |
|
424 | - /** |
|
425 | - * Returns a plain value, i.e. objects are flattened if possible. |
|
426 | - * |
|
427 | - * @param mixed $input |
|
428 | - * @return mixed |
|
429 | - * @throws UnexpectedTypeException |
|
430 | - */ |
|
431 | - protected function getPlainValue($input) |
|
432 | - { |
|
433 | - if (is_array($input)) { |
|
434 | - throw new UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932); |
|
435 | - } |
|
436 | - if ($input instanceof \DateTime) { |
|
437 | - return $input->format('U'); |
|
438 | - } elseif (is_object($input)) { |
|
439 | - if ($input instanceof LazyLoadingProxy) { |
|
440 | - $realInput = $input->_loadRealInstance(); |
|
441 | - } else { |
|
442 | - $realInput = $input; |
|
443 | - } |
|
444 | - if ($realInput instanceof DomainObjectInterface) { |
|
445 | - return $realInput->getUid(); |
|
446 | - } else { |
|
447 | - throw new UnexpectedTypeException('An object of class "' . get_class($realInput) . '" could not be converted to a plain value.', 1274799934); |
|
448 | - } |
|
449 | - } elseif (is_bool($input)) { |
|
450 | - return $input === true ? 1 : 0; |
|
451 | - } else { |
|
452 | - return $input; |
|
453 | - } |
|
454 | - } |
|
455 | - |
|
456 | - /** |
|
457 | - * Parse a DynamicOperand into SQL and parameter arrays. |
|
458 | - * |
|
459 | - * @param DynamicOperandInterface $operand |
|
460 | - * @param string $operator One of the JCR_OPERATOR_* constants |
|
461 | - * @param SourceInterface $source The source |
|
462 | - * @param array &$statementParts The query parts |
|
463 | - * @param array &$parameters The parameters that will replace the markers |
|
464 | - * @param string $valueFunction an optional SQL function to apply to the operand value |
|
465 | - * @return void |
|
466 | - */ |
|
467 | - protected function parseDynamicOperand(DynamicOperandInterface $operand, $operator, SourceInterface $source, array &$statementParts, array &$parameters, $valueFunction = null) |
|
468 | - { |
|
469 | - if ($operand instanceof LowerCaseInterface) { |
|
470 | - $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $statementParts, $parameters, 'LOWER'); |
|
471 | - } elseif ($operand instanceof UpperCaseInterface) { |
|
472 | - $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $statementParts, $parameters, 'UPPER'); |
|
473 | - } elseif ($operand instanceof PropertyValueInterface) { |
|
474 | - $propertyName = $operand->getPropertyName(); |
|
475 | - |
|
476 | - // Reset value. |
|
477 | - $this->currentChildTableNameAlias = ''; |
|
478 | - |
|
479 | - if ($source instanceof SelectorInterface) { |
|
480 | - $tableName = $this->query->getType(); |
|
481 | - while (strpos($propertyName, '.') !== false) { |
|
482 | - $this->addUnionStatement($tableName, $propertyName, $statementParts); |
|
483 | - } |
|
484 | - } elseif ($source instanceof JoinInterface) { |
|
485 | - $tableName = $source->getJoinCondition()->getSelector1Name(); |
|
486 | - } |
|
487 | - |
|
488 | - $columnName = $propertyName; |
|
489 | - $resolvedOperator = $this->resolveOperator($operator); |
|
490 | - $constraintSQL = ''; |
|
491 | - |
|
492 | - $marker = $operator === QueryInterface::OPERATOR_IN |
|
493 | - ? '(?)' |
|
494 | - : '?'; |
|
495 | - |
|
496 | - if ($valueFunction === null) { |
|
497 | - $constraintSQL .= (!empty($tableName) ? $tableName . '.' : '') . $columnName . ' ' . $resolvedOperator . ' ' . $marker; |
|
498 | - } else { |
|
499 | - $constraintSQL .= $valueFunction . '(' . (!empty($tableName) ? $tableName . '.' : '') . $columnName . ') ' . $resolvedOperator . ' ' . $marker; |
|
500 | - } |
|
501 | - |
|
502 | - if (isset($tableName) && !empty($this->currentChildTableNameAlias)) { |
|
503 | - $constraintSQL = $this->replaceTableNameByAlias($tableName, $this->currentChildTableNameAlias, $constraintSQL); |
|
504 | - } |
|
505 | - $statementParts['where'][] = $constraintSQL; |
|
506 | - } |
|
507 | - } |
|
508 | - |
|
509 | - /** |
|
510 | - * @param string &$tableName |
|
511 | - * @param string &$propertyPath |
|
512 | - * @param array &$statementParts |
|
513 | - */ |
|
514 | - protected function addUnionStatement(&$tableName, &$propertyPath, array &$statementParts) |
|
515 | - { |
|
516 | - |
|
517 | - $table = Tca::table($tableName); |
|
518 | - |
|
519 | - $explodedPropertyPath = explode('.', $propertyPath, 2); |
|
520 | - $fieldName = $explodedPropertyPath[0]; |
|
521 | - |
|
522 | - // Field of type "group" are special because property path must contain the table name |
|
523 | - // to determine the relation type. Example for sys_category, property path will look like "items.sys_file" |
|
524 | - $parts = explode('.', $propertyPath, 3); |
|
525 | - if ($table->field($fieldName)->isGroup() && count($parts) > 2) { |
|
526 | - $explodedPropertyPath[0] = $parts[0] . '.' . $parts[1]; |
|
527 | - $explodedPropertyPath[1] = $parts[2]; |
|
528 | - $fieldName = $explodedPropertyPath[0]; |
|
529 | - } |
|
530 | - |
|
531 | - $parentKeyFieldName = $table->field($fieldName)->getForeignField(); |
|
532 | - $childTableName = $table->field($fieldName)->getForeignTable(); |
|
533 | - |
|
534 | - if ($childTableName === null) { |
|
535 | - throw new InvalidRelationConfigurationException('The relation information for property "' . $fieldName . '" of class "' . $tableName . '" is missing.', 1353170925); |
|
536 | - } |
|
537 | - |
|
538 | - if ($table->field($fieldName)->hasOne()) { // includes relation "one-to-one" and "many-to-one" |
|
539 | - // sometimes the opposite relation is not defined. We don't want to force this config for backward compatibility reasons. |
|
540 | - // $parentKeyFieldName === null does the trick somehow. Before condition was if (isset($parentKeyFieldName)) |
|
541 | - if ($table->field($fieldName)->hasRelationManyToOne() || $parentKeyFieldName === null) { |
|
542 | - $statementParts['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $fieldName . '=' . $childTableName . '.uid'; |
|
543 | - } else { |
|
544 | - $statementParts['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName; |
|
545 | - } |
|
546 | - } elseif ($table->field($fieldName)->hasRelationManyToMany()) { |
|
547 | - $relationTableName = $table->field($fieldName)->getManyToManyTable(); |
|
548 | - |
|
549 | - $parentKeyFieldName = $table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local'; |
|
550 | - $childKeyFieldName = !$table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local'; |
|
551 | - |
|
552 | - // MM table e.g sys_category_record_mm |
|
553 | - $relationTableNameAlias = $this->generateAlias($relationTableName); |
|
554 | - $join = sprintf( |
|
555 | - 'LEFT JOIN %s AS %s ON %s.uid=%s.%s', $relationTableName, |
|
556 | - $relationTableNameAlias, |
|
557 | - $tableName, |
|
558 | - $relationTableNameAlias, |
|
559 | - $parentKeyFieldName |
|
560 | - ); |
|
561 | - $statementParts['unions'][$relationTableNameAlias] = $join; |
|
562 | - |
|
563 | - // Foreign table e.g sys_category |
|
564 | - $childTableNameAlias = $this->generateAlias($childTableName); |
|
565 | - $this->currentChildTableNameAlias = $childTableNameAlias; |
|
566 | - $join = sprintf( |
|
567 | - 'LEFT JOIN %s AS %s ON %s.%s=%s.uid', |
|
568 | - $childTableName, |
|
569 | - $childTableNameAlias, |
|
570 | - $relationTableNameAlias, |
|
571 | - $childKeyFieldName, |
|
572 | - $childTableNameAlias |
|
573 | - ); |
|
574 | - $statementParts['unions'][$childTableNameAlias] = $join; |
|
575 | - |
|
576 | - // Find a possible table name for a MM condition. |
|
577 | - $tableNameCondition = $table->field($fieldName)->getAdditionalTableNameCondition(); |
|
578 | - if ($tableNameCondition) { |
|
579 | - |
|
580 | - // If we can find a source file name, we can then retrieve more MM conditions from the TCA such as a field name. |
|
581 | - $sourceFileName = $this->query->getSourceFieldName(); |
|
582 | - if (empty($sourceFileName)) { |
|
583 | - $additionalMMConditions = array( |
|
584 | - 'tablenames' => $tableNameCondition, |
|
585 | - ); |
|
586 | - } else { |
|
587 | - $additionalMMConditions = Tca::table($tableNameCondition)->field($sourceFileName)->getAdditionalMMCondition(); |
|
588 | - } |
|
589 | - |
|
590 | - foreach ($additionalMMConditions as $additionalFieldName => $additionalMMCondition) { |
|
591 | - $additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition); |
|
592 | - $statementParts['unions'][$relationTableNameAlias] .= $additionalJoin; |
|
593 | - |
|
594 | - $additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition); |
|
595 | - $statementParts['unions'][$childTableNameAlias] .= $additionalJoin; |
|
596 | - } |
|
597 | - } |
|
598 | - |
|
599 | - } elseif ($table->field($fieldName)->hasMany()) { // includes relations "many-to-one" and "csv" relations |
|
600 | - $childTableNameAlias = $this->generateAlias($childTableName); |
|
601 | - $this->currentChildTableNameAlias = $childTableNameAlias; |
|
602 | - |
|
603 | - if (isset($parentKeyFieldName)) { |
|
604 | - $join = sprintf( |
|
605 | - 'LEFT JOIN %s AS %s ON %s.uid=%s.%s', |
|
606 | - $childTableName, |
|
607 | - $childTableNameAlias, |
|
608 | - $tableName, |
|
609 | - $childTableNameAlias, |
|
610 | - $parentKeyFieldName |
|
611 | - ); |
|
612 | - $statementParts['unions'][$childTableNameAlias] = $join; |
|
613 | - } else { |
|
614 | - $join = sprintf( |
|
615 | - 'LEFT JOIN %s AS %s ON (FIND_IN_SET(%s.uid, %s.%s))', |
|
616 | - $childTableName, |
|
617 | - $childTableNameAlias, |
|
618 | - $childTableNameAlias, |
|
619 | - $tableName, |
|
620 | - $fieldName |
|
621 | - ); |
|
622 | - $statementParts['unions'][$childTableNameAlias] = $join; |
|
623 | - } |
|
624 | - } else { |
|
625 | - throw new Exception('Could not determine type of relation.', 1252502725); |
|
626 | - } |
|
627 | - |
|
628 | - $statementParts['keywords']['distinct'] = 'DISTINCT'; |
|
629 | - $propertyPath = $explodedPropertyPath[1]; |
|
630 | - $tableName = $childTableName; |
|
631 | - } |
|
632 | - |
|
633 | - /** |
|
634 | - * Returns the SQL operator for the given JCR operator type. |
|
635 | - * |
|
636 | - * @param string $operator One of the JCR_OPERATOR_* constants |
|
637 | - * @return string an SQL operator |
|
638 | - * @throws Exception |
|
639 | - */ |
|
640 | - protected function resolveOperator($operator) |
|
641 | - { |
|
642 | - switch ($operator) { |
|
643 | - case self::OPERATOR_EQUAL_TO_NULL: |
|
644 | - $operator = 'IS'; |
|
645 | - break; |
|
646 | - case self::OPERATOR_NOT_EQUAL_TO_NULL: |
|
647 | - $operator = 'IS NOT'; |
|
648 | - break; |
|
649 | - case QueryInterface::OPERATOR_IN: |
|
650 | - $operator = 'IN'; |
|
651 | - break; |
|
652 | - case QueryInterface::OPERATOR_EQUAL_TO: |
|
653 | - $operator = '='; |
|
654 | - break; |
|
655 | - case QueryInterface::OPERATOR_NOT_EQUAL_TO: |
|
656 | - $operator = '!='; |
|
657 | - break; |
|
658 | - case QueryInterface::OPERATOR_LESS_THAN: |
|
659 | - $operator = '<'; |
|
660 | - break; |
|
661 | - case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO: |
|
662 | - $operator = '<='; |
|
663 | - break; |
|
664 | - case QueryInterface::OPERATOR_GREATER_THAN: |
|
665 | - $operator = '>'; |
|
666 | - break; |
|
667 | - case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO: |
|
668 | - $operator = '>='; |
|
669 | - break; |
|
670 | - case QueryInterface::OPERATOR_LIKE: |
|
671 | - $operator = 'LIKE'; |
|
672 | - break; |
|
673 | - default: |
|
674 | - throw new Exception('Unsupported operator encountered.', 1242816073); |
|
675 | - } |
|
676 | - return $operator; |
|
677 | - } |
|
678 | - |
|
679 | - /** |
|
680 | - * Adds additional WHERE statements according to the query settings. |
|
681 | - * |
|
682 | - * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings |
|
683 | - * @param string $tableNameOrAlias The table name to add the additional where clause for |
|
684 | - * @param array &$statementParts |
|
685 | - * @return void |
|
686 | - */ |
|
687 | - protected function addAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableNameOrAlias, &$statementParts) |
|
688 | - { |
|
689 | - $this->addVisibilityConstraintStatement($querySettings, $tableNameOrAlias, $statementParts); |
|
690 | - if ($querySettings->getRespectSysLanguage()) { |
|
691 | - $this->addSysLanguageStatement($tableNameOrAlias, $statementParts, $querySettings); |
|
692 | - } |
|
693 | - } |
|
694 | - |
|
695 | - /** |
|
696 | - * Adds enableFields and deletedClause to the query if necessary |
|
697 | - * |
|
698 | - * @param QuerySettingsInterface $querySettings |
|
699 | - * @param string $tableNameOrAlias The database table name |
|
700 | - * @param array &$statementParts The query parts |
|
701 | - * @return void |
|
702 | - */ |
|
703 | - protected function addVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableNameOrAlias, array &$statementParts) |
|
704 | - { |
|
705 | - $statement = ''; |
|
706 | - $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
707 | - if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) { |
|
708 | - $ignoreEnableFields = $querySettings->getIgnoreEnableFields(); |
|
709 | - $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored(); |
|
710 | - $includeDeleted = $querySettings->getIncludeDeleted(); |
|
711 | - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) { |
|
712 | - $statement .= $this->getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted); |
|
713 | - } else { |
|
714 | - // 'BE' case |
|
715 | - $statement .= $this->getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted); |
|
716 | - } |
|
717 | - |
|
718 | - // Remove the prefixing "AND" if any. |
|
719 | - if (!empty($statement)) { |
|
720 | - $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement; |
|
721 | - $statementParts['additionalWhereClause'][$tableNameOrAlias][] = $statement; |
|
722 | - } |
|
723 | - } |
|
724 | - } |
|
725 | - |
|
726 | - /** |
|
727 | - * Returns constraint statement for frontend context |
|
728 | - * |
|
729 | - * @param string $tableNameOrAlias |
|
730 | - * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored |
|
731 | - * @param array $enableFieldsToBeIgnored If $ignoreEnableFields is true, this array specifies enable fields to be ignored. If it is null or an empty array (default) all enable fields are ignored. |
|
732 | - * @param boolean $includeDeleted A flag indicating whether deleted records should be included |
|
733 | - * @return string |
|
734 | - * @throws Exception\InconsistentQuerySettingsException |
|
735 | - */ |
|
736 | - protected function getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted) |
|
737 | - { |
|
738 | - $statement = ''; |
|
739 | - $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
740 | - if ($ignoreEnableFields && !$includeDeleted) { |
|
741 | - if (count($enableFieldsToBeIgnored)) { |
|
742 | - // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented |
|
743 | - $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored)); |
|
744 | - } else { |
|
745 | - $statement .= $this->getPageRepository()->deleteClause($tableName); |
|
746 | - } |
|
747 | - } elseif (!$ignoreEnableFields && !$includeDeleted) { |
|
748 | - $statement .= $this->getPageRepository()->enableFields($tableName); |
|
749 | - } elseif (!$ignoreEnableFields && $includeDeleted) { |
|
750 | - throw new InconsistentQuerySettingsException('Query setting "ignoreEnableFields=false" can not be used together with "includeDeleted=true" in frontend context.', 1327678173); |
|
751 | - } |
|
752 | - return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement); |
|
753 | - } |
|
754 | - |
|
755 | - /** |
|
756 | - * Returns constraint statement for backend context |
|
757 | - * |
|
758 | - * @param string $tableNameOrAlias |
|
759 | - * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored |
|
760 | - * @param boolean $includeDeleted A flag indicating whether deleted records should be included |
|
761 | - * @return string |
|
762 | - */ |
|
763 | - protected function getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted) |
|
764 | - { |
|
765 | - $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
766 | - $statement = ''; |
|
767 | - if (!$ignoreEnableFields) { |
|
768 | - $statement .= BackendUtility::BEenableFields($tableName); |
|
769 | - } |
|
770 | - |
|
771 | - // If the table is found to have "workspace" support, add the corresponding fields in the statement. |
|
772 | - if (Tca::table($tableName)->hasWorkspaceSupport()) { |
|
773 | - if ($this->getBackendUser()->workspace === 0) { |
|
774 | - $statement .= ' AND ' . $tableName . '.t3ver_state<=' . new VersionState(VersionState::DEFAULT_STATE); |
|
775 | - } else { |
|
776 | - // Show only records of live and of the current workspace |
|
777 | - // In case we are in a Versioning preview |
|
778 | - $statement .= ' AND (' . |
|
779 | - $tableName . '.t3ver_wsid=0 OR ' . |
|
780 | - $tableName . '.t3ver_wsid=' . (int)$this->getBackendUser()->workspace . |
|
781 | - ')'; |
|
782 | - } |
|
783 | - |
|
784 | - // Check if this segment make sense here or whether it should be in the "if" part when we have workspace = 0 |
|
785 | - $statement .= ' AND ' . $tableName . '.pid<>-1'; |
|
786 | - } |
|
787 | - |
|
788 | - if (!$includeDeleted) { |
|
789 | - $statement .= BackendUtility::deleteClause($tableName); |
|
790 | - } |
|
791 | - |
|
792 | - return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement); |
|
793 | - } |
|
794 | - |
|
795 | - /** |
|
796 | - * Builds the language field statement |
|
797 | - * |
|
798 | - * @param string $tableNameOrAlias The database table name |
|
799 | - * @param array &$statementParts The query parts |
|
800 | - * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings |
|
801 | - * @return void |
|
802 | - * @throws Exception |
|
803 | - */ |
|
804 | - protected function addSysLanguageStatement($tableNameOrAlias, array &$statementParts, $querySettings) |
|
805 | - { |
|
806 | - $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
807 | - if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) { |
|
808 | - if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) { |
|
809 | - // Select all entries for the current language |
|
810 | - $additionalWhereClause = $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . intval($querySettings->getLanguageUid()) . ',-1)'; |
|
811 | - // If any language is set -> get those entries which are not translated yet |
|
812 | - // They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode |
|
813 | - if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) |
|
814 | - && $querySettings->getLanguageUid() > 0 |
|
815 | - ) { |
|
816 | - $additionalWhereClause .= ' OR (' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' . |
|
817 | - ' AND ' . $tableNameOrAlias . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . |
|
818 | - ' FROM ' . $tableName . |
|
819 | - ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' . |
|
820 | - ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0'; |
|
821 | - |
|
822 | - // Add delete clause to ensure all entries are loaded |
|
823 | - if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) { |
|
824 | - $additionalWhereClause .= ' AND ' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0'; |
|
825 | - } |
|
826 | - $additionalWhereClause .= '))'; |
|
827 | - } |
|
828 | - $statementParts['additionalWhereClause'][$tableNameOrAlias][] = '(' . $additionalWhereClause . ')'; |
|
829 | - } |
|
830 | - } |
|
831 | - } |
|
832 | - |
|
833 | - /** |
|
834 | - * Transforms orderings into SQL. |
|
835 | - * |
|
836 | - * @param array $orderings An array of orderings (Tx_Extbase_Persistence_QOM_Ordering) |
|
837 | - * @param SourceInterface $source The source |
|
838 | - * @param array &$statementParts The query parts |
|
839 | - * @return void |
|
840 | - * @throws Exception\UnsupportedOrderException |
|
841 | - */ |
|
842 | - protected function parseOrderings(array $orderings, SourceInterface $source, array &$statementParts) |
|
843 | - { |
|
844 | - foreach ($orderings as $fieldNameAndPath => $order) { |
|
845 | - switch ($order) { |
|
846 | - case QueryInterface::ORDER_ASCENDING: |
|
847 | - $order = 'ASC'; |
|
848 | - break; |
|
849 | - case QueryInterface::ORDER_DESCENDING: |
|
850 | - $order = 'DESC'; |
|
851 | - break; |
|
852 | - default: |
|
853 | - throw new UnsupportedOrderException('Unsupported order encountered.', 1456845126); |
|
854 | - } |
|
855 | - |
|
856 | - $tableName = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->query->getType()); |
|
857 | - $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $tableName); |
|
858 | - $statementParts['orderings'][] = sprintf('%s.%s %s', $tableName, $fieldName, $order); |
|
859 | - } |
|
860 | - } |
|
861 | - |
|
862 | - /** |
|
863 | - * Transforms limit and offset into SQL |
|
864 | - * |
|
865 | - * @param int $limit |
|
866 | - * @param int $offset |
|
867 | - * @param array &$statementParts |
|
868 | - * @return void |
|
869 | - */ |
|
870 | - protected function parseLimitAndOffset($limit, $offset, array &$statementParts) |
|
871 | - { |
|
872 | - if ($limit !== null && $offset !== null) { |
|
873 | - $statementParts['limit'] = intval($offset) . ', ' . intval($limit); |
|
874 | - } elseif ($limit !== null) { |
|
875 | - $statementParts['limit'] = intval($limit); |
|
876 | - } |
|
877 | - } |
|
878 | - |
|
879 | - /** |
|
880 | - * @param array $rows |
|
881 | - * @return array |
|
882 | - */ |
|
883 | - protected function getContentObjects(array $rows): array |
|
884 | - { |
|
885 | - $contentObjects = []; |
|
886 | - foreach ($rows as $row) { |
|
887 | - |
|
888 | - // Get language uid from querySettings. |
|
889 | - // Ensure the backend handling is not broken (fallback to Get parameter 'L' if needed) |
|
890 | - $overlaidRow = $this->doLanguageAndWorkspaceOverlay( |
|
891 | - $row, |
|
892 | - $this->query->getTypo3QuerySettings() |
|
893 | - ); |
|
894 | - |
|
895 | - $contentObjects[] = GeneralUtility::makeInstance( |
|
896 | - Content::class, |
|
897 | - $this->query->getType(), |
|
898 | - $overlaidRow |
|
899 | - ); |
|
900 | - } |
|
901 | - |
|
902 | - return $contentObjects; |
|
903 | - } |
|
904 | - |
|
905 | - /** |
|
906 | - * Performs workspace and language overlay on the given row array. The language and workspace id is automatically |
|
907 | - * detected (depending on FE or BE context). You can also explicitly set the language/workspace id. |
|
908 | - * |
|
909 | - * @param array $row |
|
910 | - * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings |
|
911 | - * @return array |
|
912 | - */ |
|
913 | - protected function doLanguageAndWorkspaceOverlay(array $row, $querySettings) |
|
914 | - { |
|
915 | - $tableName = $this->getTableName(); |
|
916 | - |
|
917 | - $pageRepository = $this->getPageRepository(); |
|
918 | - if (is_object($GLOBALS['TSFE'])) { |
|
919 | - $languageMode = $GLOBALS['TSFE']->sys_language_mode; |
|
920 | - #if ($this->isBackendUserLogged() && $this->getBackendUser()->workspace !== 0) { |
|
921 | - # $pageRepository->versioningWorkspaceId = $this->getBackendUser()->workspace; |
|
922 | - #} |
|
923 | - } else { |
|
924 | - $languageMode = ''; |
|
925 | - $workspaceUid = $this->getBackendUser()->workspace; |
|
926 | - #$pageRepository->versioningWorkspaceId = $workspaceUid; |
|
927 | - #if ($this->getBackendUser()->workspace !== 0) { |
|
928 | - # $pageRepository->versioningPreview = 1; |
|
929 | - #} |
|
930 | - } |
|
931 | - |
|
932 | - // If current row is a translation select its parent |
|
933 | - if (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) |
|
934 | - && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) |
|
935 | - ) { |
|
936 | - if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]) |
|
937 | - && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0 |
|
938 | - ) { |
|
939 | - $queryBuilder = $this->getQueryBuilder(); |
|
940 | - $row = $queryBuilder |
|
941 | - ->select($tableName . '.*') |
|
942 | - ->from($tableName) |
|
943 | - ->andWhere( |
|
944 | - $tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']], |
|
945 | - $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' = 0' |
|
946 | - ) |
|
947 | - ->execute() |
|
948 | - ->fetch(); |
|
949 | - } |
|
950 | - } |
|
951 | - |
|
952 | - // Retrieve the original uid; Used for Workspaces! |
|
953 | - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) { |
|
954 | - $pageRepository->versionOL($tableName, $row, true, true); |
|
955 | - } else { |
|
956 | - \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL($tableName, $row); |
|
957 | - } |
|
958 | - if ($pageRepository->versioningPreview && isset($row['_ORIG_uid'])) { |
|
959 | - $row['uid'] = $row['_ORIG_uid']; |
|
960 | - } |
|
961 | - |
|
962 | - // Special case for table "pages" |
|
963 | - if ($tableName == 'pages') { |
|
964 | - $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid()); |
|
965 | - } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) |
|
966 | - && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== '' |
|
967 | - ) { |
|
968 | - if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) { |
|
969 | - $overlayMode = $languageMode === 'strict' ? 'hideNonTranslated' : ''; |
|
970 | - $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode); |
|
971 | - } |
|
972 | - } |
|
973 | - |
|
974 | - return $row; |
|
975 | - } |
|
976 | - |
|
977 | - /** |
|
978 | - * Return a resolved table name given a possible table name alias. |
|
979 | - * |
|
980 | - * @param string $tableNameOrAlias |
|
981 | - * @return string |
|
982 | - */ |
|
983 | - protected function resolveTableNameAlias($tableNameOrAlias) |
|
984 | - { |
|
985 | - $resolvedTableName = $tableNameOrAlias; |
|
986 | - if (!empty($this->tableNameAliases['aliases'][$tableNameOrAlias])) { |
|
987 | - $resolvedTableName = $this->tableNameAliases['aliases'][$tableNameOrAlias]; |
|
988 | - } |
|
989 | - return $resolvedTableName; |
|
990 | - } |
|
991 | - |
|
992 | - /** |
|
993 | - * Generate a unique table name alias for the given table name. |
|
994 | - * |
|
995 | - * @param string $tableName |
|
996 | - * @return string |
|
997 | - */ |
|
998 | - protected function generateAlias($tableName) |
|
999 | - { |
|
1000 | - |
|
1001 | - if (!isset($this->tableNameAliases['aliasIncrement'][$tableName])) { |
|
1002 | - $this->tableNameAliases['aliasIncrement'][$tableName] = 0; |
|
1003 | - } |
|
1004 | - |
|
1005 | - $numberOfAliases = $this->tableNameAliases['aliasIncrement'][$tableName]; |
|
1006 | - $tableNameAlias = $tableName . $numberOfAliases; |
|
1007 | - |
|
1008 | - $this->tableNameAliases['aliasIncrement'][$tableName]++; |
|
1009 | - $this->tableNameAliases['aliases'][$tableNameAlias] = $tableName; |
|
1010 | - |
|
1011 | - return $tableNameAlias; |
|
1012 | - } |
|
1013 | - |
|
1014 | - /** |
|
1015 | - * Replace the table names by its table name alias within the given statement. |
|
1016 | - * |
|
1017 | - * @param string $tableName |
|
1018 | - * @param string $tableNameAlias |
|
1019 | - * @param string $statement |
|
1020 | - * @return string |
|
1021 | - */ |
|
1022 | - protected function replaceTableNameByAlias($tableName, $tableNameAlias, $statement) |
|
1023 | - { |
|
1024 | - if ($statement && $tableName !== $tableNameAlias) { |
|
1025 | - $statement = str_replace($tableName, $tableNameAlias, $statement); |
|
1026 | - } |
|
1027 | - return $statement; |
|
1028 | - } |
|
1029 | - |
|
1030 | - /** |
|
1031 | - * Returns an instance of the current Backend User. |
|
1032 | - * |
|
1033 | - * @return BackendUserAuthentication |
|
1034 | - */ |
|
1035 | - protected function getBackendUser() |
|
1036 | - { |
|
1037 | - return $GLOBALS['BE_USER']; |
|
1038 | - } |
|
1039 | - |
|
1040 | - /** |
|
1041 | - * Tell whether a Backend User is logged in. |
|
1042 | - * |
|
1043 | - * @return bool |
|
1044 | - */ |
|
1045 | - protected function isBackendUserLogged() |
|
1046 | - { |
|
1047 | - return is_object($GLOBALS['BE_USER']); |
|
1048 | - } |
|
1049 | - |
|
1050 | - /** |
|
1051 | - * @return PageRepository|object |
|
1052 | - */ |
|
1053 | - protected function getPageRepository() |
|
1054 | - { |
|
1055 | - if (!$this->pageRepository instanceof PageRepository) { |
|
1056 | - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() && is_object($GLOBALS['TSFE'])) { |
|
1057 | - $this->pageRepository = $GLOBALS['TSFE']->sys_page; |
|
1058 | - } else { |
|
1059 | - $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class); |
|
1060 | - } |
|
1061 | - } |
|
1062 | - |
|
1063 | - return $this->pageRepository; |
|
1064 | - } |
|
1065 | - |
|
1066 | - /** |
|
1067 | - * @return FieldPathResolver|object |
|
1068 | - */ |
|
1069 | - protected function getFieldPathResolver() |
|
1070 | - { |
|
1071 | - return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
1072 | - } |
|
1073 | - |
|
1074 | - /** |
|
1075 | - * @return object|Connection |
|
1076 | - */ |
|
1077 | - protected function getConnection(): Connection |
|
1078 | - { |
|
1079 | - /** @var ConnectionPool $connectionPool */ |
|
1080 | - return GeneralUtility::makeInstance(ConnectionPool::class) |
|
1081 | - ->getConnectionForTable($this->getTableName()); |
|
1082 | - } |
|
1083 | - |
|
1084 | - /** |
|
1085 | - * @return object|QueryBuilder |
|
1086 | - */ |
|
1087 | - protected function getQueryBuilder(): QueryBuilder |
|
1088 | - { |
|
1089 | - /** @var ConnectionPool $connectionPool */ |
|
1090 | - $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); |
|
1091 | - return $connectionPool->getQueryBuilderForTable($this->getTableName()); |
|
1092 | - } |
|
1093 | - |
|
1094 | - /** |
|
1095 | - * @return string |
|
1096 | - */ |
|
1097 | - public function getTableName(): string |
|
1098 | - { |
|
1099 | - return $this->query->getSource()->getNodeTypeName(); // getSelectorName() |
|
1100 | - } |
|
54 | + const OPERATOR_EQUAL_TO_NULL = 'operatorEqualToNull'; |
|
55 | + const OPERATOR_NOT_EQUAL_TO_NULL = 'operatorNotEqualToNull'; |
|
56 | + |
|
57 | + /** |
|
58 | + * The TYPO3 page repository. Used for language and workspace overlay |
|
59 | + * |
|
60 | + * @var PageRepository |
|
61 | + */ |
|
62 | + protected $pageRepository; |
|
63 | + |
|
64 | + /** |
|
65 | + * @var Query |
|
66 | + */ |
|
67 | + protected $query; |
|
68 | + |
|
69 | + /** |
|
70 | + * Store some info related to table name and its aliases. |
|
71 | + * |
|
72 | + * @var array |
|
73 | + */ |
|
74 | + protected $tableNameAliases = array( |
|
75 | + 'aliases' => [], |
|
76 | + 'aliasIncrement' => [], |
|
77 | + ); |
|
78 | + |
|
79 | + /** |
|
80 | + * Use to store the current foreign table name alias. |
|
81 | + * |
|
82 | + * @var string |
|
83 | + */ |
|
84 | + protected $currentChildTableNameAlias = ''; |
|
85 | + |
|
86 | + /** |
|
87 | + * @param Query $query |
|
88 | + */ |
|
89 | + public function __construct(Query $query) |
|
90 | + { |
|
91 | + $this->query = $query; |
|
92 | + } |
|
93 | + |
|
94 | + /** |
|
95 | + * @param $parameters |
|
96 | + * @return array |
|
97 | + */ |
|
98 | + protected static function getTypes($parameters) |
|
99 | + { |
|
100 | + $types = []; |
|
101 | + foreach ($parameters as $parameter) { |
|
102 | + if (is_array($parameter)) { |
|
103 | + if (MathUtility::canBeInterpretedAsInteger($parameter[0])) { |
|
104 | + $types[] = \Doctrine\DBAL\Connection::PARAM_INT_ARRAY; |
|
105 | + } else { |
|
106 | + $types[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; |
|
107 | + } |
|
108 | + } else { |
|
109 | + if (MathUtility::canBeInterpretedAsInteger($parameter)) { |
|
110 | + $types[] = ParameterType::INTEGER; |
|
111 | + } else { |
|
112 | + $types[] = ParameterType::STRING; |
|
113 | + } |
|
114 | + } |
|
115 | + } |
|
116 | + return $types; |
|
117 | + } |
|
118 | + |
|
119 | + /** |
|
120 | + * Returns the result of the query |
|
121 | + */ |
|
122 | + public function fetchResult() |
|
123 | + { |
|
124 | + $parameters = []; |
|
125 | + $statementParts = $this->parseQuery($parameters); |
|
126 | + $statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); |
|
127 | + $sql = $this->buildQuery($statementParts); |
|
128 | + //print $sql; exit(); |
|
129 | + |
|
130 | + $rows = $this->getConnection() |
|
131 | + ->executeQuery($sql, $parameters, self::getTypes($parameters)) |
|
132 | + ->fetchAll(); |
|
133 | + |
|
134 | + return $this->getContentObjects($rows); |
|
135 | + } |
|
136 | + |
|
137 | + /** |
|
138 | + * Returns the number of tuples matching the query. |
|
139 | + * |
|
140 | + * @return int The number of matching tuples |
|
141 | + */ |
|
142 | + public function countResult() |
|
143 | + { |
|
144 | + $parameters = []; |
|
145 | + $statementParts = $this->parseQuery($parameters); |
|
146 | + $statementParts = $this->processStatementStructureForRecursiveMMRelation($statementParts); |
|
147 | + $types = self::getTypes($parameters); |
|
148 | + |
|
149 | + // if limit is set, we need to count the rows "manually" as COUNT(*) ignores LIMIT constraints |
|
150 | + if (!empty($statementParts['limit'])) { |
|
151 | + $sql = $this->buildQuery($statementParts); |
|
152 | + |
|
153 | + $count = $this |
|
154 | + ->getConnection() |
|
155 | + ->executeQuery($sql, $parameters, $types) |
|
156 | + ->rowCount(); |
|
157 | + } else { |
|
158 | + $statementParts['fields'] = array('COUNT(*)'); |
|
159 | + // having orderings without grouping is not compatible with non-MySQL DBMS |
|
160 | + $statementParts['orderings'] = []; |
|
161 | + if (isset($statementParts['keywords']['distinct'])) { |
|
162 | + unset($statementParts['keywords']['distinct']); |
|
163 | + $distinctField = $this->query->getDistinct() ? $this->query->getDistinct() : 'uid'; |
|
164 | + $statementParts['fields'] = array('COUNT(DISTINCT ' . $statementParts['mainTable'] . '.' . $distinctField . ')'); |
|
165 | + } |
|
166 | + |
|
167 | + $sql = $this->buildQuery($statementParts); |
|
168 | + $count = $this |
|
169 | + ->getConnection() |
|
170 | + ->executeQuery($sql, $parameters, $types) |
|
171 | + ->fetchColumn(0); |
|
172 | + } |
|
173 | + return (int)$count; |
|
174 | + } |
|
175 | + |
|
176 | + /** |
|
177 | + * Parses the query and returns the SQL statement parts. |
|
178 | + * |
|
179 | + * @param array &$parameters |
|
180 | + * @return array |
|
181 | + */ |
|
182 | + public function parseQuery(array &$parameters) |
|
183 | + { |
|
184 | + $statementParts = []; |
|
185 | + $statementParts['keywords'] = []; |
|
186 | + $statementParts['tables'] = []; |
|
187 | + $statementParts['unions'] = []; |
|
188 | + $statementParts['fields'] = []; |
|
189 | + $statementParts['where'] = []; |
|
190 | + $statementParts['additionalWhereClause'] = []; |
|
191 | + $statementParts['orderings'] = []; |
|
192 | + $statementParts['limit'] = []; |
|
193 | + $query = $this->query; |
|
194 | + $source = $query->getSource(); |
|
195 | + $this->parseSource($source, $statementParts); |
|
196 | + $this->parseConstraint($query->getConstraint(), $source, $statementParts, $parameters); |
|
197 | + $this->parseOrderings($query->getOrderings(), $source, $statementParts); |
|
198 | + $this->parseLimitAndOffset($query->getLimit(), $query->getOffset(), $statementParts); |
|
199 | + $tableNames = array_unique(array_keys($statementParts['tables'] + $statementParts['unions'])); |
|
200 | + foreach ($tableNames as $tableNameOrAlias) { |
|
201 | + if (is_string($tableNameOrAlias) && strlen($tableNameOrAlias) > 0) { |
|
202 | + $this->addAdditionalWhereClause($query->getTypo3QuerySettings(), $tableNameOrAlias, $statementParts); |
|
203 | + } |
|
204 | + } |
|
205 | + |
|
206 | + return $statementParts; |
|
207 | + } |
|
208 | + |
|
209 | + /** |
|
210 | + * Fiddle with the statement structure to handle recursive MM relations. |
|
211 | + * For the recursive MM query to work, we must invert some values. |
|
212 | + * Let see if that is the best way of doing that... |
|
213 | + * |
|
214 | + * @param array $statementParts |
|
215 | + * @return array |
|
216 | + */ |
|
217 | + public function processStatementStructureForRecursiveMMRelation(array $statementParts) |
|
218 | + { |
|
219 | + |
|
220 | + if ($this->hasRecursiveMMRelation()) { |
|
221 | + $tableName = $this->query->getType(); |
|
222 | + |
|
223 | + // In order the MM query to work for a recursive MM query, we must invert some values. |
|
224 | + // tx_domain_model_foo0 (the alias) <--> tx_domain_model_foo (the origin table name) |
|
225 | + $values = []; |
|
226 | + foreach ($statementParts['fields'] as $key => $value) { |
|
227 | + $values[$key] = str_replace($tableName, $tableName . '0', $value); |
|
228 | + } |
|
229 | + $statementParts['fields'] = $values; |
|
230 | + |
|
231 | + // Same comment as above. |
|
232 | + $values = []; |
|
233 | + foreach ($statementParts['where'] as $key => $value) { |
|
234 | + $values[$key] = str_replace($tableName . '0', $tableName, $value); |
|
235 | + } |
|
236 | + $statementParts['where'] = $values; |
|
237 | + |
|
238 | + // We must be more restrictive by transforming the "left" union by "inner" |
|
239 | + $values = []; |
|
240 | + foreach ($statementParts['unions'] as $key => $value) { |
|
241 | + $values[$key] = str_replace('LEFT JOIN', 'INNER JOIN', $value); |
|
242 | + } |
|
243 | + $statementParts['unions'] = $values; |
|
244 | + } |
|
245 | + |
|
246 | + return $statementParts; |
|
247 | + } |
|
248 | + |
|
249 | + /** |
|
250 | + * Tell whether there is a recursive MM relation. |
|
251 | + * |
|
252 | + * @return bool |
|
253 | + */ |
|
254 | + public function hasRecursiveMMRelation() |
|
255 | + { |
|
256 | + return isset($this->tableNameAliases['aliasIncrement'][$this->query->getType()]); |
|
257 | + |
|
258 | + } |
|
259 | + |
|
260 | + /** |
|
261 | + * Returns the statement, ready to be executed. |
|
262 | + * |
|
263 | + * @param array $statementParts The SQL statement parts |
|
264 | + * @return string The SQL statement |
|
265 | + */ |
|
266 | + public function buildQuery(array $statementParts) |
|
267 | + { |
|
268 | + |
|
269 | + // Add more statement to the UNION part. |
|
270 | + if (!empty($statementParts['unions'])) { |
|
271 | + foreach ($statementParts['unions'] as $tableName => $unionPart) { |
|
272 | + if (!empty($statementParts['additionalWhereClause'][$tableName])) { |
|
273 | + $statementParts['unions'][$tableName] .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$tableName]); |
|
274 | + } |
|
275 | + } |
|
276 | + } |
|
277 | + |
|
278 | + $statement = 'SELECT ' . implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']) . ' FROM ' . implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']); |
|
279 | + if (!empty($statementParts['where'])) { |
|
280 | + $statement .= ' WHERE ' . implode('', $statementParts['where']); |
|
281 | + if (!empty($statementParts['additionalWhereClause'][$this->query->getType()])) { |
|
282 | + $statement .= ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]); |
|
283 | + } |
|
284 | + } elseif (!empty($statementParts['additionalWhereClause'])) { |
|
285 | + $statement .= ' WHERE ' . implode(' AND ', $statementParts['additionalWhereClause'][$this->query->getType()]); |
|
286 | + } |
|
287 | + if (!empty($statementParts['orderings'])) { |
|
288 | + $statement .= ' ORDER BY ' . implode(', ', $statementParts['orderings']); |
|
289 | + } |
|
290 | + if (!empty($statementParts['limit'])) { |
|
291 | + $statement .= ' LIMIT ' . $statementParts['limit']; |
|
292 | + } |
|
293 | + |
|
294 | + return $statement; |
|
295 | + } |
|
296 | + |
|
297 | + /** |
|
298 | + * Transforms a Query Source into SQL and parameter arrays |
|
299 | + * |
|
300 | + * @param SourceInterface $source The source |
|
301 | + * @param array &$sql |
|
302 | + * @return void |
|
303 | + */ |
|
304 | + protected function parseSource(SourceInterface $source, array &$sql) |
|
305 | + { |
|
306 | + $tableName = $this->getTableName(); |
|
307 | + $sql['fields'][$tableName] = $tableName . '.*'; |
|
308 | + if ($this->query->getDistinct()) { |
|
309 | + $sql['fields'][$tableName] = $tableName . '.' . $this->query->getDistinct(); |
|
310 | + $sql['keywords']['distinct'] = 'DISTINCT'; |
|
311 | + } |
|
312 | + $sql['tables'][$tableName] = $tableName; |
|
313 | + $sql['mainTable'] = $tableName; |
|
314 | + } |
|
315 | + |
|
316 | + /** |
|
317 | + * Transforms a constraint into SQL and parameter arrays |
|
318 | + * |
|
319 | + * @param ConstraintInterface $constraint The constraint |
|
320 | + * @param SourceInterface $source The source |
|
321 | + * @param array &$statementParts The query parts |
|
322 | + * @param array &$parameters The parameters that will replace the markers |
|
323 | + * @return void |
|
324 | + */ |
|
325 | + protected function parseConstraint(ConstraintInterface $constraint = null, SourceInterface $source, array &$statementParts, array &$parameters) |
|
326 | + { |
|
327 | + if ($constraint instanceof AndInterface) { |
|
328 | + $statementParts['where'][] = '('; |
|
329 | + $this->parseConstraint($constraint->getConstraint1(), $source, $statementParts, $parameters); |
|
330 | + $statementParts['where'][] = ' AND '; |
|
331 | + $this->parseConstraint($constraint->getConstraint2(), $source, $statementParts, $parameters); |
|
332 | + $statementParts['where'][] = ')'; |
|
333 | + } elseif ($constraint instanceof OrInterface) { |
|
334 | + $statementParts['where'][] = '('; |
|
335 | + $this->parseConstraint($constraint->getConstraint1(), $source, $statementParts, $parameters); |
|
336 | + $statementParts['where'][] = ' OR '; |
|
337 | + $this->parseConstraint($constraint->getConstraint2(), $source, $statementParts, $parameters); |
|
338 | + $statementParts['where'][] = ')'; |
|
339 | + } elseif ($constraint instanceof NotInterface) { |
|
340 | + $statementParts['where'][] = 'NOT ('; |
|
341 | + $this->parseConstraint($constraint->getConstraint(), $source, $statementParts, $parameters); |
|
342 | + $statementParts['where'][] = ')'; |
|
343 | + } elseif ($constraint instanceof ComparisonInterface) { |
|
344 | + $this->parseComparison($constraint, $source, $statementParts, $parameters); |
|
345 | + } |
|
346 | + } |
|
347 | + |
|
348 | + /** |
|
349 | + * Parse a Comparison into SQL and parameter arrays. |
|
350 | + * |
|
351 | + * @param ComparisonInterface $comparison The comparison to parse |
|
352 | + * @param SourceInterface $source The source |
|
353 | + * @param array &$statementParts SQL query parts to add to |
|
354 | + * @param array &$parameters Parameters to bind to the SQL |
|
355 | + * @return void |
|
356 | + * @throws Exception\RepositoryException |
|
357 | + */ |
|
358 | + protected function parseComparison(ComparisonInterface $comparison, SourceInterface $source, array &$statementParts, array &$parameters) |
|
359 | + { |
|
360 | + $operand1 = $comparison->getOperand1(); |
|
361 | + $operator = $comparison->getOperator(); |
|
362 | + $operand2 = $comparison->getOperand2(); |
|
363 | + if ($operator === QueryInterface::OPERATOR_IN) { |
|
364 | + $items = []; |
|
365 | + $hasValue = false; |
|
366 | + foreach ($operand2 as $value) { |
|
367 | + $value = $this->getPlainValue($value); |
|
368 | + if ($value !== null) { |
|
369 | + $items[] = $value; |
|
370 | + $hasValue = true; |
|
371 | + } |
|
372 | + } |
|
373 | + if ($hasValue === false) { |
|
374 | + $statementParts['where'][] = '1<>1'; |
|
375 | + } else { |
|
376 | + $this->parseDynamicOperand($operand1, $operator, $source, $statementParts, $parameters, null); |
|
377 | + $parameters[] = $items; |
|
378 | + } |
|
379 | + } elseif ($operator === QueryInterface::OPERATOR_CONTAINS) { |
|
380 | + if ($operand2 === null) { |
|
381 | + $statementParts['where'][] = '1<>1'; |
|
382 | + } else { |
|
383 | + throw new \Exception('Not implemented! Contact extension author.', 1412931227); |
|
384 | + # @todo re-implement me if necessary. |
|
385 | + #$tableName = $this->query->getType(); |
|
386 | + #$propertyName = $operand1->getPropertyName(); |
|
387 | + #while (strpos($propertyName, '.') !== false) { |
|
388 | + # $this->addUnionStatement($tableName, $propertyName, $statementParts); |
|
389 | + #} |
|
390 | + #$columnName = $propertyName; |
|
391 | + #$columnMap = $propertyName; |
|
392 | + #$typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : null; |
|
393 | + #if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) { |
|
394 | + # $relationTableName = $columnMap->getRelationTableName(); |
|
395 | + # $statementParts['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=?)'; |
|
396 | + # $parameters[] = intval($this->getPlainValue($operand2)); |
|
397 | + #} elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) { |
|
398 | + # $parentKeyFieldName = $columnMap->getParentKeyFieldName(); |
|
399 | + # if (isset($parentKeyFieldName)) { |
|
400 | + # $childTableName = $columnMap->getChildTableName(); |
|
401 | + # $statementParts['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=?)'; |
|
402 | + # $parameters[] = intval($this->getPlainValue($operand2)); |
|
403 | + # } else { |
|
404 | + # $statementParts['where'][] = 'FIND_IN_SET(?,' . $tableName . '.' . $columnName . ')'; |
|
405 | + # $parameters[] = intval($this->getPlainValue($operand2)); |
|
406 | + # } |
|
407 | + #} else { |
|
408 | + # throw new Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745); |
|
409 | + #} |
|
410 | + } |
|
411 | + } else { |
|
412 | + if ($operand2 === null) { |
|
413 | + if ($operator === QueryInterface::OPERATOR_EQUAL_TO) { |
|
414 | + $operator = self::OPERATOR_EQUAL_TO_NULL; |
|
415 | + } elseif ($operator === QueryInterface::OPERATOR_NOT_EQUAL_TO) { |
|
416 | + $operator = self::OPERATOR_NOT_EQUAL_TO_NULL; |
|
417 | + } |
|
418 | + } |
|
419 | + $this->parseDynamicOperand($operand1, $operator, $source, $statementParts, $parameters); |
|
420 | + $parameters[] = $this->getPlainValue($operand2); |
|
421 | + } |
|
422 | + } |
|
423 | + |
|
424 | + /** |
|
425 | + * Returns a plain value, i.e. objects are flattened if possible. |
|
426 | + * |
|
427 | + * @param mixed $input |
|
428 | + * @return mixed |
|
429 | + * @throws UnexpectedTypeException |
|
430 | + */ |
|
431 | + protected function getPlainValue($input) |
|
432 | + { |
|
433 | + if (is_array($input)) { |
|
434 | + throw new UnexpectedTypeException('An array could not be converted to a plain value.', 1274799932); |
|
435 | + } |
|
436 | + if ($input instanceof \DateTime) { |
|
437 | + return $input->format('U'); |
|
438 | + } elseif (is_object($input)) { |
|
439 | + if ($input instanceof LazyLoadingProxy) { |
|
440 | + $realInput = $input->_loadRealInstance(); |
|
441 | + } else { |
|
442 | + $realInput = $input; |
|
443 | + } |
|
444 | + if ($realInput instanceof DomainObjectInterface) { |
|
445 | + return $realInput->getUid(); |
|
446 | + } else { |
|
447 | + throw new UnexpectedTypeException('An object of class "' . get_class($realInput) . '" could not be converted to a plain value.', 1274799934); |
|
448 | + } |
|
449 | + } elseif (is_bool($input)) { |
|
450 | + return $input === true ? 1 : 0; |
|
451 | + } else { |
|
452 | + return $input; |
|
453 | + } |
|
454 | + } |
|
455 | + |
|
456 | + /** |
|
457 | + * Parse a DynamicOperand into SQL and parameter arrays. |
|
458 | + * |
|
459 | + * @param DynamicOperandInterface $operand |
|
460 | + * @param string $operator One of the JCR_OPERATOR_* constants |
|
461 | + * @param SourceInterface $source The source |
|
462 | + * @param array &$statementParts The query parts |
|
463 | + * @param array &$parameters The parameters that will replace the markers |
|
464 | + * @param string $valueFunction an optional SQL function to apply to the operand value |
|
465 | + * @return void |
|
466 | + */ |
|
467 | + protected function parseDynamicOperand(DynamicOperandInterface $operand, $operator, SourceInterface $source, array &$statementParts, array &$parameters, $valueFunction = null) |
|
468 | + { |
|
469 | + if ($operand instanceof LowerCaseInterface) { |
|
470 | + $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $statementParts, $parameters, 'LOWER'); |
|
471 | + } elseif ($operand instanceof UpperCaseInterface) { |
|
472 | + $this->parseDynamicOperand($operand->getOperand(), $operator, $source, $statementParts, $parameters, 'UPPER'); |
|
473 | + } elseif ($operand instanceof PropertyValueInterface) { |
|
474 | + $propertyName = $operand->getPropertyName(); |
|
475 | + |
|
476 | + // Reset value. |
|
477 | + $this->currentChildTableNameAlias = ''; |
|
478 | + |
|
479 | + if ($source instanceof SelectorInterface) { |
|
480 | + $tableName = $this->query->getType(); |
|
481 | + while (strpos($propertyName, '.') !== false) { |
|
482 | + $this->addUnionStatement($tableName, $propertyName, $statementParts); |
|
483 | + } |
|
484 | + } elseif ($source instanceof JoinInterface) { |
|
485 | + $tableName = $source->getJoinCondition()->getSelector1Name(); |
|
486 | + } |
|
487 | + |
|
488 | + $columnName = $propertyName; |
|
489 | + $resolvedOperator = $this->resolveOperator($operator); |
|
490 | + $constraintSQL = ''; |
|
491 | + |
|
492 | + $marker = $operator === QueryInterface::OPERATOR_IN |
|
493 | + ? '(?)' |
|
494 | + : '?'; |
|
495 | + |
|
496 | + if ($valueFunction === null) { |
|
497 | + $constraintSQL .= (!empty($tableName) ? $tableName . '.' : '') . $columnName . ' ' . $resolvedOperator . ' ' . $marker; |
|
498 | + } else { |
|
499 | + $constraintSQL .= $valueFunction . '(' . (!empty($tableName) ? $tableName . '.' : '') . $columnName . ') ' . $resolvedOperator . ' ' . $marker; |
|
500 | + } |
|
501 | + |
|
502 | + if (isset($tableName) && !empty($this->currentChildTableNameAlias)) { |
|
503 | + $constraintSQL = $this->replaceTableNameByAlias($tableName, $this->currentChildTableNameAlias, $constraintSQL); |
|
504 | + } |
|
505 | + $statementParts['where'][] = $constraintSQL; |
|
506 | + } |
|
507 | + } |
|
508 | + |
|
509 | + /** |
|
510 | + * @param string &$tableName |
|
511 | + * @param string &$propertyPath |
|
512 | + * @param array &$statementParts |
|
513 | + */ |
|
514 | + protected function addUnionStatement(&$tableName, &$propertyPath, array &$statementParts) |
|
515 | + { |
|
516 | + |
|
517 | + $table = Tca::table($tableName); |
|
518 | + |
|
519 | + $explodedPropertyPath = explode('.', $propertyPath, 2); |
|
520 | + $fieldName = $explodedPropertyPath[0]; |
|
521 | + |
|
522 | + // Field of type "group" are special because property path must contain the table name |
|
523 | + // to determine the relation type. Example for sys_category, property path will look like "items.sys_file" |
|
524 | + $parts = explode('.', $propertyPath, 3); |
|
525 | + if ($table->field($fieldName)->isGroup() && count($parts) > 2) { |
|
526 | + $explodedPropertyPath[0] = $parts[0] . '.' . $parts[1]; |
|
527 | + $explodedPropertyPath[1] = $parts[2]; |
|
528 | + $fieldName = $explodedPropertyPath[0]; |
|
529 | + } |
|
530 | + |
|
531 | + $parentKeyFieldName = $table->field($fieldName)->getForeignField(); |
|
532 | + $childTableName = $table->field($fieldName)->getForeignTable(); |
|
533 | + |
|
534 | + if ($childTableName === null) { |
|
535 | + throw new InvalidRelationConfigurationException('The relation information for property "' . $fieldName . '" of class "' . $tableName . '" is missing.', 1353170925); |
|
536 | + } |
|
537 | + |
|
538 | + if ($table->field($fieldName)->hasOne()) { // includes relation "one-to-one" and "many-to-one" |
|
539 | + // sometimes the opposite relation is not defined. We don't want to force this config for backward compatibility reasons. |
|
540 | + // $parentKeyFieldName === null does the trick somehow. Before condition was if (isset($parentKeyFieldName)) |
|
541 | + if ($table->field($fieldName)->hasRelationManyToOne() || $parentKeyFieldName === null) { |
|
542 | + $statementParts['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.' . $fieldName . '=' . $childTableName . '.uid'; |
|
543 | + } else { |
|
544 | + $statementParts['unions'][$childTableName] = 'LEFT JOIN ' . $childTableName . ' ON ' . $tableName . '.uid=' . $childTableName . '.' . $parentKeyFieldName; |
|
545 | + } |
|
546 | + } elseif ($table->field($fieldName)->hasRelationManyToMany()) { |
|
547 | + $relationTableName = $table->field($fieldName)->getManyToManyTable(); |
|
548 | + |
|
549 | + $parentKeyFieldName = $table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local'; |
|
550 | + $childKeyFieldName = !$table->field($fieldName)->isOppositeRelation() ? 'uid_foreign' : 'uid_local'; |
|
551 | + |
|
552 | + // MM table e.g sys_category_record_mm |
|
553 | + $relationTableNameAlias = $this->generateAlias($relationTableName); |
|
554 | + $join = sprintf( |
|
555 | + 'LEFT JOIN %s AS %s ON %s.uid=%s.%s', $relationTableName, |
|
556 | + $relationTableNameAlias, |
|
557 | + $tableName, |
|
558 | + $relationTableNameAlias, |
|
559 | + $parentKeyFieldName |
|
560 | + ); |
|
561 | + $statementParts['unions'][$relationTableNameAlias] = $join; |
|
562 | + |
|
563 | + // Foreign table e.g sys_category |
|
564 | + $childTableNameAlias = $this->generateAlias($childTableName); |
|
565 | + $this->currentChildTableNameAlias = $childTableNameAlias; |
|
566 | + $join = sprintf( |
|
567 | + 'LEFT JOIN %s AS %s ON %s.%s=%s.uid', |
|
568 | + $childTableName, |
|
569 | + $childTableNameAlias, |
|
570 | + $relationTableNameAlias, |
|
571 | + $childKeyFieldName, |
|
572 | + $childTableNameAlias |
|
573 | + ); |
|
574 | + $statementParts['unions'][$childTableNameAlias] = $join; |
|
575 | + |
|
576 | + // Find a possible table name for a MM condition. |
|
577 | + $tableNameCondition = $table->field($fieldName)->getAdditionalTableNameCondition(); |
|
578 | + if ($tableNameCondition) { |
|
579 | + |
|
580 | + // If we can find a source file name, we can then retrieve more MM conditions from the TCA such as a field name. |
|
581 | + $sourceFileName = $this->query->getSourceFieldName(); |
|
582 | + if (empty($sourceFileName)) { |
|
583 | + $additionalMMConditions = array( |
|
584 | + 'tablenames' => $tableNameCondition, |
|
585 | + ); |
|
586 | + } else { |
|
587 | + $additionalMMConditions = Tca::table($tableNameCondition)->field($sourceFileName)->getAdditionalMMCondition(); |
|
588 | + } |
|
589 | + |
|
590 | + foreach ($additionalMMConditions as $additionalFieldName => $additionalMMCondition) { |
|
591 | + $additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition); |
|
592 | + $statementParts['unions'][$relationTableNameAlias] .= $additionalJoin; |
|
593 | + |
|
594 | + $additionalJoin = sprintf(' AND %s.%s = "%s"', $relationTableNameAlias, $additionalFieldName, $additionalMMCondition); |
|
595 | + $statementParts['unions'][$childTableNameAlias] .= $additionalJoin; |
|
596 | + } |
|
597 | + } |
|
598 | + |
|
599 | + } elseif ($table->field($fieldName)->hasMany()) { // includes relations "many-to-one" and "csv" relations |
|
600 | + $childTableNameAlias = $this->generateAlias($childTableName); |
|
601 | + $this->currentChildTableNameAlias = $childTableNameAlias; |
|
602 | + |
|
603 | + if (isset($parentKeyFieldName)) { |
|
604 | + $join = sprintf( |
|
605 | + 'LEFT JOIN %s AS %s ON %s.uid=%s.%s', |
|
606 | + $childTableName, |
|
607 | + $childTableNameAlias, |
|
608 | + $tableName, |
|
609 | + $childTableNameAlias, |
|
610 | + $parentKeyFieldName |
|
611 | + ); |
|
612 | + $statementParts['unions'][$childTableNameAlias] = $join; |
|
613 | + } else { |
|
614 | + $join = sprintf( |
|
615 | + 'LEFT JOIN %s AS %s ON (FIND_IN_SET(%s.uid, %s.%s))', |
|
616 | + $childTableName, |
|
617 | + $childTableNameAlias, |
|
618 | + $childTableNameAlias, |
|
619 | + $tableName, |
|
620 | + $fieldName |
|
621 | + ); |
|
622 | + $statementParts['unions'][$childTableNameAlias] = $join; |
|
623 | + } |
|
624 | + } else { |
|
625 | + throw new Exception('Could not determine type of relation.', 1252502725); |
|
626 | + } |
|
627 | + |
|
628 | + $statementParts['keywords']['distinct'] = 'DISTINCT'; |
|
629 | + $propertyPath = $explodedPropertyPath[1]; |
|
630 | + $tableName = $childTableName; |
|
631 | + } |
|
632 | + |
|
633 | + /** |
|
634 | + * Returns the SQL operator for the given JCR operator type. |
|
635 | + * |
|
636 | + * @param string $operator One of the JCR_OPERATOR_* constants |
|
637 | + * @return string an SQL operator |
|
638 | + * @throws Exception |
|
639 | + */ |
|
640 | + protected function resolveOperator($operator) |
|
641 | + { |
|
642 | + switch ($operator) { |
|
643 | + case self::OPERATOR_EQUAL_TO_NULL: |
|
644 | + $operator = 'IS'; |
|
645 | + break; |
|
646 | + case self::OPERATOR_NOT_EQUAL_TO_NULL: |
|
647 | + $operator = 'IS NOT'; |
|
648 | + break; |
|
649 | + case QueryInterface::OPERATOR_IN: |
|
650 | + $operator = 'IN'; |
|
651 | + break; |
|
652 | + case QueryInterface::OPERATOR_EQUAL_TO: |
|
653 | + $operator = '='; |
|
654 | + break; |
|
655 | + case QueryInterface::OPERATOR_NOT_EQUAL_TO: |
|
656 | + $operator = '!='; |
|
657 | + break; |
|
658 | + case QueryInterface::OPERATOR_LESS_THAN: |
|
659 | + $operator = '<'; |
|
660 | + break; |
|
661 | + case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO: |
|
662 | + $operator = '<='; |
|
663 | + break; |
|
664 | + case QueryInterface::OPERATOR_GREATER_THAN: |
|
665 | + $operator = '>'; |
|
666 | + break; |
|
667 | + case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO: |
|
668 | + $operator = '>='; |
|
669 | + break; |
|
670 | + case QueryInterface::OPERATOR_LIKE: |
|
671 | + $operator = 'LIKE'; |
|
672 | + break; |
|
673 | + default: |
|
674 | + throw new Exception('Unsupported operator encountered.', 1242816073); |
|
675 | + } |
|
676 | + return $operator; |
|
677 | + } |
|
678 | + |
|
679 | + /** |
|
680 | + * Adds additional WHERE statements according to the query settings. |
|
681 | + * |
|
682 | + * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings |
|
683 | + * @param string $tableNameOrAlias The table name to add the additional where clause for |
|
684 | + * @param array &$statementParts |
|
685 | + * @return void |
|
686 | + */ |
|
687 | + protected function addAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableNameOrAlias, &$statementParts) |
|
688 | + { |
|
689 | + $this->addVisibilityConstraintStatement($querySettings, $tableNameOrAlias, $statementParts); |
|
690 | + if ($querySettings->getRespectSysLanguage()) { |
|
691 | + $this->addSysLanguageStatement($tableNameOrAlias, $statementParts, $querySettings); |
|
692 | + } |
|
693 | + } |
|
694 | + |
|
695 | + /** |
|
696 | + * Adds enableFields and deletedClause to the query if necessary |
|
697 | + * |
|
698 | + * @param QuerySettingsInterface $querySettings |
|
699 | + * @param string $tableNameOrAlias The database table name |
|
700 | + * @param array &$statementParts The query parts |
|
701 | + * @return void |
|
702 | + */ |
|
703 | + protected function addVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableNameOrAlias, array &$statementParts) |
|
704 | + { |
|
705 | + $statement = ''; |
|
706 | + $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
707 | + if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) { |
|
708 | + $ignoreEnableFields = $querySettings->getIgnoreEnableFields(); |
|
709 | + $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored(); |
|
710 | + $includeDeleted = $querySettings->getIncludeDeleted(); |
|
711 | + if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) { |
|
712 | + $statement .= $this->getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted); |
|
713 | + } else { |
|
714 | + // 'BE' case |
|
715 | + $statement .= $this->getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted); |
|
716 | + } |
|
717 | + |
|
718 | + // Remove the prefixing "AND" if any. |
|
719 | + if (!empty($statement)) { |
|
720 | + $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement; |
|
721 | + $statementParts['additionalWhereClause'][$tableNameOrAlias][] = $statement; |
|
722 | + } |
|
723 | + } |
|
724 | + } |
|
725 | + |
|
726 | + /** |
|
727 | + * Returns constraint statement for frontend context |
|
728 | + * |
|
729 | + * @param string $tableNameOrAlias |
|
730 | + * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored |
|
731 | + * @param array $enableFieldsToBeIgnored If $ignoreEnableFields is true, this array specifies enable fields to be ignored. If it is null or an empty array (default) all enable fields are ignored. |
|
732 | + * @param boolean $includeDeleted A flag indicating whether deleted records should be included |
|
733 | + * @return string |
|
734 | + * @throws Exception\InconsistentQuerySettingsException |
|
735 | + */ |
|
736 | + protected function getFrontendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted) |
|
737 | + { |
|
738 | + $statement = ''; |
|
739 | + $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
740 | + if ($ignoreEnableFields && !$includeDeleted) { |
|
741 | + if (count($enableFieldsToBeIgnored)) { |
|
742 | + // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented |
|
743 | + $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored)); |
|
744 | + } else { |
|
745 | + $statement .= $this->getPageRepository()->deleteClause($tableName); |
|
746 | + } |
|
747 | + } elseif (!$ignoreEnableFields && !$includeDeleted) { |
|
748 | + $statement .= $this->getPageRepository()->enableFields($tableName); |
|
749 | + } elseif (!$ignoreEnableFields && $includeDeleted) { |
|
750 | + throw new InconsistentQuerySettingsException('Query setting "ignoreEnableFields=false" can not be used together with "includeDeleted=true" in frontend context.', 1327678173); |
|
751 | + } |
|
752 | + return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement); |
|
753 | + } |
|
754 | + |
|
755 | + /** |
|
756 | + * Returns constraint statement for backend context |
|
757 | + * |
|
758 | + * @param string $tableNameOrAlias |
|
759 | + * @param boolean $ignoreEnableFields A flag indicating whether the enable fields should be ignored |
|
760 | + * @param boolean $includeDeleted A flag indicating whether deleted records should be included |
|
761 | + * @return string |
|
762 | + */ |
|
763 | + protected function getBackendConstraintStatement($tableNameOrAlias, $ignoreEnableFields, $includeDeleted) |
|
764 | + { |
|
765 | + $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
766 | + $statement = ''; |
|
767 | + if (!$ignoreEnableFields) { |
|
768 | + $statement .= BackendUtility::BEenableFields($tableName); |
|
769 | + } |
|
770 | + |
|
771 | + // If the table is found to have "workspace" support, add the corresponding fields in the statement. |
|
772 | + if (Tca::table($tableName)->hasWorkspaceSupport()) { |
|
773 | + if ($this->getBackendUser()->workspace === 0) { |
|
774 | + $statement .= ' AND ' . $tableName . '.t3ver_state<=' . new VersionState(VersionState::DEFAULT_STATE); |
|
775 | + } else { |
|
776 | + // Show only records of live and of the current workspace |
|
777 | + // In case we are in a Versioning preview |
|
778 | + $statement .= ' AND (' . |
|
779 | + $tableName . '.t3ver_wsid=0 OR ' . |
|
780 | + $tableName . '.t3ver_wsid=' . (int)$this->getBackendUser()->workspace . |
|
781 | + ')'; |
|
782 | + } |
|
783 | + |
|
784 | + // Check if this segment make sense here or whether it should be in the "if" part when we have workspace = 0 |
|
785 | + $statement .= ' AND ' . $tableName . '.pid<>-1'; |
|
786 | + } |
|
787 | + |
|
788 | + if (!$includeDeleted) { |
|
789 | + $statement .= BackendUtility::deleteClause($tableName); |
|
790 | + } |
|
791 | + |
|
792 | + return $this->replaceTableNameByAlias($tableName, $tableNameOrAlias, $statement); |
|
793 | + } |
|
794 | + |
|
795 | + /** |
|
796 | + * Builds the language field statement |
|
797 | + * |
|
798 | + * @param string $tableNameOrAlias The database table name |
|
799 | + * @param array &$statementParts The query parts |
|
800 | + * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings |
|
801 | + * @return void |
|
802 | + * @throws Exception |
|
803 | + */ |
|
804 | + protected function addSysLanguageStatement($tableNameOrAlias, array &$statementParts, $querySettings) |
|
805 | + { |
|
806 | + $tableName = $this->resolveTableNameAlias($tableNameOrAlias); |
|
807 | + if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) { |
|
808 | + if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) { |
|
809 | + // Select all entries for the current language |
|
810 | + $additionalWhereClause = $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . intval($querySettings->getLanguageUid()) . ',-1)'; |
|
811 | + // If any language is set -> get those entries which are not translated yet |
|
812 | + // They will be removed by t3lib_page::getRecordOverlay if not matching overlay mode |
|
813 | + if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) |
|
814 | + && $querySettings->getLanguageUid() > 0 |
|
815 | + ) { |
|
816 | + $additionalWhereClause .= ' OR (' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' . |
|
817 | + ' AND ' . $tableNameOrAlias . '.uid NOT IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . |
|
818 | + ' FROM ' . $tableName . |
|
819 | + ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' . |
|
820 | + ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '>0'; |
|
821 | + |
|
822 | + // Add delete clause to ensure all entries are loaded |
|
823 | + if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) { |
|
824 | + $additionalWhereClause .= ' AND ' . $tableNameOrAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0'; |
|
825 | + } |
|
826 | + $additionalWhereClause .= '))'; |
|
827 | + } |
|
828 | + $statementParts['additionalWhereClause'][$tableNameOrAlias][] = '(' . $additionalWhereClause . ')'; |
|
829 | + } |
|
830 | + } |
|
831 | + } |
|
832 | + |
|
833 | + /** |
|
834 | + * Transforms orderings into SQL. |
|
835 | + * |
|
836 | + * @param array $orderings An array of orderings (Tx_Extbase_Persistence_QOM_Ordering) |
|
837 | + * @param SourceInterface $source The source |
|
838 | + * @param array &$statementParts The query parts |
|
839 | + * @return void |
|
840 | + * @throws Exception\UnsupportedOrderException |
|
841 | + */ |
|
842 | + protected function parseOrderings(array $orderings, SourceInterface $source, array &$statementParts) |
|
843 | + { |
|
844 | + foreach ($orderings as $fieldNameAndPath => $order) { |
|
845 | + switch ($order) { |
|
846 | + case QueryInterface::ORDER_ASCENDING: |
|
847 | + $order = 'ASC'; |
|
848 | + break; |
|
849 | + case QueryInterface::ORDER_DESCENDING: |
|
850 | + $order = 'DESC'; |
|
851 | + break; |
|
852 | + default: |
|
853 | + throw new UnsupportedOrderException('Unsupported order encountered.', 1456845126); |
|
854 | + } |
|
855 | + |
|
856 | + $tableName = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $this->query->getType()); |
|
857 | + $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $tableName); |
|
858 | + $statementParts['orderings'][] = sprintf('%s.%s %s', $tableName, $fieldName, $order); |
|
859 | + } |
|
860 | + } |
|
861 | + |
|
862 | + /** |
|
863 | + * Transforms limit and offset into SQL |
|
864 | + * |
|
865 | + * @param int $limit |
|
866 | + * @param int $offset |
|
867 | + * @param array &$statementParts |
|
868 | + * @return void |
|
869 | + */ |
|
870 | + protected function parseLimitAndOffset($limit, $offset, array &$statementParts) |
|
871 | + { |
|
872 | + if ($limit !== null && $offset !== null) { |
|
873 | + $statementParts['limit'] = intval($offset) . ', ' . intval($limit); |
|
874 | + } elseif ($limit !== null) { |
|
875 | + $statementParts['limit'] = intval($limit); |
|
876 | + } |
|
877 | + } |
|
878 | + |
|
879 | + /** |
|
880 | + * @param array $rows |
|
881 | + * @return array |
|
882 | + */ |
|
883 | + protected function getContentObjects(array $rows): array |
|
884 | + { |
|
885 | + $contentObjects = []; |
|
886 | + foreach ($rows as $row) { |
|
887 | + |
|
888 | + // Get language uid from querySettings. |
|
889 | + // Ensure the backend handling is not broken (fallback to Get parameter 'L' if needed) |
|
890 | + $overlaidRow = $this->doLanguageAndWorkspaceOverlay( |
|
891 | + $row, |
|
892 | + $this->query->getTypo3QuerySettings() |
|
893 | + ); |
|
894 | + |
|
895 | + $contentObjects[] = GeneralUtility::makeInstance( |
|
896 | + Content::class, |
|
897 | + $this->query->getType(), |
|
898 | + $overlaidRow |
|
899 | + ); |
|
900 | + } |
|
901 | + |
|
902 | + return $contentObjects; |
|
903 | + } |
|
904 | + |
|
905 | + /** |
|
906 | + * Performs workspace and language overlay on the given row array. The language and workspace id is automatically |
|
907 | + * detected (depending on FE or BE context). You can also explicitly set the language/workspace id. |
|
908 | + * |
|
909 | + * @param array $row |
|
910 | + * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings |
|
911 | + * @return array |
|
912 | + */ |
|
913 | + protected function doLanguageAndWorkspaceOverlay(array $row, $querySettings) |
|
914 | + { |
|
915 | + $tableName = $this->getTableName(); |
|
916 | + |
|
917 | + $pageRepository = $this->getPageRepository(); |
|
918 | + if (is_object($GLOBALS['TSFE'])) { |
|
919 | + $languageMode = $GLOBALS['TSFE']->sys_language_mode; |
|
920 | + #if ($this->isBackendUserLogged() && $this->getBackendUser()->workspace !== 0) { |
|
921 | + # $pageRepository->versioningWorkspaceId = $this->getBackendUser()->workspace; |
|
922 | + #} |
|
923 | + } else { |
|
924 | + $languageMode = ''; |
|
925 | + $workspaceUid = $this->getBackendUser()->workspace; |
|
926 | + #$pageRepository->versioningWorkspaceId = $workspaceUid; |
|
927 | + #if ($this->getBackendUser()->workspace !== 0) { |
|
928 | + # $pageRepository->versioningPreview = 1; |
|
929 | + #} |
|
930 | + } |
|
931 | + |
|
932 | + // If current row is a translation select its parent |
|
933 | + if (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) |
|
934 | + && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) |
|
935 | + ) { |
|
936 | + if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]) |
|
937 | + && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0 |
|
938 | + ) { |
|
939 | + $queryBuilder = $this->getQueryBuilder(); |
|
940 | + $row = $queryBuilder |
|
941 | + ->select($tableName . '.*') |
|
942 | + ->from($tableName) |
|
943 | + ->andWhere( |
|
944 | + $tableName . '.uid=' . (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']], |
|
945 | + $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' = 0' |
|
946 | + ) |
|
947 | + ->execute() |
|
948 | + ->fetch(); |
|
949 | + } |
|
950 | + } |
|
951 | + |
|
952 | + // Retrieve the original uid; Used for Workspaces! |
|
953 | + if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) { |
|
954 | + $pageRepository->versionOL($tableName, $row, true, true); |
|
955 | + } else { |
|
956 | + \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL($tableName, $row); |
|
957 | + } |
|
958 | + if ($pageRepository->versioningPreview && isset($row['_ORIG_uid'])) { |
|
959 | + $row['uid'] = $row['_ORIG_uid']; |
|
960 | + } |
|
961 | + |
|
962 | + // Special case for table "pages" |
|
963 | + if ($tableName == 'pages') { |
|
964 | + $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid()); |
|
965 | + } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) |
|
966 | + && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== '' |
|
967 | + ) { |
|
968 | + if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], array(-1, 0))) { |
|
969 | + $overlayMode = $languageMode === 'strict' ? 'hideNonTranslated' : ''; |
|
970 | + $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode); |
|
971 | + } |
|
972 | + } |
|
973 | + |
|
974 | + return $row; |
|
975 | + } |
|
976 | + |
|
977 | + /** |
|
978 | + * Return a resolved table name given a possible table name alias. |
|
979 | + * |
|
980 | + * @param string $tableNameOrAlias |
|
981 | + * @return string |
|
982 | + */ |
|
983 | + protected function resolveTableNameAlias($tableNameOrAlias) |
|
984 | + { |
|
985 | + $resolvedTableName = $tableNameOrAlias; |
|
986 | + if (!empty($this->tableNameAliases['aliases'][$tableNameOrAlias])) { |
|
987 | + $resolvedTableName = $this->tableNameAliases['aliases'][$tableNameOrAlias]; |
|
988 | + } |
|
989 | + return $resolvedTableName; |
|
990 | + } |
|
991 | + |
|
992 | + /** |
|
993 | + * Generate a unique table name alias for the given table name. |
|
994 | + * |
|
995 | + * @param string $tableName |
|
996 | + * @return string |
|
997 | + */ |
|
998 | + protected function generateAlias($tableName) |
|
999 | + { |
|
1000 | + |
|
1001 | + if (!isset($this->tableNameAliases['aliasIncrement'][$tableName])) { |
|
1002 | + $this->tableNameAliases['aliasIncrement'][$tableName] = 0; |
|
1003 | + } |
|
1004 | + |
|
1005 | + $numberOfAliases = $this->tableNameAliases['aliasIncrement'][$tableName]; |
|
1006 | + $tableNameAlias = $tableName . $numberOfAliases; |
|
1007 | + |
|
1008 | + $this->tableNameAliases['aliasIncrement'][$tableName]++; |
|
1009 | + $this->tableNameAliases['aliases'][$tableNameAlias] = $tableName; |
|
1010 | + |
|
1011 | + return $tableNameAlias; |
|
1012 | + } |
|
1013 | + |
|
1014 | + /** |
|
1015 | + * Replace the table names by its table name alias within the given statement. |
|
1016 | + * |
|
1017 | + * @param string $tableName |
|
1018 | + * @param string $tableNameAlias |
|
1019 | + * @param string $statement |
|
1020 | + * @return string |
|
1021 | + */ |
|
1022 | + protected function replaceTableNameByAlias($tableName, $tableNameAlias, $statement) |
|
1023 | + { |
|
1024 | + if ($statement && $tableName !== $tableNameAlias) { |
|
1025 | + $statement = str_replace($tableName, $tableNameAlias, $statement); |
|
1026 | + } |
|
1027 | + return $statement; |
|
1028 | + } |
|
1029 | + |
|
1030 | + /** |
|
1031 | + * Returns an instance of the current Backend User. |
|
1032 | + * |
|
1033 | + * @return BackendUserAuthentication |
|
1034 | + */ |
|
1035 | + protected function getBackendUser() |
|
1036 | + { |
|
1037 | + return $GLOBALS['BE_USER']; |
|
1038 | + } |
|
1039 | + |
|
1040 | + /** |
|
1041 | + * Tell whether a Backend User is logged in. |
|
1042 | + * |
|
1043 | + * @return bool |
|
1044 | + */ |
|
1045 | + protected function isBackendUserLogged() |
|
1046 | + { |
|
1047 | + return is_object($GLOBALS['BE_USER']); |
|
1048 | + } |
|
1049 | + |
|
1050 | + /** |
|
1051 | + * @return PageRepository|object |
|
1052 | + */ |
|
1053 | + protected function getPageRepository() |
|
1054 | + { |
|
1055 | + if (!$this->pageRepository instanceof PageRepository) { |
|
1056 | + if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() && is_object($GLOBALS['TSFE'])) { |
|
1057 | + $this->pageRepository = $GLOBALS['TSFE']->sys_page; |
|
1058 | + } else { |
|
1059 | + $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class); |
|
1060 | + } |
|
1061 | + } |
|
1062 | + |
|
1063 | + return $this->pageRepository; |
|
1064 | + } |
|
1065 | + |
|
1066 | + /** |
|
1067 | + * @return FieldPathResolver|object |
|
1068 | + */ |
|
1069 | + protected function getFieldPathResolver() |
|
1070 | + { |
|
1071 | + return GeneralUtility::makeInstance(FieldPathResolver::class); |
|
1072 | + } |
|
1073 | + |
|
1074 | + /** |
|
1075 | + * @return object|Connection |
|
1076 | + */ |
|
1077 | + protected function getConnection(): Connection |
|
1078 | + { |
|
1079 | + /** @var ConnectionPool $connectionPool */ |
|
1080 | + return GeneralUtility::makeInstance(ConnectionPool::class) |
|
1081 | + ->getConnectionForTable($this->getTableName()); |
|
1082 | + } |
|
1083 | + |
|
1084 | + /** |
|
1085 | + * @return object|QueryBuilder |
|
1086 | + */ |
|
1087 | + protected function getQueryBuilder(): QueryBuilder |
|
1088 | + { |
|
1089 | + /** @var ConnectionPool $connectionPool */ |
|
1090 | + $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); |
|
1091 | + return $connectionPool->getQueryBuilderForTable($this->getTableName()); |
|
1092 | + } |
|
1093 | + |
|
1094 | + /** |
|
1095 | + * @return string |
|
1096 | + */ |
|
1097 | + public function getTableName(): string |
|
1098 | + { |
|
1099 | + return $this->query->getSource()->getNodeTypeName(); // getSelectorName() |
|
1100 | + } |
|
1101 | 1101 | |
1102 | 1102 | } |