Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Commit 1dd5867

Browse files
authored
Resolve - Multiple PRs (#136)
- Add docs to setup this project locally + fix #98 + enhance docs - Enhance all code generation for x-db-type #119 . E.g. x-db-type should be reflected in model validation rules() + faker and other place where relevant. PR: SOHELAHMED7#17 - SOHELAHMED7#15
2 parents fa9a42a + 49265e2 commit 1dd5867

File tree

84 files changed

+2762
-125
lines changed

Some content is hidden

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

84 files changed

+2762
-125
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ installdocker:
5252
testdocker:
5353
docker-compose run --rm php sh -c 'vendor/bin/phpunit'
5454

55-
.PHONY: all check-style fix-style install test clean clean_all up cli installdocker migrate testdocker
55+
efs: clean_all up migrate # Everything From Scratch
56+
57+
.PHONY: all check-style fix-style install test clean clean_all up cli installdocker migrate testdocker efs
5658

5759

5860
# Docs:

README.md

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ if (YII_ENV_DEV) {
6060
'generators' => [
6161
// add ApiGenerator to Gii module
6262
'api' => \cebe\yii2openapi\generator\ApiGenerator::class,
63+
64+
// --------- OR ---------
65+
// to disable generation of migrations files or with default config change
66+
'api' => [
67+
'class' => \cebe\yii2openapi\generator\ApiGenerator::class
68+
'generateMigrations' => false # this config can also be applied in CLI command
69+
],
6370
],
6471
];
6572
}
@@ -71,7 +78,7 @@ To use the web generator, open `index.php?r=gii` and select the `REST API Genera
7178

7279
On console you can run the generator with `./yii gii/api --openApiPath=@app/openapi.yaml`. Where `@app/openapi.yaml` should be the absolute path to your OpenAPI spec file. This can be JSON as well as YAML (see also [cebe/php-openapi](https://github.com/cebe/php-openapi/) for supported formats).
7380

74-
Run `./yii gii/api --help` for all options.
81+
Run `./yii gii/api --help` for all options. Example: Disable generation of migrations files `./yii gii/api --generateMigrations=0`
7582

7683
See [Petstore example](https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/petstore.yaml) for example OpenAPI spec.
7784

@@ -198,11 +205,12 @@ created_at:
198205

199206
Also see: https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
200207

201-
### Many-to-Many relation definition
208+
209+
## Many-to-Many relation definition
202210

203211
There are two ways for define many-to-many relations:
204212

205-
#### Simple many-to-many without junction model
213+
### Simple many-to-many without junction model
206214

207215
- property name for many-to-many relation should be equal lower-cased, pluralized related schema name
208216

@@ -231,7 +239,7 @@ Tag:
231239
$ref: '#/components/schemas/Post'
232240
```
233241
234-
#### Many-to-many with junction model
242+
### Many-to-many with junction model
235243
236244
This way allowed creating multiple many-to-many relations between to models
237245
@@ -274,45 +282,7 @@ User:
274282
- see both examples here [tests/specs/many2many.yaml](tests/specs/many2many.yaml)
275283
276284
277-
## Assumptions
278-
279-
When generating code from an OpenAPI description there are many possible ways to achive a fitting result.
280-
Thus there are some assumptions and limitations that are currently applied to make this work.
281-
Here is a (possibly incomplete) list:
282-
283-
- The current implementation works best with OpenAPI description that follows the [JSON:API](https://jsonapi.org/) guidelines.
284-
- The request and response format/schema is currently not extracted from OpenAPI schema and may need to be adjusted manually if it does not follow JSON:API
285-
- column/field/property with name `id` is considered as Primary Key by this library and it is automatically handled by DB/Yii; so remove it from validation `rules()`
286-
- other fields can currently be used as primary keys using the `x-pk` OpenAPI extension (see below) but it may not be work correctly in all cases, please report bugs if you find them.
287-
288-
Other things to keep in mind:
289-
290-
### Adding columns to existing tables
291-
292-
When adding new fields in the API models, new migrations will be generated to add these fields to the table.
293-
For a project that is already in production, it should be considered to adjust the generated migration to add default
294-
values for existing data records.
295-
296-
One case where this is important is the addition of a new column with `NOT NULL` contraint, which does not provide a default value.
297-
Such a migration will fail when the table is not empty:
298-
299-
```php
300-
$this->addColumn('{{%company}}', 'name', $this->string(128)->notNull());
301-
```
302-
303-
Fails on a PostgreSQL database with
304-
305-
> add column name string(128) NOT NULL to table {{%company}} ...Exception: SQLSTATE[23502]: Not null violation: 7 ERROR: column "name" contains null values
306-
307-
The solution would be to create the column, allowing NULL, set the value to a default and add the null constraint later.
308-
309-
```php
310-
$this->addColumn('{{%company}}', 'name', $this->string(128)->null());
311-
$this->update('{{%company}}', ['name' => 'No name']);
312-
$this->alterColumn('{{%company}}', 'name', $this->string(128)->notNull());
313-
```
314-
315-
### Handling of `NOT NULL` constraints
285+
## Handling of `NOT NULL` constraints
316286
317287
`NOT NULL` in DB migrations is determined by `nullable` and `required` properties of the OpenAPI schema.
318288
e.g. attribute = 'my_property'.
@@ -360,7 +330,7 @@ e.g. attribute = 'my_property'.
360330
nullable: false
361331
```
362332

363-
### Handling of `enum` (#enum)
333+
## Handling of `enum` (#enum)
364334
It works on all 3 DB: MySQL, MariaDb and PgSQL.
365335

366336
```yaml
@@ -375,7 +345,7 @@ It works on all 3 DB: MySQL, MariaDb and PgSQL.
375345

376346
Note: Change in enum values are not very simple. For Mysql and Mariadb, migrations will be generated but in many cases custom modification in it are required. For Pgsql migrations for change in enum values will not be generated. It should be handled manually.
377347

378-
### Handling of `numeric` (#numeric, #MariaDb)
348+
## Handling of `numeric` (#numeric, #MariaDb)
379349

380350
precision-default = 10
381351
scale-default = 2
@@ -408,6 +378,44 @@ DB-Result = decimal(12,2)
408378
DB-Result = decimal(10,2)
409379

410380

381+
## Assumptions
382+
383+
When generating code from an OpenAPI description there are many possible ways to achive a fitting result.
384+
Thus there are some assumptions and limitations that are currently applied to make this work.
385+
Here is a (possibly incomplete) list:
386+
387+
- The current implementation works best with OpenAPI description that follows the [JSON:API](https://jsonapi.org/) guidelines.
388+
- The request and response format/schema is currently not extracted from OpenAPI schema and may need to be adjusted manually if it does not follow JSON:API
389+
- column/field/property with name `id` is considered as Primary Key by this library and it is automatically handled by DB/Yii; so remove it from validation `rules()`
390+
- other fields can currently be used as primary keys using the `x-pk` OpenAPI extension (see below) but it may not be work correctly in all cases, please report bugs if you find them.
391+
392+
Other things to keep in mind:
393+
394+
### Adding columns to existing tables
395+
396+
When adding new fields in the API models, new migrations will be generated to add these fields to the table.
397+
For a project that is already in production, it should be considered to adjust the generated migration to add default
398+
values for existing data records.
399+
400+
One case where this is important is the addition of a new column with `NOT NULL` contraint, which does not provide a default value.
401+
Such a migration will fail when the table is not empty:
402+
403+
```php
404+
$this->addColumn('{{%company}}', 'name', $this->string(128)->notNull());
405+
```
406+
407+
Fails on a PostgreSQL database with
408+
409+
> add column name string(128) NOT NULL to table {{%company}} ...Exception: SQLSTATE[23502]: Not null violation: 7 ERROR: column "name" contains null values
410+
411+
The solution would be to create the column, allowing NULL, set the value to a default and add the null constraint later.
412+
413+
```php
414+
$this->addColumn('{{%company}}', 'name', $this->string(128)->null());
415+
$this->update('{{%company}}', ['name' => 'No name']);
416+
$this->alterColumn('{{%company}}', 'name', $this->string(128)->notNull());
417+
```
418+
411419
## Screenshots
412420

413421
Gii Generator Form:
@@ -421,7 +429,25 @@ Generated files:
421429

422430
# Development
423431

424-
There commands are available to develop and check the tests. It is available inside the Docker container. To enter into bash shell of container, run `make cli` .
432+
To contribute or play around, steps to set up this project locally are:
433+
434+
```bash
435+
# in your CLI
436+
git clone https://github.com/cebe/yii2-openapi.git
437+
cd yii2-openapi
438+
make clean_all
439+
make up
440+
make cli
441+
composer install
442+
make migrate
443+
444+
# to check everything is setup up correctly ensure all tests passes
445+
./vendor/bin/phpunit
446+
447+
# create new branch from master and Happy contributing!
448+
```
449+
450+
These commands are available to develop and check the tests. It is available inside the Docker container. To enter into bash shell of container, run `make cli` .
425451

426452
```bash
427453
cd tests

src/lib/FakerStubResolver.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function resolve():?string
5151
case 'bool':
5252
return '$faker->boolean';
5353
case 'int':
54+
case 'integer':
5455
return $this->fakeForInt($limits['min'], $limits['max']);
5556
case 'string':
5657
return $this->fakeForString();
@@ -68,10 +69,17 @@ private function fakeForString():?string
6869
{
6970
$formats = [
7071
'date' => '$faker->dateTimeThisCentury->format(\'Y-m-d\')',
71-
'date-time' => '$faker->dateTimeThisYear(\'now\', \'UTC\')->format(DATE_ATOM)', // ISO-8601
72+
'date-time' => '$faker->dateTimeThisYear(\'now\', \'UTC\')->format(\'Y-m-d H:i:s\')', // DATE_ATOM=>ISO-8601
7273
'email' => '$faker->safeEmail',
74+
75+
// for x-db-type
76+
'datetime' => '$faker->dateTimeThisYear(\'now\', \'UTC\')->format(\'Y-m-d H:i:s\')', // DATE_ATOM=>ISO-8601
77+
'timestamp' => '$faker->dateTimeThisYear(\'now\', \'UTC\')->format(\'Y-m-d H:i:s\')', // DATE_ATOM=>ISO-8601
78+
'time' => '$faker->time(\'H:i:s\')',
79+
'year' => '$faker->year',
7380
];
7481
$format = $this->property->getAttr('format');
82+
$format = $format === null ? $this->property->getAttr('x-db-type') : $format;
7583
if ($format && isset($formats[$format])) {
7684
return $formats[$format];
7785
}
@@ -135,7 +143,11 @@ private function fakeForString():?string
135143
// TODO maybe also consider OpenAPI examples here
136144

137145
if ($size) {
138-
return 'substr($faker->text(' . $size . '), 0, ' . $size . ')';
146+
$method = 'text';
147+
if ($size < 5) {
148+
$method = 'word';
149+
}
150+
return 'substr($faker->'.$method.'(' . $size . '), 0, ' . $size . ')';
139151
}
140152
return '$faker->sentence';
141153
}

src/lib/ValidationRulesBuilder.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use cebe\yii2openapi\lib\items\Attribute;
1111
use cebe\yii2openapi\lib\items\DbModel;
1212
use cebe\yii2openapi\lib\items\ValidationRule;
13+
use yii\helpers\VarDumper;
14+
use yii\validators\DateValidator;
1315
use function count;
1416
use function implode;
1517
use function in_array;
@@ -89,18 +91,33 @@ private function resolveAttributeRules(Attribute $attribute):void
8991
if ($attribute->isReadOnly()) {
9092
return;
9193
}
92-
if ($attribute->phpType === 'bool') {
94+
if ($attribute->phpType === 'bool' || $attribute->phpType === 'boolean') {
9395
$this->rules[$attribute->columnName . '_boolean'] = new ValidationRule([$attribute->columnName], 'boolean');
96+
$this->defaultRule($attribute);
9497
return;
9598
}
9699

97100
if (in_array($attribute->dbType, ['time', 'date', 'datetime'], true)) {
98101
$key = $attribute->columnName . '_' . $attribute->dbType;
99-
$this->rules[$key] = new ValidationRule([$attribute->columnName], $attribute->dbType, []);
102+
$params = [];
103+
if ($attribute->dbType === 'date') {
104+
$params['format'] = 'php:Y-m-d';
105+
}
106+
if ($attribute->dbType === 'datetime') {
107+
$params['format'] = 'php:Y-m-d H:i:s';
108+
}
109+
if ($attribute->dbType === 'time') {
110+
$params['format'] = 'php:H:i:s';
111+
}
112+
113+
$this->rules[$key] = new ValidationRule([$attribute->columnName], $attribute->dbType, $params);
114+
$this->defaultRule($attribute);
100115
return;
101116
}
102-
if (in_array($attribute->phpType, ['int', 'double', 'float']) && !$attribute->isReference()) {
117+
118+
if (in_array($attribute->phpType, ['int', 'integer', 'double', 'float']) && !$attribute->isReference()) {
103119
$this->addNumericRule($attribute);
120+
$this->defaultRule($attribute);
104121
return;
105122
}
106123
if ($attribute->phpType === 'string' && !$attribute->isReference()) {
@@ -110,8 +127,10 @@ private function resolveAttributeRules(Attribute $attribute):void
110127
$key = $attribute->columnName . '_in';
111128
$this->rules[$key] =
112129
new ValidationRule([$attribute->columnName], 'in', ['range' => $attribute->enumValues]);
130+
$this->defaultRule($attribute);
113131
return;
114132
}
133+
$this->defaultRule($attribute);
115134
$this->addRulesByAttributeName($attribute);
116135
}
117136

@@ -137,7 +156,7 @@ private function addRulesByAttributeName(Attribute $attribute):void
137156
private function addExistRules(array $relations):void
138157
{
139158
foreach ($relations as $attribute) {
140-
if ($attribute->phpType === 'int') {
159+
if ($attribute->phpType === 'int' || $attribute->phpType === 'integer') {
141160
$this->addNumericRule($attribute);
142161
} elseif ($attribute->phpType === 'string') {
143162
$this->addStringRule($attribute);
@@ -167,6 +186,21 @@ private function addStringRule(Attribute $attribute):void
167186
$this->rules[$key] = new ValidationRule([$attribute->columnName], 'string', $params);
168187
}
169188

189+
private function defaultRule(Attribute $attribute):void
190+
{
191+
if ($attribute->defaultValue === null) {
192+
return;
193+
}
194+
if ($attribute->defaultValue instanceof \yii\db\Expression) {
195+
return;
196+
}
197+
198+
$params = [];
199+
$params['value'] = $attribute->defaultValue;
200+
$key = $attribute->columnName . '_default';
201+
$this->rules[$key] = new ValidationRule([$attribute->columnName], 'default', $params);
202+
}
203+
170204
private function addNumericRule(Attribute $attribute):void
171205
{
172206
$params = [];
@@ -176,7 +210,7 @@ private function addNumericRule(Attribute $attribute):void
176210
if ($attribute->limits['max'] !== null) {
177211
$params['max'] = $attribute->limits['max'];
178212
}
179-
$validator = $attribute->phpType === 'int' ? 'integer' : 'double';
213+
$validator = ($attribute->phpType === 'int' || $attribute->phpType === 'integer') ? 'integer' : 'double';
180214
$key = $attribute->columnName . '_' . $validator;
181215
$this->rules[$key] = new ValidationRule([$attribute->columnName], $validator, $params);
182216
}
@@ -206,7 +240,7 @@ private function prepareTypeScope():void
206240
continue;
207241
}
208242

209-
if (in_array($attribute->phpType, ['int', 'string', 'bool', 'float', 'double'])) {
243+
if (in_array($attribute->phpType, ['int', 'integer', 'string', 'bool', 'boolean', 'float', 'double'])) {
210244
continue;
211245
}
212246

src/lib/items/DbModel.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,17 @@ public function getValidationRules():string
9191
$rules = Yii::createObject(ValidationRulesBuilder::class, [$this])->build();
9292
$rules = array_map('strval', $rules);
9393
$rules = VarDumper::export($rules);
94-
return str_replace([PHP_EOL, "\'", "'[[", "]',"], [PHP_EOL . ' ', "'", '[[', '],'], $rules);
94+
return str_replace([
95+
PHP_EOL,
96+
"\'",
97+
"'[[",
98+
"]',"
99+
], [
100+
PHP_EOL . ' ',
101+
"'",
102+
'[[',
103+
'],'
104+
], $rules);
95105
}
96106

97107
/**

src/lib/items/RouteData.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,34 @@ public function init()
194194
'type' => $pathParameters[$paramName]->schema->type ?? null,
195195
//'model' => $additional ? SchemaResponseResolver::guessModelByRef($additional) : null,
196196
];
197+
if (isset($pathParameters[$paramName]->schema->pattern)) {
198+
$this->params[$paramName]['pattern'] = $pathParameters[$paramName]->schema->pattern;
199+
}
197200
} else {
198201
$this->params[$paramName] = null;
199202
}
200203

201204
$type = $this->params[$paramName]['type'] ?? null;
205+
$patternInSpec = $this->params[$paramName]['pattern'] ?? null;
206+
207+
$defaultIntRegex = '\d';
208+
$defaultStrRegex = '[\w-]';
209+
210+
$intRegex = $defaultIntRegex;
211+
$strRegex = $defaultStrRegex;
212+
213+
if ($patternInSpec !== null) {
214+
$patternInSpec = ltrim($patternInSpec, '^');
215+
$patternInSpec = rtrim($patternInSpec, '$');
216+
$intRegex = $patternInSpec;
217+
$strRegex = $patternInSpec;
218+
}
219+
202220
//check minimum/maximum for routes like <year:\d{4}> ?
203221
if ($type === 'integer') {
204-
$patternParts[$p] = '<' . $paramName . ':\d+>';
222+
$patternParts[$p] = '<' . $paramName . ':'.$intRegex.'+>';
205223
} elseif ($type === 'string') {
206-
$patternParts[$p] = '<' . $paramName . ':[\w-]+>';
224+
$patternParts[$p] = '<' . $paramName . ':'.$strRegex.'+>';
207225
} else {
208226
$patternParts[$p] = '<' . $paramName . '>';
209227
}

0 commit comments

Comments
 (0)