Completed
Push — master ( 6db6da...5370d9 )
by Angel
03:42
created

ApiVersion   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 317
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Importance

Changes 0
Metric Value
wmc 32
lcom 2
cbo 5
dl 0
loc 317
rs 9.84
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getStability() 0 4 1
A getRoutes() 0 10 3
A getFactSheet() 0 16 1
B init() 0 33 9
A beforeAction() 0 12 3
A defaultUrlRules() 0 11 1
B createUrlRules() 0 40 7
A buildControllerRoute() 0 11 1
A buildControllerClass() 0 16 2
A calcTime() 0 13 3
A getSelfLink() 0 4 1
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
    const STABILITY_DEVELOPMENT = 'development';
34
    const STABILITY_STABLE = 'stable';
35
    const STABILITY_DEPRECATED = 'deprecated';
36
    const STABILITY_OBSOLETE = 'obsolete';
37
38
    /**
39
     * @var string subfix used to create the default classes
40
     */
41
    public $controllerSubfix = 'Resource';
42
43
    /**
44
     * @var string full class name which will be used as default for routing.
45
     */
46
    public $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 $releaseDate;
53
54
    /**
55
     * @var string date in Y-m-d format for the date at which this version
56
     * became deprecated
57
     */
58
    public $deprecationDate;
59
60
    /**
61
     * @var string date in Y-m-d format for the date at which this version
62
     * became obsolete
63
     */
64
    public $obsoleteDate;
65
66
    /**
67
     * @var string URL where the api documentation can be found.
68
     */
69
    public $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 $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 $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 $resources = [];
134
135
    /**
136
     * @return string[] gets the list of routes allowed for this api version.
137
     */
138
    public function getRoutes(): array
139
    {
140
        $routes = ['/'];
141
        foreach ($this->resources as $index => $value) {
142
            $routes[] =
143
                (is_string($index) ? $index : $value);
144
        }
145
146
        return $routes;
147
    }
148
149
    /**
150
     * @return array stability, life cycle and resources for this version.
151
     */
152
    public function getFactSheet(): array
153
    {
154
        return [
155
            'stability' => $this->stability,
156
            'lifeCycle' => [
157
                'releaseDate' => $this->releaseDate,
158
                'deprecationDate' => $this->deprecationDate,
159
                'obsoleteDate' => $this->obsoleteDate,
160
            ],
161
            'routes' => $this->getRoutes(),
162
            '_links' => [
163
                'self' => $this->getSelfLink(),
164
                'apidoc' => $this->apidoc,
165
            ],
166
        ];
167
    }
168
169
    /**
170
     * @inheritdoc
171
     */
172
    public function init()
173
    {
174
        parent::init();
175
        $releaseTime = $this->calcTime($this->releaseDate);
176
        $now = time();
177
178
        if ($releaseTime !== null && $releaseTime <= $now) {
179
            $deprecationTime = $this->calcTime($this->deprecationDate);
180
            $obsoleteTime = $this->calcTime($this->obsoleteDate);
181
            if ($deprecationTime !== null && $obsoleteTime !== null) {
182
                if ($obsoleteTime < $deprecationTime) {
183
                    throw new InvalidConfigException(
184
                        'The `obsoleteDate` must not be earlier than `deprecationDate`'
185
                    );
186
                }
187
                if ($deprecationTime < $releaseTime) {
188
                    throw new InvalidConfigException(
189
                        'The `deprecationDate` must not be earlier than `releaseDate`'
190
                    );
191
                }
192
193
                if ($obsoleteTime < $now) {
194
                    $this->stability = self::STABILITY_OBSOLETE;
195
                } elseif ($deprecationTime < $now) {
196
                    $this->stability = self::STABILITY_DEPRECATED;
197
                } else {
198
                    $this->stability = self::STABILITY_STABLE;
199
                }
200
            } else {
201
                $this->stability = self::STABILITY_STABLE;
202
            }
203
        }
204
    }
205
206
    /**
207
     * @inheritdoc
208
     */
209
    public function beforeAction($action)
210
    {
211
        if (!parent::beforeAction($action)) {
212
            return false;
213
        }
214
215
        foreach ($this->responseFormatters as $id => $responseFormatter) {
216
            Yii::$app->response->formatters[$id] = $responseFormatter;
217
        }
218
219
        return true;
220
    }
