[TASK] Streamline Page Argument merge strategies
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Routing / PageArguments.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Routing;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use TYPO3\CMS\Core\Utility\ArrayUtility;
20
21 /**
22 * Contains all resolved parameters when a page is resolved from a page path segment plus all fragments.
23 */
24 class PageArguments implements RouteResultInterface
25 {
26 /**
27 * @var int
28 */
29 protected $pageId;
30
31 /**
32 * @var array
33 */
34 protected $arguments;
35
36 /**
37 * @var array
38 */
39 protected $staticArguments;
40
41 /**
42 * @var array
43 */
44 protected $dynamicArguments;
45
46 /**
47 * @var array
48 */
49 protected $routeArguments;
50
51 /**
52 * @var array
53 */
54 protected $queryArguments = [];
55
56 /**
57 * @var bool
58 */
59 protected $dirty = false;
60
61 /**
62 * @param int $pageId
63 * @param array $routeArguments
64 * @param array $staticArguments
65 * @param array $remainingArguments
66 */
67 public function __construct(int $pageId, array $routeArguments, array $staticArguments = [], array $remainingArguments = [])
68 {
69 $this->pageId = $pageId;
70 $this->routeArguments = $this->sort($routeArguments);
71 $this->staticArguments = $this->sort($staticArguments);
72 $this->arguments = $this->routeArguments;
73 $this->updateDynamicArguments();
74 if (!empty($remainingArguments)) {
75 $this->updateQueryArguments($remainingArguments);
76 }
77 }
78
79 /**
80 * @return bool
81 */
82 public function areDirty(): bool
83 {
84 return $this->dirty;
85 }
86
87 /**
88 * @return array
89 */
90 public function getRouteArguments(): array
91 {
92 return $this->routeArguments;
93 }
94
95 /**
96 * @return int
97 */
98 public function getPageId(): int
99 {
100 return $this->pageId;
101 }
102
103 /**
104 * @param string $name
105 * @return mixed|null
106 */
107 public function get(string $name)
108 {
109 return $this->arguments[$name] ?? null;
110 }
111
112 /**
113 * @return array
114 */
115 public function getArguments(): array
116 {
117 return $this->arguments;
118 }
119
120 /**
121 * @return array
122 */
123 public function getStaticArguments(): array
124 {
125 return $this->staticArguments;
126 }
127
128 /**
129 * @return array
130 */
131 public function getDynamicArguments(): array
132 {
133 return $this->dynamicArguments;
134 }
135
136 /**
137 * @return array
138 */
139 public function getQueryArguments(): array
140 {
141 return $this->queryArguments;
142 }
143
144 /**
145 * @param array $queryArguments
146 * @return static
147 * @internal this is internal due to the issue that a PageArgument should not be modified, but must be within TYPO3 Core currently.
148 */
149 public function withQueryArguments(array $queryArguments): self
150 {
151 $queryArguments = $this->sort($queryArguments);
152 if ($this->queryArguments === $queryArguments) {
153 return $this;
154 }
155 // in case query arguments would override route arguments,
156 // the state is considered as dirty (since it's not distinct)
157 // thus, route arguments take precedence over query arguments
158 $additionalQueryArguments = $this->diff($queryArguments, $this->routeArguments);
159 $dirty = $additionalQueryArguments !== $queryArguments;
160 // apply changes
161 $target = clone $this;
162 $target->dirty = $this->dirty || $dirty;
163 $target->queryArguments = $queryArguments;
164 $target->arguments = array_replace_recursive($target->arguments, $additionalQueryArguments);
165 $target->updateDynamicArguments();
166 return $target;
167 }
168
169 /**
170 * @param array $queryArguments
171 */
172 protected function updateQueryArguments(array $queryArguments)
173 {
174 $queryArguments = $this->sort($queryArguments);
175 if ($this->queryArguments === $queryArguments) {
176 return;
177 }
178 // in case query arguments would override route arguments,
179 // the state is considered as dirty (since it's not distinct)
180 // thus, route arguments take precedence over query arguments
181 $additionalQueryArguments = $this->diff($queryArguments, $this->routeArguments);
182 $dirty = $additionalQueryArguments !== $queryArguments;
183 $this->dirty = $this->dirty || $dirty;
184 $this->queryArguments = $queryArguments;
185 $this->arguments = array_replace_recursive($this->arguments, $additionalQueryArguments);
186 $this->updateDynamicArguments();
187 }
188
189 /**
190 * Updates dynamic arguments based on definitions for static arguments.
191 */
192 protected function updateDynamicArguments(): void
193 {
194 $this->dynamicArguments = $this->diff(
195 $this->arguments,
196 $this->staticArguments
197 );
198 }
199
200 /**
201 * Cleans empty array recursively.
202 *
203 * @param array $array
204 * @return array
205 */
206 protected function clean(array $array): array
207 {
208 foreach ($array as $key => &$item) {
209 if (!is_array($item)) {
210 continue;
211 }
212 if (!empty($item)) {
213 $item = $this->clean($item);
214 }
215 if (empty($item)) {
216 unset($array[$key]);
217 }
218 }
219 return $array;
220 }
221
222 /**
223 * Sorts array keys recursively.
224 *
225 * @param array $array
226 * @return array
227 */
228 protected function sort(array $array): array
229 {
230 $array = $this->clean($array);
231 ArrayUtility::naturalKeySortRecursive($array);
232 return $array;
233 }
234
235 /**
236 * Removes keys that are defined in $second from $first recursively.
237 *
238 * @param array $first
239 * @param array $second
240 * @return array
241 */
242 protected function diff(array $first, array $second): array
243 {
244 return ArrayUtility::arrayDiffAssocRecursive($first, $second);
245 }
246
247 /**
248 * @param mixed $offset
249 * @return bool
250 */
251 public function offsetExists($offset): bool
252 {
253 return $offset === 'pageId' || isset($this->arguments[$offset]);
254 }
255
256 /**
257 * @param mixed $offset
258 * @return mixed
259 */
260 public function offsetGet($offset)
261 {
262 if ($offset === 'pageId') {
263 return $this->getPageId();
264 }
265 return $this->arguments[$offset] ?? null;
266 }
267
268 /**
269 * @param mixed $offset
270 * @param mixed $value
271 */
272 public function offsetSet($offset, $value)
273 {
274 throw new \InvalidArgumentException('PageArguments cannot be modified.', 1538152266);
275 }
276
277 /**
278 * @param mixed $offset
279 */
280 public function offsetUnset($offset)
281 {
282 throw new \InvalidArgumentException('PageArguments cannot be modified.', 1538152269);
283 }
284 }