ROAResearch /
yii2-roa
| 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
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
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
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
|
|||||
| 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 |