Skip to content

Commit 1e837f3

Browse files
committed
Merge #13 remote-tracking branch 'php-openapi/144-methods-naming-for-non-crud-actions'
* php-openapi/144-methods-naming-for-non-crud-actions: Refactor Remove TODO Refactor and add docs Add more concrete tests Complete the test Fix failing tests Add fix & test for cebe#84 Add docs about URL rules config Add docs Add more tests Fix bug Fix style Fix failing test Fix bug - WIP Fix this issue Fix issue in generated url rule config Fix issue: `actionCreateinvoicePayment` instead of `actionCreateInvoicePayment` Initial commit to create this PR
2 parents 9126089 + 5f7e993 commit 1e837f3

File tree

35 files changed

+921
-18
lines changed

35 files changed

+921
-18
lines changed

README.md

+75
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,81 @@ Provide custom database table column name in case of relationship column. This w
317317
- x-fk-column-name: redelivery_of # this will create `redelivery_of` column instead of `redelivery_of_id`
318318
```
319319
320+
### `x-route`
321+
322+
To customize route (controller ID/action ID) for a path, use custom key `x-route` with value `<controller ID>/<action ID>`. It can be used for non-crud paths. It must be used under HTTP method key but not
323+
directly under the `paths` key of OpenAPI spec. Example:
324+
325+
```yaml
326+
paths:
327+
/payments/invoice/{invoice}:
328+
parameters:
329+
- name: invoice
330+
in: path
331+
description: lorem ipsum
332+
required: true
333+
schema:
334+
type: integer
335+
post:
336+
x-route: 'payments/invoice'
337+
summary: Pay Invoice
338+
description: Pay for Invoice with given invoice number
339+
requestBody:
340+
description: Record new payment for an invoice
341+
content:
342+
application/json:
343+
schema:
344+
$ref: '#/components/schemas/Payments'
345+
required: true
346+
responses:
347+
'200':
348+
description: Successfully paid the invoice
349+
content:
350+
application/json:
351+
schema:
352+
$ref: '#/components/schemas/Success'
353+
```
354+
355+
It won't generate `actionCreateInvoice` in `PaymentsController.php` file, but will generate `actionInvoice` instead in
356+
same file.
357+
358+
Generated URL rules config for above is (in `urls.rest.php` or pertinent file):
359+
```php
360+
'POST payments/invoice/<invoice:\d+>' => 'payments/invoice',
361+
'payments/invoice/<invoice:\d+>' => 'payments/options',
362+
```
363+
364+
Also, if same action is needed for HTTP GET and POST then use same value for `x-route`. Example:
365+
366+
```yaml
367+
paths:
368+
/a1/b1:
369+
get:
370+
x-route: 'abc/xyz'
371+
operationId: opnid1
372+
summary: List
373+
description: Lists
374+
responses:
375+
'200':
376+
description: The Response
377+
post:
378+
x-route: 'abc/xyz'
379+
operationId: opnid2
380+
summary: create
381+
description: create
382+
responses:
383+
'200':
384+
description: The Response
385+
```
386+
387+
Generated URL rules config for above is (in `urls.rest.php` or pertinent file):
388+
```php
389+
'GET a1/b1' => 'abc/xyz',
390+
'POST a1/b1' => 'abc/xyz',
391+
'a1/b1' => 'abc/options',
392+
```
393+
`x-route` does not support [Yii Modules](https://www.yiiframework.com/doc/guide/2.0/en/structure-modules).
394+
320395
## Many-to-Many relation definition
321396

322397
There are two ways for define many-to-many relations:

src/generator/ApiGenerator.php

+33-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
namespace cebe\yii2openapi\generator;
99

1010
use cebe\yii2openapi\lib\items\DbModel;
11-
use yii\db\mysql\Schema as MySqlSchema;
12-
use SamIT\Yii2\MariaDb\Schema as MariaDbSchema;
13-
use yii\db\pgsql\Schema as PgSqlSchema;
1411
use cebe\openapi\Reader;
1512
use cebe\openapi\spec\OpenApi;
1613
use cebe\yii2openapi\lib\Config;
@@ -21,9 +18,14 @@
2118
use cebe\yii2openapi\lib\generators\RestActionGenerator;
2219
use cebe\yii2openapi\lib\generators\TransformersGenerator;
2320
use cebe\yii2openapi\lib\generators\UrlRulesGenerator;
21+
use cebe\yii2openapi\lib\items\FractalAction;
22+
use cebe\yii2openapi\lib\items\RestAction;
2423
use cebe\yii2openapi\lib\PathAutoCompletion;
2524
use cebe\yii2openapi\lib\SchemaToDatabase;
2625
use Yii;
26+
use yii\db\mysql\Schema as MySqlSchema;
27+
use SamIT\Yii2\MariaDb\Schema as MariaDbSchema;
28+
use yii\db\pgsql\Schema as PgSqlSchema;
2729
use yii\gii\CodeFile;
2830
use yii\gii\Generator;
2931
use yii\helpers\Html;
@@ -485,6 +487,7 @@ public function generate():array
485487
$urlRulesGenerator = Yii::createObject(UrlRulesGenerator::class, [$config, $actions]);
486488
$files = $urlRulesGenerator->generate();
487489

490+
$actions = static::removeDuplicateActions($actions); // in case of non-crud actions having custom route `x-route` set
488491
$controllersGenerator = Yii::createObject(ControllersGenerator::class, [$config, $actions]);
489492
$files->merge($controllersGenerator->generate());
490493

@@ -533,4 +536,31 @@ public static function isMariaDb():bool
533536
{
534537
return strpos(Yii::$app->db->schema->getServerVersion(), 'MariaDB') !== false;
535538
}
539+
540+
/**
541+
* @param RestAction[]|FractalAction[] $actions
542+
* @return RestAction[]|FractalAction[]
543+
* https://github.com/cebe/yii2-openapi/issues/84
544+
*/
545+
public static function removeDuplicateActions(array $actions): array
546+
{
547+
$actions = array_filter($actions, function ($action) {
548+
/** @var $action RestAction|FractalAction */
549+
if ($action instanceof RestAction && $action->isDuplicate) {
550+
return false;
551+
}
552+
return true;
553+
});
554+
555+
$actions = array_map(function ($action) {
556+
/** @var $action RestAction|FractalAction */
557+
if ($action instanceof RestAction && $action->zeroParams) {
558+
$action->idParam = null;
559+
$action->params = [];
560+
}
561+
return $action;
562+
}, $actions);
563+
564+
return $actions;
565+
}
536566
}

src/generator/default/urls.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
<?php
2+
3+
use yii\helpers\VarDumper;
4+
5+
?>
16
<?= '<?php' ?>
27

38
/**
49
* OpenAPI UrlRules
510
*
611
* This file is auto generated.
712
*/
8-
<?php $rules = \yii\helpers\VarDumper::export($urls);?>
13+
<?php /** @var array $urls */
14+
$rules = VarDumper::export($urls); ?>
915
return <?= str_replace('\\\\', '\\', $rules); ?>;

src/lib/CustomSpecAttr.php

+5
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,9 @@ class CustomSpecAttr
4040
* Foreign key column name. See README for usage docs
4141
*/
4242
public const FK_COLUMN_NAME = 'x-fk-column-name';
43+
44+
/**
45+
* Custom route (controller ID/action ID) instead of auto-generated. See README for usage docs. https://github.com/cebe/yii2-openapi/issues/144
46+
*/
47+
public const ROUTE = 'x-route';
4348
}

src/lib/generators/ControllersGenerator.php

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ protected function makeCustomController(
129129
$params = array_map(static function ($param) {
130130
return ['name' => $param];
131131
}, $action->getParamNames());
132+
132133
$reflection->addMethod(
133134
$action->actionMethodName,
134135
$params,

src/lib/generators/JsonActionGenerator.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ class JsonActionGenerator extends RestActionGenerator
2626
* @throws \yii\base\InvalidConfigException
2727
* @throws \cebe\openapi\exceptions\UnresolvableReferenceException
2828
*/
29-
protected function prepareAction(string $method, Operation $operation, RouteData $routeData):BaseObject
30-
{
29+
protected function prepareAction(
30+
string $method,
31+
Operation $operation,
32+
RouteData $routeData,
33+
?string $customRoute = null
34+
): BaseObject {
3135
$actionType = $this->resolveActionType($routeData, $method);
3236
$modelClass = ResponseSchema::guessModelClass($operation, $actionType);
3337
$expectedRelations = in_array($actionType, ['list', 'view'])

src/lib/generators/RestActionGenerator.php

+34-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use cebe\openapi\spec\PathItem;
1212
use cebe\openapi\spec\Reference;
1313
use cebe\yii2openapi\lib\Config;
14+
use cebe\yii2openapi\lib\CustomSpecAttr;
1415
use cebe\yii2openapi\lib\items\RestAction;
1516
use cebe\yii2openapi\lib\items\RouteData;
1617
use cebe\yii2openapi\lib\openapi\ResponseSchema;
@@ -58,6 +59,7 @@ public function generate():array
5859
return array_merge(...$actions);
5960
}
6061

62+
private $allCustomRoutes = [];
6163
/**
6264
* @param string $path
6365
* @param \cebe\openapi\spec\PathItem $pathItem
@@ -71,7 +73,24 @@ protected function resolvePath(string $path, PathItem $pathItem):array
7173

7274
$routeData = Yii::createObject(RouteData::class, [$pathItem, $path, $this->config->urlPrefixes]);
7375
foreach ($pathItem->getOperations() as $method => $operation) {
74-
$actions[] = $this->prepareAction($method, $operation, $routeData);
76+
$customRoute = null;
77+
if (isset($operation->{CustomSpecAttr::ROUTE})) { # https://github.com/cebe/yii2-openapi/issues/144
78+
$customRoute = $operation->{CustomSpecAttr::ROUTE};
79+
}
80+
81+
$action = $this->prepareAction($method, $operation, $routeData, $customRoute);
82+
if ($customRoute !== null) {
83+
if (in_array($customRoute, array_keys($this->allCustomRoutes))) {
84+
$action->isDuplicate = true;
85+
if ($action->params !== $this->allCustomRoutes[$customRoute]->params) {
86+
$this->allCustomRoutes[$customRoute]->zeroParams = true;
87+
}
88+
} else {
89+
$action->isDuplicate = false;
90+
$this->allCustomRoutes[$customRoute] = $action;
91+
}
92+
}
93+
$actions[] = $action;
7594
}
7695
return $actions;
7796
}
@@ -84,8 +103,12 @@ protected function resolvePath(string $path, PathItem $pathItem):array
84103
* @throws \cebe\openapi\exceptions\UnresolvableReferenceException
85104
* @throws \yii\base\InvalidConfigException
86105
*/
87-
protected function prepareAction(string $method, Operation $operation, RouteData $routeData):BaseObject
88-
{
106+
protected function prepareAction(
107+
string $method,
108+
Operation $operation,
109+
RouteData $routeData,
110+
?string $customRoute = null
111+
): BaseObject {
89112
$actionType = $this->resolveActionType($routeData, $method);
90113
$modelClass = ResponseSchema::guessModelClass($operation, $actionType);
91114
$responseWrapper = ResponseSchema::findResponseWrapper($operation, $modelClass);
@@ -106,12 +129,19 @@ protected function prepareAction(string $method, Operation $operation, RouteData
106129
$controllerId = isset($this->config->controllerModelMap[$modelClass])
107130
? Inflector::camel2id($this->config->controllerModelMap[$modelClass])
108131
: Inflector::camel2id($modelClass);
132+
} elseif (!empty($customRoute)) {
133+
$controllerId = explode('/', $customRoute)[0];
109134
} else {
110135
$controllerId = $routeData->controller;
111136
}
137+
$action = Inflector::camel2id($routeData->action);
138+
if (!empty($customRoute)) {
139+
$actionType = '';
140+
$action = explode('/', $customRoute)[1];
141+
}
112142
return Yii::createObject(RestAction::class, [
113143
[
114-
'id' => trim("$actionType{$routeData->action}", '-'),
144+
'id' => trim("$actionType-$action", '-'),
115145
'controllerId' => $controllerId,
116146
'urlPath' => $routeData->path,
117147
'requestMethod' => strtoupper($method),

src/lib/items/RestAction.php

+21-2
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,32 @@ final class RestAction extends BaseObject
6868
*/
6969
public $responseWrapper;
7070

71+
/**
72+
* @var bool
73+
* @see $isDuplicate
74+
* https://github.com/cebe/yii2-openapi/issues/84
75+
* see `x-route` in README.md
76+
* Used for generating only one action for paths like: `GET /calendar/domains` and `GET /calendar/domains/{id}` given that they have same `x-route`.
77+
* If duplicates routes have same params then `false`, else action is generated with no (0) params `true`
78+
*/
79+
public $zeroParams = false;
80+
81+
/**
82+
* @var bool
83+
* https://github.com/cebe/yii2-openapi/issues/84
84+
* Generate only one action for paths like: `GET /calendar/domains` and `GET /calendar/domains/{id}` given that they have same `x-route`.
85+
* @see $zeroParams
86+
* see `x-route` in README.md
87+
*/
88+
public $isDuplicate = false;
89+
7190
public function getRoute():string
7291
{
7392
if ($this->prefix && !empty($this->prefixSettings)) {
7493
$prefix = $this->prefixSettings['module'] ?? $this->prefix;
75-
return trim($prefix, '/').'/'.$this->controllerId.'/'.$this->id;
94+
return trim($prefix, '/') . '/' . $this->controllerId . '/' . $this->id;
7695
}
77-
return $this->controllerId.'/'.$this->id;
96+
return $this->controllerId . '/' . $this->id;
7897
}
7998

8099
public function getOptionsRoute():string
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
/**
3+
* OpenAPI UrlRules
4+
*
5+
* This file is auto generated.
6+
*/
7+
return [
8+
'GET fruit/mango' => 'fruits/mango',
9+
'GET fruits/mango' => 'fruit/mango',
10+
'POST fruits/mango' => 'fruit/create-mango',
11+
'GET animal/goat' => 'animal/goat',
12+
'POST animal/goat' => 'animal/create-goat',
13+
'POST payments/invoice/<invoice:\d+>' => 'payments/invoice',
14+
'GET payments/invoice-payment' => 'payment/invoice-payment',
15+
'GET a1/b1' => 'abc/xyz',
16+
'POST a1/b1' => 'abc/xyz',
17+
'GET aa2/bb2' => 'payments/xyz2',
18+
'fruit/mango' => 'fruits/options',
19+
'fruits/mango' => 'fruit/options',
20+
'animal/goat' => 'animal/options',
21+
'payments/invoice/<invoice:\d+>' => 'payments/options',
22+
'payments/invoice-payment' => 'payment/options',
23+
'a1/b1' => 'abc/options',
24+
'aa2/bb2' => 'payments/options',
25+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace app\controllers;
4+
5+
class AbcController extends \app\controllers\base\AbcController
6+
{
7+
8+
public function checkAccess($action, $model = null, $params = [])
9+
{
10+
//TODO implement checkAccess
11+
}
12+
13+
public function actionXyz()
14+
{
15+
//TODO implement actionXyz
16+
}
17+
18+
19+
}
20+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace app\controllers;
4+
5+
class AnimalController extends \app\controllers\base\AnimalController
6+
{
7+
8+
public function checkAccess($action, $model = null, $params = [])
9+
{
10+
//TODO implement checkAccess
11+
}
12+
13+
public function actionGoat()
14+
{
15+
//TODO implement actionGoat
16+
}
17+
18+
public function actionCreateGoat()
19+
{
20+
//TODO implement actionCreateGoat
21+
}
22+
23+
24+
}
25+

0 commit comments

Comments
 (0)