Completed
Pull Request — master (#5)
by Angel
09:33
created

ApiVersion   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Test Coverage

Coverage 74.49%

Importance

Changes 0
Metric Value
eloc 118
dl 0
loc 315
ccs 73
cts 98
cp 0.7449
rs 9.84
c 0
b 0
f 0
wmc 32

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getRoutes() 0 9 3
A beforeAction() 0 11 3
A buildControllerRoute() 0 9 1
A buildControllerClass() 0 15 2
A getFactSheet() 0 13 1
A getStability() 0 3 1
B init() 0 30 9
A calcTime() 0 12 3
A defaultUrlRules() 0 8 1
B createUrlRules() 0 39 7
A getSelfLink() 0 3 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 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 21
    }
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()
226
    {
227
        return [
228 21
            Yii::createObject([
229 21
                '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 createUrlRules(CompositeUrlRule $urlRule): array
241
    {
242 21
        $rules = $this->defaultUrlRules();
243 21
        if ($this->stability == self::STABILITY_OBSOLETE) {
244 2
            $rules[] = Yii::createObject([
245 2
                'class' => \yii\web\UrlRule::class,
246 2
                'pattern' => $this->uniqueId . '/<route:.+>',
247 2
                'route' => $this->module->uniqueId . '/index/gone',
248
            ]);
249
250 2
            return $rules;
251
        }
252
253 20
        foreach ($this->resources as $route => $controller) {
254 20
            $route = is_int($route) ? $controller : $route;
255 20
            $controllerRoute = $this->buildControllerRoute($route);
256 20
            if (is_string($controller)) {
257
                $controller = [
258
                    'class' => $this->buildControllerClass($controllerRoute),
259
                ];
260 20
            } elseif (is_array($controller) && empty($controller['class'])) {
261
                $controller['class'] = $this->buildControllerClass(
262
                    $controllerRoute
263
                );
264
            }
265 20
            $rules[] = Yii::createObject(array_merge(
266
                [
267 20
                    'class' => $this->urlRuleClass,
268
                    'controller' => [
269 20
                        $route => "{$this->uniqueId}/$controllerRoute",
270
                    ],
271 20
                    'prefix' => $this->uniqueId,
272
                ],
273 20
                ArrayHelper::remove($controller, 'urlRule', [])
0 ignored issues
show
Bug introduced by
It seems like yii\helpers\ArrayHelper:...er, '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

273
                /** @scrutinizer ignore-type */ ArrayHelper::remove($controller, 'urlRule', [])
Loading history...
274
            ));
275 20
            $this->controllerMap[$controllerRoute] = $controller;
276
        }
277
278 20
        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 20
    private function buildControllerRoute(string $roaRoute): string
288
    {
289 20
        return strtr(
290 20
            preg_replace(
291 20
                '/\/\<.*?\>\//',
292 20
                '--',
293 20
                $roaRoute
294
            ),
295 20
            ['/' => '-']
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
325
     */
326 21
    private function calcTime($date): ?int
327
    {
328 21
        if ($date === null) {
0 ignored issues
show
introduced by
The condition $date === null is always false.
Loading history...
329 1
            return null;
330
        }
331 21
        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 21
        return $dt->getTimestamp();
338
    }
339
340
    /**
341
     * @return string HTTP Url linking to this module
342
     */
343 3
    public function getSelfLink(): string
344
    {
345 3
        return Url::to(['//' . $this->getUniqueId()], true);
346
    }
347
}
348