1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Extbase\Error;
19
20/**
21 * Result object for operations dealing with objects, such as the Property Mapper or the Validators.
22 */
23class Result
24{
25    /**
26     * @var Error[]
27     */
28    protected $errors = [];
29
30    /**
31     * Caches the existence of errors
32     * @var bool
33     */
34    protected $errorsExist = false;
35
36    /**
37     * @var Warning[]
38     */
39    protected $warnings = [];
40
41    /**
42     * Caches the existence of warning
43     * @var bool
44     */
45    protected $warningsExist = false;
46
47    /**
48     * @var Notice[]
49     */
50    protected $notices = [];
51
52    /**
53     * Caches the existence of notices
54     * @var bool
55     */
56    protected $noticesExist = false;
57
58    /**
59     * The result objects for the sub properties
60     *
61     * @var Result[]
62     */
63    protected $propertyResults = [];
64
65    /**
66     * @var Result
67     */
68    protected $parent;
69
70    /**
71     * Injects the parent result and propagates the
72     * cached error states upwards
73     *
74     * @param Result $parent
75     */
76    public function setParent(Result $parent): void
77    {
78        if ($this->parent !== $parent) {
79            $this->parent = $parent;
80            if ($this->hasErrors()) {
81                $parent->setErrorsExist();
82            }
83            if ($this->hasWarnings()) {
84                $parent->setWarningsExist();
85            }
86            if ($this->hasNotices()) {
87                $parent->setNoticesExist();
88            }
89        }
90    }
91
92    /**
93     * Add an error to the current Result object
94     *
95     * @param Error $error
96     */
97    public function addError(Error $error): void
98    {
99        $this->errors[] = $error;
100        $this->setErrorsExist();
101    }
102
103    /**
104     * Add a warning to the current Result object
105     *
106     * @param Warning $warning
107     */
108    public function addWarning(Warning $warning): void
109    {
110        $this->warnings[] = $warning;
111        $this->setWarningsExist();
112    }
113
114    /**
115     * Add a notice to the current Result object
116     *
117     * @param Notice $notice
118     */
119    public function addNotice(Notice $notice): void
120    {
121        $this->notices[] = $notice;
122        $this->setNoticesExist();
123    }
124
125    /**
126     * Get all errors in the current Result object (non-recursive)
127     *
128     * @return Error[]
129     */
130    public function getErrors(): array
131    {
132        return $this->errors;
133    }
134
135    /**
136     * Get all warnings in the current Result object (non-recursive)
137     *
138     * @return Warning[]
139     */
140    public function getWarnings(): array
141    {
142        return $this->warnings;
143    }
144
145    /**
146     * Get all notices in the current Result object (non-recursive)
147     *
148     * @return Notice[]
149     */
150    public function getNotices(): array
151    {
152        return $this->notices;
153    }
154
155    /**
156     * Get the first error object of the current Result object (non-recursive)
157     *
158     * @return bool|Error
159     */
160    public function getFirstError()
161    {
162        reset($this->errors);
163        return current($this->errors);
164    }
165
166    /**
167     * Get the first warning object of the current Result object (non-recursive)
168     *
169     * @return bool|Warning
170     */
171    public function getFirstWarning()
172    {
173        reset($this->warnings);
174        return current($this->warnings);
175    }
176
177    /**
178     * Get the first notice object of the current Result object (non-recursive)
179     *
180     * @return bool|Notice
181     */
182    public function getFirstNotice()
183    {
184        reset($this->notices);
185        return current($this->notices);
186    }
187
188    /**
189     * Return a Result object for the given property path. This is
190     * a fluent interface, so you will probably use it like:
191     * $result->forProperty('foo.bar')->getErrors() -- to get all errors
192     * for property "foo.bar"
193     *
194     * @param string|null $propertyPath
195     * @return Result
196     */
197    public function forProperty(?string $propertyPath): Result
198    {
199        if ($propertyPath === '' || $propertyPath === null) {
200            return $this;
201        }
202        if (strpos($propertyPath, '.') !== false) {
203            return $this->recurseThroughResult(explode('.', $propertyPath));
204        }
205        if (!isset($this->propertyResults[$propertyPath])) {
206            $this->propertyResults[$propertyPath] = new self();
207            $this->propertyResults[$propertyPath]->setParent($this);
208        }
209        return $this->propertyResults[$propertyPath];
210    }
211
212    /**
213     * @todo: consider making this method protected as it will and should not be called from an outside scope
214     *
215     * @param array $pathSegments
216     * @return Result
217     *
218     * @internal only to be used within Extbase, not part of TYPO3 Core API.
219     */
220    public function recurseThroughResult(array $pathSegments): Result
221    {
222        if (count($pathSegments) === 0) {
223            return $this;
224        }
225
226        $propertyName = array_shift($pathSegments);
227
228        if (!isset($this->propertyResults[$propertyName])) {
229            $this->propertyResults[$propertyName] = new self();
230            $this->propertyResults[$propertyName]->setParent($this);
231        }
232
233        return $this->propertyResults[$propertyName]->recurseThroughResult($pathSegments);
234    }
235
236    /**
237     * Sets the error cache to TRUE and propagates the information
238     * upwards the Result-Object Tree
239     */
240    protected function setErrorsExist(): void
241    {
242        $this->errorsExist = true;
243        if ($this->parent !== null) {
244            $this->parent->setErrorsExist();
245        }
246    }
247
248    /**
249     * Sets the warning cache to TRUE and propagates the information
250     * upwards the Result-Object Tree
251     */
252    protected function setWarningsExist(): void
253    {
254        $this->warningsExist = true;
255        if ($this->parent !== null) {
256            $this->parent->setWarningsExist();
257        }
258    }
259
260    /**
261     * Sets the notices cache to TRUE and propagates the information
262     * upwards the Result-Object Tree
263     */
264    protected function setNoticesExist(): void
265    {
266        $this->noticesExist = true;
267        if ($this->parent !== null) {
268            $this->parent->setNoticesExist();
269        }
270    }
271
272    /**
273     * Does the current Result object have Notices, Errors or Warnings? (Recursively)
274     *
275     * @return bool
276     */
277    public function hasMessages(): bool
278    {
279        return $this->errorsExist || $this->noticesExist || $this->warningsExist;
280    }
281
282    /**
283     * Clears the result
284     */
285    public function clear(): void
286    {
287        $this->errors = [];
288        $this->notices = [];
289        $this->warnings = [];
290
291        $this->warningsExist = false;
292        $this->noticesExist = false;
293        $this->errorsExist = false;
294
295        $this->propertyResults = [];
296    }
297
298    /**
299     * Does the current Result object have Errors? (Recursively)
300     *
301     * @return bool
302     */
303    public function hasErrors(): bool
304    {
305        if (count($this->errors) > 0) {
306            return true;
307        }
308
309        foreach ($this->propertyResults as $subResult) {
310            if ($subResult->hasErrors()) {
311                return true;
312            }
313        }
314
315        return false;
316    }
317
318    /**
319     * Does the current Result object have Warnings? (Recursively)
320     *
321     * @return bool
322     */
323    public function hasWarnings(): bool
324    {
325        if (count($this->warnings) > 0) {
326            return true;
327        }
328
329        foreach ($this->propertyResults as $subResult) {
330            if ($subResult->hasWarnings()) {
331                return true;
332            }
333        }
334
335        return false;
336    }
337
338    /**
339     * Does the current Result object have Notices? (Recursively)
340     *
341     * @return bool
342     */
343    public function hasNotices(): bool
344    {
345        if (count($this->notices) > 0) {
346            return true;
347        }
348
349        foreach ($this->propertyResults as $subResult) {
350            if ($subResult->hasNotices()) {
351                return true;
352            }
353        }
354
355        return false;
356    }
357
358    /**
359     * Get a list of all Error objects recursively. The result is an array,
360     * where the key is the property path where the error occurred, and the
361     * value is a list of all errors (stored as array)
362     *
363     * @return array<string,array<Error>>
364     */
365    public function getFlattenedErrors(): array
366    {
367        $result = [];
368        $this->flattenErrorTree($result, []);
369        return $result;
370    }
371
372    /**
373     * Get a list of all Warning objects recursively. The result is an array,
374     * where the key is the property path where the warning occurred, and the
375     * value is a list of all warnings (stored as array)
376     *
377     * @return array<string,array<Warning>>
378     */
379    public function getFlattenedWarnings(): array
380    {
381        $result = [];
382        $this->flattenWarningsTree($result, []);
383        return $result;
384    }
385
386    /**
387     * Get a list of all Notice objects recursively. The result is an array,
388     * where the key is the property path where the notice occurred, and the
389     * value is a list of all notices (stored as array)
390     *
391     * @return array<string,array<Notice>>
392     */
393    public function getFlattenedNotices(): array
394    {
395        $result = [];
396        $this->flattenNoticesTree($result, []);
397        return $result;
398    }
399
400    /**
401     * @param array $result
402     * @param array $level
403     */
404    protected function flattenErrorTree(array &$result, array $level): void
405    {
406        if (count($this->errors) > 0) {
407            $result[implode('.', $level)] = $this->errors;
408        }
409        foreach ($this->propertyResults as $subPropertyName => $subResult) {
410            $level[] = $subPropertyName;
411            $subResult->flattenErrorTree($result, $level);
412            array_pop($level);
413        }
414    }
415
416    /**
417     * @param array $result
418     * @param array $level
419     */
420    protected function flattenWarningsTree(array &$result, array $level): void
421    {
422        if (count($this->warnings) > 0) {
423            $result[implode('.', $level)] = $this->warnings;
424        }
425        foreach ($this->propertyResults as $subPropertyName => $subResult) {
426            $level[] = $subPropertyName;
427            $subResult->flattenWarningsTree($result, $level);
428            array_pop($level);
429        }
430    }
431
432    /**
433     * @param array $result
434     * @param array $level
435     */
436    protected function flattenNoticesTree(array &$result, array $level): void
437    {
438        if (count($this->notices) > 0) {
439            $result[implode('.', $level)] = $this->notices;
440        }
441        foreach ($this->propertyResults as $subPropertyName => $subResult) {
442            $level[] = $subPropertyName;
443            $subResult->flattenNoticesTree($result, $level);
444            array_pop($level);
445        }
446    }
447
448    /**
449     * Merge the given Result object into this one.
450     *
451     * @param Result $otherResult
452     */
453    public function merge(Result $otherResult): void
454    {
455        if ($otherResult->errorsExist) {
456            $this->mergeProperty($otherResult, 'getErrors', 'addError');
457        }
458        if ($otherResult->warningsExist) {
459            $this->mergeProperty($otherResult, 'getWarnings', 'addWarning');
460        }
461        if ($otherResult->noticesExist) {
462            $this->mergeProperty($otherResult, 'getNotices', 'addNotice');
463        }
464
465        foreach ($otherResult->getSubResults() as $subPropertyName => $subResult) {
466            /** @var Result $subResult */
467            if (array_key_exists($subPropertyName, $this->propertyResults) && $this->propertyResults[$subPropertyName]->hasMessages()) {
468                $this->forProperty($subPropertyName)->merge($subResult);
469            } else {
470                $this->propertyResults[$subPropertyName] = $subResult;
471                $subResult->setParent($this);
472            }
473        }
474    }
475
476    /**
477     * Merge a single property from the other result object.
478     *
479     * @param Result $otherResult
480     * @param string $getterName
481     * @param string $adderName
482     */
483    protected function mergeProperty(Result $otherResult, string $getterName, string $adderName): void
484    {
485        $getter = [$otherResult, $getterName];
486        $adder = [$this, $adderName];
487
488        if (!is_callable($getter) || !is_callable($adder)) {
489            return;
490        }
491
492        foreach ($getter() as $messageInOtherResult) {
493            $adder($messageInOtherResult);
494        }
495    }
496
497    /**
498     * Get a list of all sub Result objects available.
499     *
500     * @return Result[]
501     */
502    public function getSubResults(): array
503    {
504        return $this->propertyResults;
505    }
506}
507