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\Core\Routing;
19
20use TYPO3\CMS\Core\Utility\ArrayUtility;
21
22/**
23 * Contains all resolved parameters when a page is resolved from a page path segment plus all fragments.
24 */
25class PageArguments implements RouteResultInterface
26{
27    /**
28     * @var int
29     */
30    protected $pageId;
31
32    /**
33     * @var string
34     */
35    protected $pageType;
36
37    /**
38     * All (merged) arguments of this URI (routeArguments + dynamicArguments)
39     *
40     * @var array<string, string|array>
41     */
42    protected $arguments;
43
44    /**
45     * Route arguments mapped by static mappers
46     * "static" means the provided values in a URI maps to a finite number of values
47     * (routeArguments - "arguments mapped by non static mapper")
48     *
49     * @var array<string, string|array>
50     */
51    protected $staticArguments;
52
53    /**
54     * Route arguments, that have an infinite number of possible values
55     * AND query string arguments. These arguments require a cHash.
56     *
57     * @var array<string, string|array>
58     */
59    protected $dynamicArguments;
60
61    /**
62     * Arguments defined in and mapped by a route enhancer
63     *
64     * @var array<string, string|array>
65     */
66    protected $routeArguments;
67
68    /**
69     * Query arguments in the generated URI
70     *
71     * @var array<string, string|array>
72     */
73    protected $queryArguments = [];
74
75    /**
76     * @var bool
77     */
78    protected $dirty = false;
79
80    /**
81     * @param int $pageId
82     * @param string $pageType
83     * @param array $routeArguments
84     * @param array $staticArguments
85     * @param array $remainingArguments
86     */
87    public function __construct(int $pageId, string $pageType, array $routeArguments, array $staticArguments = [], array $remainingArguments = [])
88    {
89        $this->pageId = $pageId;
90        $this->pageType = $pageType;
91        $this->routeArguments = $this->sort($routeArguments);
92        $this->staticArguments = $this->sort($staticArguments);
93        $this->arguments = $this->routeArguments;
94        $this->updateDynamicArguments();
95        if (!empty($remainingArguments)) {
96            $this->updateQueryArguments($remainingArguments);
97        }
98    }
99
100    /**
101     * @return bool
102     */
103    public function areDirty(): bool
104    {
105        return $this->dirty;
106    }
107
108    /**
109     * @return array<string, string|array>
110     */
111    public function getRouteArguments(): array
112    {
113        return $this->routeArguments;
114    }
115
116    /**
117     * @return int
118     */
119    public function getPageId(): int
120    {
121        return $this->pageId;
122    }
123
124    /**
125     * @return string
126     */
127    public function getPageType(): string
128    {
129        return $this->pageType;
130    }
131
132    /**
133     * @param string $name
134     * @return string|array<string, string|array>|null
135     */
136    public function get(string $name)
137    {
138        return $this->arguments[$name] ?? null;
139    }
140
141    /**
142     * @return array<string, string|array>
143     */
144    public function getArguments(): array
145    {
146        return $this->arguments;
147    }
148
149    /**
150     * @return array<string, string|array>
151     */
152    public function getStaticArguments(): array
153    {
154        return $this->staticArguments;
155    }
156
157    /**
158     * @return array<string, string|array>
159     */
160    public function getDynamicArguments(): array
161    {
162        return $this->dynamicArguments;
163    }
164
165    /**
166     * @return array<string, string|array>
167     */
168    public function getQueryArguments(): array
169    {
170        return $this->queryArguments;
171    }
172
173    /**
174     * @param array<string, string|array> $queryArguments
175     */
176    protected function updateQueryArguments(array $queryArguments)
177    {
178        $queryArguments = $this->sort($queryArguments);
179        if ($this->queryArguments === $queryArguments) {
180            return;
181        }
182        // in case query arguments would override route arguments,
183        // the state is considered as dirty (since it's not distinct)
184        // thus, route arguments take precedence over query arguments
185        $additionalQueryArguments = $this->diff($queryArguments, $this->routeArguments);
186        $dirty = $additionalQueryArguments !== $queryArguments;
187        $this->dirty = $this->dirty || $dirty;
188        $this->queryArguments = $queryArguments;
189        $this->arguments = array_replace_recursive($this->arguments, $additionalQueryArguments);
190        $this->updateDynamicArguments();
191    }
192
193    /**
194     * Updates dynamic arguments based on definitions for static arguments.
195     */
196    protected function updateDynamicArguments(): void
197    {
198        $this->dynamicArguments = $this->diff(
199            $this->arguments,
200            $this->staticArguments
201        );
202    }
203
204    /**
205     * Cleans empty array recursively.
206     *
207     * @param array<string, string|array> $array
208     * @return array
209     */
210    protected function clean(array $array): array
211    {
212        foreach ($array as $key => &$item) {
213            if (!is_array($item)) {
214                continue;
215            }
216            if (!empty($item)) {
217                $item = $this->clean($item);
218            }
219            if (empty($item)) {
220                unset($array[$key]);
221            }
222        }
223        return $array;
224    }
225
226    /**
227     * Sorts array keys recursively.
228     *
229     * @param array<string, string|array> $array
230     * @return array
231     */
232    protected function sort(array $array): array
233    {
234        $array = $this->clean($array);
235        ArrayUtility::naturalKeySortRecursive($array);
236        return $array;
237    }
238
239    /**
240     * Removes keys that are defined in $second from $first recursively.
241     *
242     * @param array<string, string|array> $first
243     * @param array<string, string|array> $second
244     * @return array
245     */
246    protected function diff(array $first, array $second): array
247    {
248        return ArrayUtility::arrayDiffKeyRecursive($first, $second);
249    }
250
251    /**
252     * @param mixed $offset
253     * @return bool
254     */
255    public function offsetExists($offset): bool
256    {
257        return $offset === 'pageId' || $offset === 'pageType' || isset($this->arguments[$offset]);
258    }
259
260    /**
261     * @param mixed $offset
262     * @return string|array<string, string|array>|null
263     */
264    public function offsetGet($offset)
265    {
266        if ($offset === 'pageId') {
267            return $this->getPageId();
268        }
269        if ($offset === 'pageType') {
270            return $this->getPageType();
271        }
272        return $this->arguments[$offset] ?? null;
273    }
274
275    /**
276     * @param mixed $offset
277     * @param mixed $value
278     */
279    public function offsetSet($offset, $value)
280    {
281        throw new \InvalidArgumentException('PageArguments cannot be modified.', 1538152266);
282    }
283
284    /**
285     * @param mixed $offset
286     */
287    public function offsetUnset($offset)
288    {
289        throw new \InvalidArgumentException('PageArguments cannot be modified.', 1538152269);
290    }
291}
292