Skip to content

Commit 7265f1c

Browse files
committed
Merge branch 'controller-namespace-issue-for-modules-in-urlprefixes' of github.com:php-openapi/yii2-openapi into 102-fractalaction-not-generated-for-root-path
2 parents 905d925 + 18a7d2a commit 7265f1c

File tree

147 files changed

+3571
-142
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

147 files changed

+3571
-142
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,8 +610,8 @@ then the value for `comments` can be
610610

611611
### `x-route`
612612

613-
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
614-
directly under the `paths` key of OpenAPI spec. Example:
613+
To customize [route](https://www.yiiframework.com/doc/guide/2.0/en/runtime-routing) (controller ID/action ID) for a path, use custom key `x-route` with value `[<module ID>/<child module ID>/...]<controller ID>/<action ID>`. It can be used for non-crud paths. It must be used under HTTP method key but not
614+
directly under the `paths` key of OpenAPI spec. Providing `<controller ID>` and `<action ID>` is required. Example:
615615

616616
```yaml
617617
paths:
@@ -681,7 +681,17 @@ Generated URL rules config for above is (in `urls.rest.php` or pertinent file):
681681
'POST a1/b1' => 'abc/xyz',
682682
'a1/b1' => 'abc/options',
683683
```
684-
`x-route` does not support [Yii Modules](https://www.yiiframework.com/doc/guide/2.0/en/structure-modules).
684+
685+
`x-route` must not start with slash `/`. For example `x-route: /user/posts` is incorrect. It must start with [module ID](https://www.yiiframework.com/doc/guide/2.0/en/structure-modules) or [controller ID](https://www.yiiframework.com/doc/guide/2.0/en/structure-controllers#controller-ids)
686+
687+
#### Route, path and namespace
688+
689+
Route, path and namespace for controller/action will be resolved in following manner (from highest priority to lowest):
690+
691+
- [`x-route`](#x-route)
692+
- [`urlPrefixes`](https://github.com/php-openapi/yii2-openapi/blob/649743cf0a78743f550edcbd4e93fdffc55c76fd/src/lib/Config.php#L51)
693+
- [`controllerNamespace`](https://github.com/php-openapi/yii2-openapi/blob/649743cf0a78743f550edcbd4e93fdffc55c76fd/src/lib/Config.php#L77) of this lib
694+
- [`controllerNamespace`](https://github.com/yiisoft/yii2/blob/16f50626e1aa81200f109c1a455a5c9b18acfdda/framework/base/Application.php#L93) of Yii app
685695

686696
### `x-description-is-comment`
687697

src/lib/Config.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public function getOpenApi():OpenApi
183183
return $this->openApi;
184184
}
185185

186-
public function getPathFromNamespace(string $namespace):string
186+
public static function getPathFromNamespace(string $namespace): string
187187
{
188188
return Yii::getAlias('@' . str_replace('\\', '/', ltrim($namespace, '\\')));
189189
}

src/lib/generators/ControllersGenerator.php

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use Laminas\Code\Generator\ValueGenerator;
1919
use Yii;
2020
use yii\gii\CodeFile;
21-
use yii\helpers\ArrayHelper;
2221
use yii\helpers\Inflector;
2322

2423
class ControllersGenerator
@@ -38,7 +37,13 @@ class ControllersGenerator
3837
public function __construct(Config $config, array $actions = [])
3938
{
4039
$this->config = $config;
41-
$this->controllers = ArrayHelper::index($actions, null, 'controllerId');
40+
foreach ($actions as $action) {
41+
$r = $action->getRoute();
42+
$r = explode('/', $r);
43+
array_pop($r);
44+
array_pop($r);
45+
$this->controllers[implode('/', $r) . '/' . $action->controllerId][] = $action;
46+
}
4247
$this->files = new CodeFiles([]);
4348
}
4449

@@ -51,22 +56,25 @@ public function generate():CodeFiles
5156
return new CodeFiles([]);
5257
}
5358
$namespace = $this->config->controllerNamespace ?? Yii::$app->controllerNamespace;
54-
$path = $this->config->getPathFromNamespace($namespace);
59+
$path = Config::getPathFromNamespace($namespace);
5560
$templateName = $this->config->useJsonApi ? 'controller_jsonapi.php' : 'controller.php';
5661

57-
foreach ($this->controllers as $controller => $actions) {
62+
foreach ($this->controllers as $controllerWithPrefix => $actions) {
5863
$controllerNamespace = $namespace;
5964
$controllerPath = $path;
6065
/**
6166
* @var RestAction|FractalAction $action
6267
**/
6368
$action = $actions[0];
64-
if ($action->prefix && !empty($action->prefixSettings)) {
69+
if (!empty($action->prefixSettings)) {
6570
$controllerNamespace = trim($action->prefixSettings['namespace'], '\\');
6671
$controllerPath = $action->prefixSettings['path']
67-
?? $this->config->getPathFromNamespace($controllerNamespace);
72+
?? Config::getPathFromNamespace($controllerNamespace);
6873
}
69-
$className = Inflector::id2camel($controller) . 'Controller';
74+
75+
$routeParts = explode('/', $controllerWithPrefix);
76+
77+
$className = Inflector::id2camel(end($routeParts)) . 'Controller';
7078
$this->files->add(new CodeFile(
7179
Yii::getAlias($controllerPath . "/base/$className.php"),
7280
$this->config->render(
@@ -86,6 +94,18 @@ public function generate():CodeFiles
8694
$classFileGenerator->generate()
8795
));
8896
}
97+
98+
// generate Module.php file for modules
99+
foreach ($action->modulesList as $moduleId => $moduleDetail) {
100+
// only generate Module.php file if they do not exist, do not override
101+
if (!file_exists(Yii::getAlias($moduleDetail['path'] . "/Module.php"))) {
102+
$moduleFileGenerator = $this->makeModuleFile('Module', $moduleDetail['namespace'], $moduleId, $action);
103+
$this->files->add(new CodeFile(
104+
Yii::getAlias($moduleDetail['path'] . "/Module.php"),
105+
$moduleFileGenerator->generate()
106+
));
107+
}
108+
}
89109
}
90110
return $this->files;
91111
}
@@ -156,4 +176,37 @@ protected function makeCustomController(
156176
$classFileGenerator->setClasses([$reflection]);
157177
return $classFileGenerator;
158178
}
179+
180+
/**
181+
* @param RestAction|FractalAction $action
182+
*/
183+
public function makeModuleFile(string $class, string $namespace, $moduleId, $action): FileGenerator
184+
{
185+
$file = new FileGenerator;
186+
$reflection = new ClassGenerator(
187+
$class,
188+
$namespace,
189+
null,
190+
'yii\base\Module'
191+
);
192+
193+
$moduleIds = array_keys($action->modulesList);
194+
$position = array_search($moduleId, $moduleIds);
195+
$childModuleId = $childModuleCode = null;
196+
if (array_key_exists($position + 1, $moduleIds)) { # if `true`, child module exists
197+
$childModuleId = $moduleIds[$position + 1];
198+
$childModuleNamespace = $action->modulesList[$childModuleId]['namespace'];
199+
$childModuleCode = <<<PHP
200+
\$this->modules = [
201+
'{$childModuleId}' => [
202+
'class' => \\{$childModuleNamespace}\Module::class,
203+
],
204+
];
205+
PHP;
206+
}
207+
208+
$reflection->addMethod('init', [], AbstractMemberGenerator::FLAG_PUBLIC, 'parent::init();' . PHP_EOL . $childModuleCode);
209+
$file->setClasses([$reflection]);
210+
return $file;
211+
}
159212
}

src/lib/generators/JsonActionGenerator.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,19 @@ protected function prepareAction(
7474
}
7575
}
7676

77+
$actionId = $routeData->isNonCrudAction() ? trim("{$actionType}-{$routeData->action}", '-')
78+
: "$actionType{$routeData->action}";
79+
if (!empty($customRoute)) {
80+
$parts = explode('/', $customRoute);
81+
$controllerId = $parts[count($parts) - 2];
82+
$actionId = $parts[count($parts) - 1];
83+
}
84+
7785
return Yii::createObject(FractalAction::class, [
7886
[
7987
'singularResourceKey' => $this->config->singularResourceKeys,
8088
'type' => $routeData->type,
81-
'id' => $routeData->isNonCrudAction() ? trim("{$actionType}-{$routeData->action}", '-')
82-
: "$actionType{$routeData->action}",
89+
'id' => $actionId,
8390
'controllerId' => $controllerId,
8491
'urlPath' => $routeData->path,
8592
'requestMethod' => strtoupper($method),
@@ -96,6 +103,8 @@ protected function prepareAction(
96103
'expectedRelations' => $expectedRelations,
97104
'prefix' => $routeData->getPrefix(),
98105
'prefixSettings' => $routeData->getPrefixSettings(),
106+
'xRoute' => $customRoute,
107+
'modulesList' => $routeData->listModules()
99108
],
100109
]);
101110
}

src/lib/generators/ModelsGenerator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public function generate():CodeFiles
4646
if (!$this->config->generateModels) {
4747
return $this->files;
4848
}
49-
$modelPath = $this->config->getPathFromNamespace($this->config->modelNamespace);
50-
$fakerPath = $this->config->getPathFromNamespace($this->config->fakerNamespace);
49+
$modelPath = Config::getPathFromNamespace($this->config->modelNamespace);
50+
$fakerPath = Config::getPathFromNamespace($this->config->fakerNamespace);
5151
if ($this->config->generateModelFaker) {
5252
$this->files->add(new CodeFile(
5353
Yii::getAlias("$fakerPath/BaseModelFaker.php"),

src/lib/generators/RestActionGenerator.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ protected function resolvePath(string $path, PathItem $pathItem):array
7171
{
7272
$actions = [];
7373

74-
$routeData = Yii::createObject(RouteData::class, [$pathItem, $path, $this->config->urlPrefixes]);
7574
foreach ($pathItem->getOperations() as $method => $operation) {
75+
$routeData = Yii::createObject(RouteData::class, [$path, $pathItem, $method, $operation, $this->config->urlPrefixes]);
7676
$customRoute = null;
7777
if (isset($operation->{CustomSpecAttr::ROUTE})) { # https://github.com/cebe/yii2-openapi/issues/144
7878
$customRoute = $operation->{CustomSpecAttr::ROUTE};
@@ -119,7 +119,10 @@ protected function prepareAction(
119119
$this->knownModelClasses[$routeData->path] = $modelClass;
120120
}
121121

122-
if ($routeData->isRelationship()) {
122+
if (!empty($customRoute)) {
123+
$parts = explode('/', $customRoute);
124+
$controllerId = $parts[count($parts) - 2];
125+
} elseif ($routeData->isRelationship()) {
123126
$controllerId = $routeData->controller;
124127
$modelClass = Inflector::id2camel(Inflector::singularize($controllerId));
125128
$controllerId = isset($this->config->controllerModelMap[$modelClass])
@@ -129,15 +132,17 @@ protected function prepareAction(
129132
$controllerId = isset($this->config->controllerModelMap[$modelClass])
130133
? Inflector::camel2id($this->config->controllerModelMap[$modelClass])
131134
: Inflector::camel2id($modelClass);
132-
} elseif (!empty($customRoute)) {
133-
$controllerId = explode('/', $customRoute)[0];
134135
} else {
135136
$controllerId = $routeData->controller;
136137
}
137138
$action = Inflector::camel2id($routeData->action);
139+
if (!$action && !$actionType) {
140+
$action = 'index';
141+
}
138142
if (!empty($customRoute)) {
139143
$actionType = '';
140-
$action = explode('/', $customRoute)[1];
144+
$parts = explode('/', $customRoute);
145+
$action = $parts[count($parts) - 1];
141146
}
142147
return Yii::createObject(RestAction::class, [
143148
[
@@ -154,7 +159,9 @@ protected function prepareAction(
154159
: null,
155160
'responseWrapper' => $responseWrapper,
156161
'prefix' => $routeData->getPrefix(),
157-
'prefixSettings' => $routeData->getPrefixSettings()
162+
'prefixSettings' => $routeData->getPrefixSettings(),
163+
'xRoute' => $customRoute,
164+
'modulesList' => $routeData->listModules()
158165
],
159166
]);
160167
}

src/lib/generators/TransformersGenerator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function generate():CodeFiles
4848
if (!$this->config->generateControllers || !$this->config->useJsonApi) {
4949
return $this->files;
5050
}
51-
$transformerPath = $this->config->getPathFromNamespace($this->config->transformerNamespace);
51+
$transformerPath = Config::getPathFromNamespace($this->config->transformerNamespace);
5252
foreach ($this->models as $model) {
5353
$transformer = Yii::createObject(Transformer::class, [
5454
$model,

src/lib/items/OptionsRoutesTrait.php renamed to src/lib/items/ActionHelperTrait.php

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,36 @@
77

88
namespace cebe\yii2openapi\lib\items;
99

10-
trait OptionsRoutesTrait
10+
trait ActionHelperTrait
1111
{
12+
public ?string $xRoute = null;
13+
14+
# list of module this action is part of. 'key' is module ID and 'value' is path where Module.php file must be generated
15+
public array $modulesList = [];
16+
17+
/**
18+
* @see $isDuplicate
19+
* https://github.com/cebe/yii2-openapi/issues/84
20+
* see `x-route` in README.md
21+
* Used for generating only one action for paths like: `GET /calendar/domains` and `GET /calendar/domains/{id}` given that they have same `x-route`.
22+
* If duplicates routes have same params then `false`, else action is generated with no (0) params `true`
23+
*/
24+
public bool $zeroParams = false;
25+
26+
/**
27+
* https://github.com/cebe/yii2-openapi/issues/84
28+
* Generate only one action for paths like: `GET /calendar/domains` and `GET /calendar/domains/{id}` given that they have same `x-route`.
29+
* @see $zeroParams
30+
* see `x-route` in README.md
31+
*/
32+
public bool $isDuplicate = false;
33+
1234
public function getOptionsRoute():string
1335
{
14-
if ($this->prefix && !empty($this->prefixSettings)) {
15-
if (isset($this->prefixSettings['module'])) {
16-
$prefix = $this->prefixSettings['module'];
17-
return static::finalOptionsRoute($prefix, $this->controllerId);
18-
} elseif (isset($this->prefixSettings['namespace']) && str_contains($this->prefixSettings['namespace'], '\modules\\')) { # if `module` not present then check in namespace and then in path
19-
$prefix = static::computeModule('\\', $this->prefixSettings['namespace']);
20-
if ($prefix) {
21-
return static::finalOptionsRoute($prefix, $this->controllerId);
22-
}
23-
} elseif (isset($this->prefixSettings['path']) && str_contains($this->prefixSettings['path'], '/modules/')) {
24-
$prefix = static::computeModule('/', $this->prefixSettings['path']);
25-
if ($prefix) {
26-
return static::finalOptionsRoute($prefix, $this->controllerId);
27-
}
28-
}
29-
}
30-
return $this->controllerId.'/options';
36+
$r = $this->getRoute();
37+
$r = explode('/', $r);
38+
array_pop($r);
39+
return implode('/', $r) . '/options';
3140
}
3241

3342
/**
@@ -59,4 +68,30 @@ public static function finalOptionsRoute(string $prefix, string $controllerId):
5968
{
6069
return trim($prefix, '/') . '/' . $controllerId . '/options';
6170
}
71+
72+
public function getRoute(): string
73+
{
74+
if ($this->xRoute) {
75+
return $this->xRoute;
76+
}
77+
78+
if (!empty($this->prefixSettings)) {
79+
if (isset($this->prefixSettings['module'])) {
80+
$prefix = $this->prefixSettings['module'];
81+
return trim($prefix, '/') . '/' . $this->controllerId . ($this->id ? '/' . $this->id : '');
82+
} elseif (isset($this->prefixSettings['namespace']) && str_contains($this->prefixSettings['namespace'], '\modules\\')) { # if `module` not present then check in namespace and then in path
83+
$prefix = static::computeModule('\\', $this->prefixSettings['namespace']);
84+
if ($prefix) {
85+
return trim($prefix, '/') . '/' . $this->controllerId . ($this->id ? '/' . $this->id : '');
86+
}
87+
} elseif (isset($this->prefixSettings['path']) && str_contains($this->prefixSettings['path'], '/modules/')) {
88+
$prefix = static::computeModule('/', $this->prefixSettings['path']);
89+
if ($prefix) {
90+
return trim($prefix, '/') . '/' . $this->controllerId . ($this->id ? '/' . $this->id : '');
91+
}
92+
}
93+
}
94+
95+
return $this->controllerId . ($this->id ? '/' . $this->id : '');
96+
}
6297
}

src/lib/items/FractalAction.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
*/
3333
final class FractalAction extends BaseObject
3434
{
35-
use OptionsRoutesTrait;
35+
use ActionHelperTrait;
3636

3737
/**@var string* */
3838
public $id;
@@ -95,15 +95,6 @@ private function templateFactory():FractalActionTemplates
9595
return $this->templateFactory;
9696
}
9797

98-
public function getRoute():string
99-
{
100-
if ($this->prefix && !empty($this->prefixSettings)) {
101-
$prefix = $this->prefixSettings['module'] ?? $this->prefix;
102-
return trim($prefix, '/').'/'.$this->controllerId.'/'.$this->id;
103-
}
104-
return $this->controllerId.'/'.$this->id;
105-
}
106-
10798
public function getBaseModelName():string
10899
{
109100
return $this->modelFqn ? StringHelper::basename($this->modelFqn) : '';

0 commit comments

Comments
 (0)