ApiVersion::defaultUrlRules()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 8
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
It seems like $controllerRoute can also be of type null; however, parameter $controllerRoute of roaresearch\yii2\roa\mod...:buildControllerClass() does only seem to accept string, 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 ignore-type  annotation

266
                    ?? $this->buildControllerClass(/** @scrutinizer ignore-type */ $controllerRoute);
Loading history...
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
Bug introduced by
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 ignore-type  annotation

313
                /** @scrutinizer ignore-type */ ArrayHelper::remove($c, 'urlRule', [])
Loading history...
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
introduced by
The condition $date === null is always false.
Loading history...
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