221
222
    /**
223
     * @return array list of configured urlrules by default
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
224
     */
225
    protected function defaultUrlRules()
226
    {
227
        return [
228
            Yii::createObject([
229
                'class' => \yii\web\UrlRule::class,
230
                'pattern' => $this->uniqueId,
231
                'route' => $this->uniqueId,
232
                'normalizer' => ['class' => \yii\web\UrlNormalizer::class],
233
            ]),
234
        ];
235
    }
236
237
    /**
238
     * @inheritdoc
239
     */
240
    public function createUrlRules(CompositeUrlRule $urlRule): array
241
    {
242
        $rules = $this->defaultUrlRules();
243
        if ($this->stability == self::STABILITY_OBSOLETE) {
244
            $rules[] = Yii::createObject([
245
                'class' => \yii\web\UrlRule::class,
246
                'pattern' => $this->uniqueId . '/<route:*+>',
247
                'route' => $this->uniqueId . '/index/gone',
248
            ]);
249
250
            return $rules;
251
        }
252
253
        foreach ($this->resources as $route => $controller) {
254
            $route = is_int($route) ? $controller : $route;
255
            $controllerRoute = $this->buildControllerRoute($route);
256
            if (is_string($controller)) {
257
                $controller = [
258
                    'class' => $this->buildControllerClass($controllerRoute),
259
                ];
260
            } elseif (is_array($controller) && empty($controller['class'])) {
261
                $controller['class'] = $this->buildControllerClass(
262
                    $controllerRoute
263
                );
264
            }
265
            $rules[] = Yii::createObject(array_merge(
266
                [
267
                    'class' => $this->urlRuleClass,
268
                    'controller' => [
269
                        $route => "{$this->uniqueId}/$controllerRoute",
270
                    ],
271
                    'prefix' => $this->uniqueId,
272
                ],
273
                ArrayHelper::remove($controller, 'urlRule', [])
274
            ));
275
            $this->controllerMap[$controllerRoute] = $controller;
276
        }
277
278
        return $rules;
279
    }
280
281
    /**
282
     * Converts a ROA route to an MVC route to be handled by `$controllerMap`
283
     *
284
     * @param string $roaRoute
285
     * @return string
286
     */
287
    private function buildControllerRoute(string $roaRoute): string
288
    {
289
        return strtr(
290
            preg_replace(
291
                '/\/\<.*?\>\//',
292
                '--',
293
                $roaRoute
294
            ),
295
            ['/' => '-']
296
        );
297
    }
298
299
    /**
300
     * Converts an MVC route to the default controller class.
301
     *
302
     * @param string $controllerRoute
303
     * @return string
304
     */
305
    private function buildControllerClass(string $controllerRoute): string
306
    {
307
        $lastSeparator = strrpos($controllerRoute, '--');
308
        if ($lastSeparator === false) {
309
            $lastClass = $controllerRoute;
310
            $ns = '';
311
        } else {
312
            $lastClass = substr($controllerRoute, $lastSeparator + 2);
313
            $ns = substr($controllerRoute, 0, $lastSeparator + 2);
314
        }
315
316
        return $this->controllerNamespace
317
            . '\\' . strtr($ns, ['--' => '\\'])
318
            . str_replace(' ', '', ucwords(str_replace('-', ' ', $lastClass)))
319
            . $this->controllerSubfix;
320
    }
321
322
    /**
323
     * @param string $date in 'Y-m-d' format
324
     * @return ?int unix timestamp
0 ignored issues
show
Documentation introduced by
The doc-type ?int could not be parsed: Unknown type name "?int" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
325
     */
326
    private function calcTime($date): ?int
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
327
    {
328
        if ($date === null) {
329
            return null;
330
        }
331
        if (false === ($dt = DateTime::createFromFormat('Y-m-d', $date))) {
332
            throw new InvalidConfigException(
333
                'Dates must use the "Y-m-d" format.'
334
            );
335
        }
336
337
        return $dt->getTimestamp();
338
    }
339
340
    /**
341
     * @return string HTTP Url linking to this module
342
     */
343
    public function getSelfLink(): string
344
    {
345
        return Url::to(['//' . $this->getUniqueId()], true);
346
    }
347
}
348