1 | <?php |
||||
2 | |||||
3 | namespace roaresearch\yii2\roa\modules; |
||||
4 | |||||
5 | use DateTime; |
||||
6 | use roaresearch\yii2\roa\{ |
||||
7 | controllers\ApiVersionController, |
||||
8 | urlRules\Composite as CompositeUrlRule, |
||||
9 | urlRules\Resource as ResourceUrlRule, |
||||
10 | urlRules\UrlRuleCreator |
||||
11 | }; |
||||
12 | use Yii; |
||||
13 | use yii\{ |
||||
14 | base\InvalidConfigException, |
||||
15 | helpers\ArrayHelper, |
||||
16 | helpers\Url, |
||||
17 | web\JsonResponseFormatter, |
||||
18 | web\Response, |
||||
19 | web\UrlManager, |
||||
20 | web\XmlResponseFormatter |
||||
21 | }; |
||||
22 | |||||
23 | /** |
||||
24 | * Class to attach a version to an `ApiContainer` module. |
||||
25 | * |
||||
26 | * You can control the stability by setting the properties `$releaseDate`, |
||||
27 | * `$deprecationDate` and `$obsoleteDate`. |
||||
28 | * |
||||
29 | * The resources are declared using the `$resources` array property |
||||
30 | */ |
||||
31 | class ApiVersion extends \yii\base\Module implements UrlRuleCreator |
||||
32 | { |
||||
33 | public const STABILITY_DEVELOPMENT = 'development'; |
||||
34 | public const STABILITY_STABLE = 'stable'; |
||||
35 | public const STABILITY_DEPRECATED = 'deprecated'; |
||||
36 | public const STABILITY_OBSOLETE = 'obsolete'; |
||||
37 | |||||
38 | /** |
||||
39 | * @var string subfix used to create the default classes |
||||
40 | */ |
||||
41 | public string $controllerSubfix = 'Resource'; |
||||
42 | |||||
43 | /** |
||||
44 | * @var string full class name which will be used as default for routing. |
||||
45 | */ |
||||
46 | public string $urlRuleClass = ResourceUrlRule::class; |
||||
47 | |||||
48 | /** |
||||
49 | * @var ?string date in Y-m-d format for the date at which this version |
||||
50 | * became stable |
||||
51 | */ |
||||
52 | public ?string $releaseDate = null; |
||||
53 | |||||
54 | /** |
||||
55 | * @var ?string date in Y-m-d format for the date at which this version |
||||
56 | * became deprecated |
||||
57 | */ |
||||
58 | public ?string $deprecationDate = null; |
||||
59 | |||||
60 | /** |
||||
61 | * @var ?string date in Y-m-d format for the date at which this version |
||||
62 | * became obsolete |
||||
63 | */ |
||||
64 | public ?string $obsoleteDate = null; |
||||
65 | |||||
66 | /** |
||||
67 | * @var string URL where the api documentation can be found. |
||||
68 | */ |
||||
69 | public ?string $apidoc = null; |
||||
70 | |||||
71 | /** |
||||
72 | * @var array|ResponseFormatterInterface[] response formatters which will |
||||
73 | * be attached to `Yii::$app->response->formatters`. By default just enable |
||||
74 | * HAL responses. |
||||
75 | */ |
||||
76 | public array $responseFormatters = [ |
||||
77 | Response::FORMAT_JSON => [ |
||||
78 | 'class' => JsonResponseFormatter::class, |
||||
79 | 'contentType' => JsonResponseFormatter::CONTENT_TYPE_HAL_JSON, |
||||
80 | ], |
||||
81 | Response::FORMAT_XML => [ |
||||
82 | 'class' => XmlResponseFormatter::class, |
||||
83 | 'contentType' => 'application/hal+xml', |
||||
84 | ], |
||||
85 | ]; |
||||
86 | |||||
87 | /** |
||||
88 | * @var string the stability level |
||||
89 | */ |
||||
90 | protected string $stability = self::STABILITY_DEVELOPMENT; |
||||
91 | |||||
92 | /** |
||||
93 | * @return string the stability defined for this version. |
||||
94 | */ |
||||
95 | public function getStability(): string |
||||
96 | { |
||||
97 | return $this->stability; |
||||
98 | } |
||||
99 | |||||
100 | /** |
||||
101 | * @inheritdoc |
||||
102 | */ |
||||
103 | public $defaultRoute = 'index'; |
||||
104 | |||||
105 | /** |
||||
106 | * @inheritdoc |
||||
107 | */ |
||||
108 | public $controllerMap = ['index' => ApiVersionController::class]; |
||||
109 | |||||
110 | /** |
||||
111 | * @var string[] list of 'patternRoute' => 'resource' pairs to connect a |
||||
112 | * route to a resource. if no key is used, then the value will be the |
||||
113 | * pattern too. |
||||
114 | * |
||||
115 | * Special properties: |
||||
116 | * |
||||
117 | * - urlRule array the configuration for how the routing url rules will be |
||||
118 | * created before attaching them to urlManager. |
||||
119 | * |
||||
120 | * ```php |
||||
121 | * [ |
||||
122 | * 'profile', // resources\ProfileResource |
||||
123 | * 'profile/history', // resources\profile\HistoryResource |
||||
124 | * 'profile/image' => [ |
||||
125 | * 'class' => resources\profile\ImageResource::class, |
||||
126 | * 'urlRule' => ['class' => 'roaresearch\yii2\\roa\\urlRules\\File'], |
||||
127 | * ], |
||||
128 | * 'post' => ['class' => resources\post\PostResource::class], |
||||
129 | * 'post/<post_id:[\d]+>/reply', // resources\post\ReplyResource |
||||
130 | * ] |
||||
131 | * ``` |
||||
132 | */ |
||||
133 | public array $resources = []; |
||||
134 | |||||
135 | /** |
||||
136 | * @return string[] gets the list of routes allowed for this api version. |
||||
137 | */ |
||||
138 | 3 | public function getRoutes(): array |
|||
139 | { |
||||
140 | 3 | $routes = ['/']; |
|||
141 | 3 | foreach ($this->resources as $index => $value) { |
|||
142 | 3 | $routes[] = |
|||
143 | 3 | (is_string($index) ? $index : $value); |
|||
144 | } |
||||
145 | |||||
146 | 3 | return $routes; |
|||
147 | } |
||||
148 | |||||
149 | /** |
||||
150 | * @return array stability, life cycle and resources for this version. |
||||
151 | */ |
||||
152 | 3 | public function getFactSheet(): array |
|||
153 | { |
||||
154 | return [ |
||||
155 | 3 | 'stability' => $this->stability, |
|||
156 | 'lifeCycle' => [ |
||||
157 | 3 | 'releaseDate' => $this->releaseDate, |
|||
158 | 3 | 'deprecationDate' => $this->deprecationDate, |
|||
159 | 3 | 'obsoleteDate' => $this->obsoleteDate, |
|||
160 | ], |
||||
161 | 3 | 'routes' => $this->getRoutes(), |
|||
162 | '_links' => [ |
||||
163 | 3 | 'self' => $this->getSelfLink(), |
|||
164 | 3 | 'apidoc' => $this->apidoc, |
|||
165 | ], |
||||
166 | ]; |
||||
167 | } |
||||
168 | |||||
169 | /** |
||||
170 | * @inheritdoc |
||||
171 | */ |
||||
172 | 21 | public function init() |
|||
173 | { |
||||
174 | 21 | parent::init(); |
|||
175 | 21 | $releaseTime = $this->calcTime($this->releaseDate); |
|||
176 | 21 | $now = time(); |
|||
177 | |||||
178 | 21 | if ($releaseTime !== null && $releaseTime <= $now) { |
|||
179 | 21 | $deprecationTime = $this->calcTime($this->deprecationDate); |
|||
180 | 21 | $obsoleteTime = $this->calcTime($this->obsoleteDate); |
|||
181 | 21 | if ($deprecationTime !== null && $obsoleteTime !== null) { |
|||
182 | 21 | if ($obsoleteTime < $deprecationTime) { |
|||
183 | throw new InvalidConfigException( |
||||
184 | 'The `obsoleteDate` must not be earlier than `deprecationDate`' |
||||
185 | ); |
||||
186 | } |
||||
187 | 21 | if ($deprecationTime < $releaseTime) { |
|||
188 | throw new InvalidConfigException( |
||||
189 | 'The `deprecationDate` must not be earlier than `releaseDate`' |
||||
190 | ); |
||||
191 | } |
||||
192 | |||||
193 | 21 | if ($obsoleteTime < $now) { |
|||
194 | 2 | $this->stability = self::STABILITY_OBSOLETE; |
|||
195 | 20 | } elseif ($deprecationTime < $now) { |
|||
196 | $this->stability = self::STABILITY_DEPRECATED; |
||||
197 | } else { |
||||
198 | 21 | $this->stability = self::STABILITY_STABLE; |
|||
199 | } |
||||
200 | } else { |
||||
201 | $this->stability = self::STABILITY_STABLE; |
||||
202 | } |
||||
203 | } |
||||
204 | } |
||||
205 | |||||
206 | /** |
||||
207 | * @inheritdoc |
||||
208 | */ |
||||
209 | 20 | public function beforeAction($action) |
|||
210 | { |
||||
211 | 20 | if (!parent::beforeAction($action)) { |
|||
212 | return false; |
||||
213 | } |
||||
214 | |||||
215 | 20 | foreach ($this->responseFormatters as $id => $responseFormatter) { |
|||
216 | 20 | Yii::$app->response->formatters[$id] = $responseFormatter; |
|||
217 | } |
||||
218 | |||||
219 | 20 | return true; |
|||
220 | } |
||||
221 | |||||
222 | /** |
||||
223 | * @return array list of configured urlrules by default |
||||
224 | */ |
||||
225 | 21 | protected function defaultUrlRules(): array |
|||
226 | { |
||||
227 | return [ |
||||
228 | 21 | Yii::createObject([ |
|||
229 | 'class' => \yii\web\UrlRule::class, |
||||
230 | 21 | 'pattern' => $this->uniqueId, |
|||
231 | 21 | 'route' => $this->uniqueId, |
|||
232 | 'normalizer' => ['class' => \yii\web\UrlNormalizer::class], |
||||
233 | ]), |
||||
234 | ]; |
||||
235 | } |
||||
236 | |||||
237 | /** |
||||
238 | * @inheritdoc |
||||
239 | */ |
||||
240 | 21 | public function initCreator(CompositeUrlRule $urlRule): void |
|||
241 | { |
||||
242 | 21 | if ($this->stability == self::STABILITY_OBSOLETE) { |
|||
243 | 2 | return; |
|||
244 | } |
||||
245 | |||||
246 | 20 | $resources = []; // normalized resources |
|||
247 | 20 | foreach ($this->resources as $route => $controller) { |
|||
248 | 20 | if (is_string($controller)) { |
|||
249 | $route = $controller; |
||||
250 | $controllerRoute = $this->buildControllerRoute($route); |
||||
251 | |||||
252 | $this->controllerMap[$controllerRoute] = $resources[$route] = [ |
||||
253 | 'class' => $this->buildControllerClass($controllerRoute), |
||||
254 | ]; |
||||
255 | $resources[$route]['controllerRoute'] = $controllerRoute; |
||||
256 | |||||
257 | continue; |
||||
258 | } |
||||
259 | |||||
260 | 20 | if (is_array($controller)) { |
|||
261 | 20 | $controllerRoute = isset($controller['controllerRoute']) |
|||
262 | ? ArrayHelper::remove($controller, 'controllerRoute') |
||||
263 | 20 | : $this->buildControllerRoute($route); |
|||
264 | |||||
265 | 20 | $controller['class'] = $controller['class'] |
|||
266 | ?? $this->buildControllerClass($controllerRoute); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
267 | |||||
268 | 20 | $resources[$route] = $controller; |
|||
269 | 20 | $resources[$route]['controllerRoute'] = $controllerRoute; |
|||
270 | |||||
271 | 20 | ArrayHelper::remove($controller, 'urlRule'); |
|||
272 | 20 | $this->controllerMap[$controllerRoute] = $controller; |
|||
273 | |||||
274 | 20 | continue; |
|||
275 | } |
||||
276 | |||||
277 | // case its an object |
||||
278 | $resources[$route] = [ |
||||
279 | 'controllerRoute' => $cR = $this->buildControllerRoute($route), |
||||
280 | ]; |
||||
281 | |||||
282 | $this->controllerMap[$cR] = $controller; |
||||
283 | } |
||||
284 | |||||
285 | 20 | $this->resources = $resources; // homologate resources |
|||
286 | } |
||||
287 | |||||
288 | /** |
||||
289 | * @inheritdoc |
||||
290 | */ |
||||
291 | 21 | public function createUrlRules(CompositeUrlRule $urlRule): array |
|||
292 | { |
||||
293 | 21 | $rules = $this->defaultUrlRules(); |
|||
294 | 21 | if ($this->stability == self::STABILITY_OBSOLETE) { |
|||
295 | 2 | $rules[] = Yii::createObject([ |
|||
296 | 'class' => \yii\web\UrlRule::class, |
||||
297 | 2 | 'pattern' => $this->uniqueId . '/<route:*+>', |
|||
298 | 2 | 'route' => $this->module->uniqueId . '/index/gone', |
|||
299 | ]); |
||||
300 | |||||
301 | 2 | return $rules; |
|||
302 | } |
||||
303 | |||||
304 | 20 | foreach ($this->resources as $route => $c) { |
|||
305 | 20 | $rules[] = Yii::createObject(array_merge( |
|||
306 | [ |
||||
307 | 20 | 'class' => $this->urlRuleClass, |
|||
308 | 'controller' => [ |
||||
309 | 20 | $route => "{$this->uniqueId}/{$c['controllerRoute']}" |
|||
310 | ], |
||||
311 | 20 | 'prefix' => $this->uniqueId, |
|||
312 | ], |
||||
313 | 20 | ArrayHelper::remove($c, 'urlRule', []) |
|||
0 ignored issues
–
show
It seems like
yii\helpers\ArrayHelper:...$c, 'urlRule', array()) can also be of type null ; however, parameter $arrays of array_merge() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
314 | )); |
||||
315 | } |
||||
316 | |||||
317 | 20 | return $rules; |
|||
318 | } |
||||
319 | |||||
320 | /** |
||||
321 | * Converts a ROA route to an MVC route to be handled by `$controllerMap` |
||||
322 | * |
||||
323 | * @param string $roaRoute |
||||
324 | * @return string |
||||
325 | */ |
||||
326 | 20 | private function buildControllerRoute(string $roaRoute): string |
|||
327 | { |
||||
328 | 20 | return strtr( |
|||
329 | 20 | preg_replace( |
|||
330 | '/\/\<.*?\>\//', |
||||
331 | '--', |
||||
332 | $roaRoute |
||||
333 | ), |
||||
334 | ['/' => '-'] |
||||
335 | ); |
||||
336 | } |
||||
337 | |||||
338 | /** |
||||
339 | * Converts an MVC route to the default controller class. |
||||
340 | * |
||||
341 | * @param string $controllerRoute |
||||
342 | * @return string |
||||
343 | */ |
||||
344 | private function buildControllerClass(string $controllerRoute): string |
||||
345 | { |
||||
346 | $lastSeparator = strrpos($controllerRoute, '--'); |
||||
347 | if ($lastSeparator === false) { |
||||
348 | $lastClass = $controllerRoute; |
||||
349 | $ns = ''; |
||||
350 | } else { |
||||
351 | $lastClass = substr($controllerRoute, $lastSeparator + 2); |
||||
352 | $ns = substr($controllerRoute, 0, $lastSeparator + 2); |
||||
353 | } |
||||
354 | |||||
355 | return $this->controllerNamespace |
||||
356 | . '\\' . strtr($ns, ['--' => '\\']) |
||||
357 | . str_replace(' ', '', ucwords(str_replace('-', ' ', $lastClass))) |
||||
358 | . $this->controllerSubfix; |
||||
359 | } |
||||
360 | |||||
361 | /** |
||||
362 | * @param string $date in 'Y-m-d' format |
||||
363 | * @return ?int unix timestamp |
||||
364 | */ |
||||
365 | 21 | private function calcTime($date): ?int |
|||
366 | { |
||||
367 | 21 | if ($date === null) { |
|||
0 ignored issues
–
show
|
|||||
368 | 1 | return null; |
|||
369 | } |
||||
370 | 21 | $dt = DateTime::createFromFormat('Y-m-d', $date) |
|||
371 | ?: throw new InvalidConfigException( |
||||
372 | 'Dates must use the "Y-m-d" format.' |
||||
373 | ); |
||||
374 | |||||
375 | 21 | return $dt->getTimestamp(); |
|||
376 | } |
||||
377 | |||||
378 | /** |
||||
379 | * @return string HTTP Url linking to this module |
||||
380 | */ |
||||
381 | 3 | public function getSelfLink(): string |
|||
382 | { |
||||
383 | 3 | return Url::to(['//' . $this->getUniqueId()], true); |
|||
384 | } |
||||
385 | } |
||||
386 |