diff --git a/src/lib/SchemaToDatabase.php b/src/lib/SchemaToDatabase.php index ccb230a3..beb27180 100644 --- a/src/lib/SchemaToDatabase.php +++ b/src/lib/SchemaToDatabase.php @@ -94,7 +94,7 @@ public function prepareModels(): array $openApi = $this->config->getOpenApi(); $junctions = $this->findJunctionSchemas(); - foreach ($openApi->components->schemas as $schemaName => $openApiSchema) { + foreach ($openApi->components->schemas ?? [] as $schemaName => $openApiSchema) { $schema = Yii::createObject(ComponentSchema::class, [$openApiSchema, $schemaName]); if (!$this->canGenerateModel($schemaName, $schema)) { @@ -153,7 +153,7 @@ public function findJunctionSchemas(): JunctionSchemas { $junctions = []; $openApi = $this->config->getOpenApi(); - foreach ($openApi->components->schemas as $schemaName => $openApiSchema) { + foreach ($openApi->components->schemas ?? [] as $schemaName => $openApiSchema) { /**@var ComponentSchema $schema */ $schema = Yii::createObject(ComponentSchema::class, [$openApiSchema, $schemaName]); if ($schema->isNonDb()) { diff --git a/src/lib/generators/ControllersGenerator.php b/src/lib/generators/ControllersGenerator.php index bd749c9f..ea0c73c4 100644 --- a/src/lib/generators/ControllersGenerator.php +++ b/src/lib/generators/ControllersGenerator.php @@ -59,7 +59,7 @@ public function generate():CodeFiles $controllerPath = $path; /** * @var RestAction|FractalAction $action - **/ + **/ $action = $actions[0]; if ($action->prefix && !empty($action->prefixSettings)) { $controllerNamespace = trim($action->prefixSettings['namespace'], '\\'); @@ -126,15 +126,31 @@ protected function makeCustomController( ]; $reflection->addMethod('checkAccess', $params, AbstractMemberGenerator::FLAG_PUBLIC, '//TODO implement checkAccess'); foreach ($abstractActions as $action) { + $responseHttpStatusCodes = ''; + foreach ($this->config->getOpenApi()->paths->getPaths()[$action->urlPath]->getOperations() as $verb => $operation) { + $codes = array_keys($operation->responses->getResponses()); + + $only200OrDefault = false; + if ($codes === [200] || $codes === ['default']) { + $only200OrDefault = true; + } + if (in_array('default', $codes) && in_array(200, $codes) && count($codes) === 2) { + $only200OrDefault = true; + } + + if ($verb === strtolower($action->requestMethod) && !$only200OrDefault) { + $responseHttpStatusCodes = implode(', ', $codes); + } + } + $params = array_map(static function ($param) { return ['name' => $param]; }, $action->getParamNames()); - $reflection->addMethod( $action->actionMethodName, $params, AbstractMemberGenerator::FLAG_PUBLIC, - '//TODO implement ' . $action->actionMethodName + '//TODO implement ' . $action->actionMethodName . ($responseHttpStatusCodes ? PHP_EOL . '// In order to conform with OpenAPI spec, response of this action must have one of the following HTTP status code: ' . $responseHttpStatusCodes : '') ); } $classFileGenerator->setClasses([$reflection]); diff --git a/tests/specs/blog_v2/controllers/CommentController.php b/tests/specs/blog_v2/controllers/CommentController.php index 0448ec72..c46307ed 100644 --- a/tests/specs/blog_v2/controllers/CommentController.php +++ b/tests/specs/blog_v2/controllers/CommentController.php @@ -18,6 +18,7 @@ public function actionListForPost($postId) public function actionCreateForPost($postId) { //TODO implement actionCreateForPost + // In order to conform with OpenAPI spec, response of this action must have one of the following HTTP status code: 201, default } public function actionViewForPost($slug, $id) @@ -28,6 +29,7 @@ public function actionViewForPost($slug, $id) public function actionDeleteForPost($slug, $id) { //TODO implement actionDeleteForPost + // In order to conform with OpenAPI spec, response of this action must have one of the following HTTP status code: 204 } public function actionUpdateForPost($slug, $id) diff --git a/tests/specs/issue_fix/163_generator_crash_when_using_reference_inside_an_object/pgsql/controllers/ContactController.php b/tests/specs/issue_fix/163_generator_crash_when_using_reference_inside_an_object/pgsql/controllers/ContactController.php index 10422b18..5f21cabb 100644 --- a/tests/specs/issue_fix/163_generator_crash_when_using_reference_inside_an_object/pgsql/controllers/ContactController.php +++ b/tests/specs/issue_fix/163_generator_crash_when_using_reference_inside_an_object/pgsql/controllers/ContactController.php @@ -13,11 +13,13 @@ public function checkAccess($action, $model = null, $params = []) public function actionListForAccount($accountId) { //TODO implement actionListForAccount + // In order to conform with OpenAPI spec, response of this action must have one of the following HTTP status code: 200, 403 } public function actionViewForAccount($accountId, $contactId) { //TODO implement actionViewForAccount + // In order to conform with OpenAPI spec, response of this action must have one of the following HTTP status code: 200, 403 } diff --git a/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/controllers/AccountController.php b/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/controllers/AccountController.php index b4044b8f..cfa4a6d3 100644 --- a/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/controllers/AccountController.php +++ b/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/controllers/AccountController.php @@ -13,6 +13,7 @@ public function checkAccess($action, $model = null, $params = []) public function actionView($id) { //TODO implement actionView + // In order to conform with OpenAPI spec, response of this action must have one of the following HTTP status code: 200, 404 } diff --git a/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.php b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.php new file mode 100644 index 00000000..9a6fa0f2 --- /dev/null +++ b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.php @@ -0,0 +1,13 @@ + '@specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.yaml', + 'generateUrls' => false, + 'generateModels' => true, + 'excludeModels' => [ + 'Error', + ], + 'generateControllers' => false, + 'generateMigrations' => false, + 'generateModelFaker' => true, // `generateModels` must be `true` in order to use `generateModelFaker` as `true` +]; diff --git a/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.yaml b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.yaml new file mode 100644 index 00000000..8f748b8e --- /dev/null +++ b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.3 + +info: + title: '#78' + version: 1.0.0 + +paths: + /: + get: + responses: + '200': + description: The Response + +components: + schemas: + Payment: + properties: + id: + type: integer + amount: + type: integer + readOnly: true + currency: + type: string diff --git a/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/BaseModelFaker.php b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/BaseModelFaker.php new file mode 100644 index 00000000..c367fbb4 --- /dev/null +++ b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/BaseModelFaker.php @@ -0,0 +1,144 @@ +faker = FakerFactory::create(str_replace('-', '_', \Yii::$app->language)); + $this->uniqueFaker = new UniqueGenerator($this->faker); + } + + abstract public function generateModel($attributes = []); + + public function getFaker():Generator + { + return $this->faker; + } + + public function getUniqueFaker():UniqueGenerator + { + return $this->uniqueFaker; + } + + public function setFaker(Generator $faker):void + { + $this->faker = $faker; + } + + public function setUniqueFaker(UniqueGenerator $faker):void + { + $this->uniqueFaker = $faker; + } + + /** + * Generate and return model + * @param array|callable $attributes + * @param UniqueGenerator|null $uniqueFaker + * @return \yii\db\ActiveRecord + * @example MyFaker::makeOne(['user_id' => 1, 'title' => 'foo']); + * @example MyFaker::makeOne( function($model, $faker) { + * $model->scenario = 'create'; + * $model->setAttributes(['user_id' => 1, 'title' => $faker->sentence]); + * return $model; + * }); + */ + public static function makeOne($attributes = [], ?UniqueGenerator $uniqueFaker = null) + { + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + $model = $fakeBuilder->generateModel($attributes); + return $model; + } + + /** + * Generate, save and return model + * @param array|callable $attributes + * @param UniqueGenerator|null $uniqueFaker + * @return \yii\db\ActiveRecord + * @example MyFaker::saveOne(['user_id' => 1, 'title' => 'foo']); + * @example MyFaker::saveOne( function($model, $faker) { + * $model->scenario = 'create'; + * $model->setAttributes(['user_id' => 1, 'title' => $faker->sentence]); + * return $model; + * }); + */ + public static function saveOne($attributes = [], ?UniqueGenerator $uniqueFaker = null) + { + $model = static::makeOne($attributes, $uniqueFaker); + $model->save(); + return $model; + } + + /** + * Generate and return multiple models + * @param int $number + * @param array|callable $commonAttributes + * @return \yii\db\ActiveRecord[]|array + * @example TaskFaker::make(5, ['project_id'=>1, 'user_id' => 2]); + * @example TaskFaker::make(5, function($model, $faker, $uniqueFaker) { + * $model->setAttributes(['name' => $uniqueFaker->username, 'state'=>$faker->boolean(20)]); + * return $model; + * }); + */ + public static function make(int $number, $commonAttributes = [], ?UniqueGenerator $uniqueFaker = null):array + { + if ($number < 1) { + return []; + } + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + return array_map(function () use ($commonAttributes, $fakeBuilder) { + $model = $fakeBuilder->generateModel($commonAttributes); + return $model; + }, range(0, $number -1)); + } + + /** + * Generate, save and return multiple models + * @param int $number + * @param array|callable $commonAttributes + * @return \yii\db\ActiveRecord[]|array + * @example TaskFaker::save(5, ['project_id'=>1, 'user_id' => 2]); + * @example TaskFaker::save(5, function($model, $faker, $uniqueFaker) { + * $model->setAttributes(['name' => $uniqueFaker->username, 'state'=>$faker->boolean(20)]); + * return $model; + * }); + */ + public static function save(int $number, $commonAttributes = [], ?UniqueGenerator $uniqueFaker = null):array + { + if ($number < 1) { + return []; + } + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + return array_map(function () use ($commonAttributes, $fakeBuilder) { + $model = $fakeBuilder->generateModel($commonAttributes); + $model->save(); + return $model; + }, range(0, $number -1)); + } +} diff --git a/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/Payment.php b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/Payment.php new file mode 100644 index 00000000..660670f4 --- /dev/null +++ b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/Payment.php @@ -0,0 +1,10 @@ +generateModels(['author_id' => 1]); + * $model = (new PostFaker())->generateModels(function($model, $faker, $uniqueFaker) { + * $model->scenario = 'create'; + * $model->author_id = 1; + * return $model; + * }); + **/ + public function generateModel($attributes = []) + { + $faker = $this->faker; + $uniqueFaker = $this->uniqueFaker; + $model = new Payment(); + //$model->id = $uniqueFaker->numberBetween(0, 1000000); + $model->amount = $faker->numberBetween(0, 1000000); + $model->currency = $faker->currencyCode; + if (!is_callable($attributes)) { + $model->setAttributes($attributes, false); + } else { + $model = $attributes($model, $faker, $uniqueFaker); + } + return $model; + } +} diff --git a/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/base/Payment.php b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/base/Payment.php new file mode 100644 index 00000000..129cce90 --- /dev/null +++ b/tests/specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql/models/base/Payment.php @@ -0,0 +1,31 @@ + [['currency'], 'trim'], + 'currency_string' => [['currency'], 'string'], + ]; + } +} diff --git a/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.php b/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.php new file mode 100644 index 00000000..cf88e27a --- /dev/null +++ b/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.php @@ -0,0 +1,13 @@ + '@specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.yml', + 'generateUrls' => false, + 'generateModels' => false, + 'excludeModels' => [ + 'Error', + ], + 'generateControllers' => true, + 'generateMigrations' => false, + 'generateModelFaker' => false, +]; diff --git a/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.yml b/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.yml new file mode 100644 index 00000000..df5b1314 --- /dev/null +++ b/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.yml @@ -0,0 +1,36 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: 79_response_status_codes_are_not_the_codes_defined_in_spec +paths: + /mango/cake: + get: + responses: + '200': + description: The information + '403': + description: The information + '404': + description: The information + post: + responses: + '201': + description: The information + '403': + description: The information + '404': + description: The information + '422': + description: The information + +components: + schemas: + Address: + type: object + description: desc + properties: + id: + type: integer + name: + type: string + maxLength: 64 diff --git a/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/mysql/controllers/MangoController.php b/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/mysql/controllers/MangoController.php new file mode 100644 index 00000000..46c40ad1 --- /dev/null +++ b/tests/specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/mysql/controllers/MangoController.php @@ -0,0 +1,27 @@ + [ + 'class' => \yii\rest\OptionsAction::class, + ], + ]; + } + + /** + * Checks the privilege of the current user. + * + * This method checks whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws \yii\web\ForbiddenHttpException if the user does not have access + */ + abstract public function checkAccess($action, $model = null, $params = []); + + abstract public function actionCake(); + + abstract public function actionCreateCake(); + +} diff --git a/tests/specs/issue_fix/96_component_schema_should_be_optional/index.php b/tests/specs/issue_fix/96_component_schema_should_be_optional/index.php new file mode 100644 index 00000000..34db291a --- /dev/null +++ b/tests/specs/issue_fix/96_component_schema_should_be_optional/index.php @@ -0,0 +1,13 @@ + '@specs/issue_fix/96_component_schema_should_be_optional/index.yml', + 'generateUrls' => false, + 'generateModels' => true, + 'excludeModels' => [ + 'Error', + ], + 'generateControllers' => false, + 'generateMigrations' => false, + 'generateModelFaker' => true, +]; diff --git a/tests/specs/issue_fix/96_component_schema_should_be_optional/index.yml b/tests/specs/issue_fix/96_component_schema_should_be_optional/index.yml new file mode 100644 index 00000000..5dcbc3cb --- /dev/null +++ b/tests/specs/issue_fix/96_component_schema_should_be_optional/index.yml @@ -0,0 +1,13 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: 96_component_schema_should_be_optional + +paths: + /: + get: + summary: List + operationId: list + responses: + '200': + description: The information diff --git a/tests/specs/issue_fix/96_component_schema_should_be_optional/mysql/models/BaseModelFaker.php b/tests/specs/issue_fix/96_component_schema_should_be_optional/mysql/models/BaseModelFaker.php new file mode 100644 index 00000000..c367fbb4 --- /dev/null +++ b/tests/specs/issue_fix/96_component_schema_should_be_optional/mysql/models/BaseModelFaker.php @@ -0,0 +1,144 @@ +faker = FakerFactory::create(str_replace('-', '_', \Yii::$app->language)); + $this->uniqueFaker = new UniqueGenerator($this->faker); + } + + abstract public function generateModel($attributes = []); + + public function getFaker():Generator + { + return $this->faker; + } + + public function getUniqueFaker():UniqueGenerator + { + return $this->uniqueFaker; + } + + public function setFaker(Generator $faker):void + { + $this->faker = $faker; + } + + public function setUniqueFaker(UniqueGenerator $faker):void + { + $this->uniqueFaker = $faker; + } + + /** + * Generate and return model + * @param array|callable $attributes + * @param UniqueGenerator|null $uniqueFaker + * @return \yii\db\ActiveRecord + * @example MyFaker::makeOne(['user_id' => 1, 'title' => 'foo']); + * @example MyFaker::makeOne( function($model, $faker) { + * $model->scenario = 'create'; + * $model->setAttributes(['user_id' => 1, 'title' => $faker->sentence]); + * return $model; + * }); + */ + public static function makeOne($attributes = [], ?UniqueGenerator $uniqueFaker = null) + { + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + $model = $fakeBuilder->generateModel($attributes); + return $model; + } + + /** + * Generate, save and return model + * @param array|callable $attributes + * @param UniqueGenerator|null $uniqueFaker + * @return \yii\db\ActiveRecord + * @example MyFaker::saveOne(['user_id' => 1, 'title' => 'foo']); + * @example MyFaker::saveOne( function($model, $faker) { + * $model->scenario = 'create'; + * $model->setAttributes(['user_id' => 1, 'title' => $faker->sentence]); + * return $model; + * }); + */ + public static function saveOne($attributes = [], ?UniqueGenerator $uniqueFaker = null) + { + $model = static::makeOne($attributes, $uniqueFaker); + $model->save(); + return $model; + } + + /** + * Generate and return multiple models + * @param int $number + * @param array|callable $commonAttributes + * @return \yii\db\ActiveRecord[]|array + * @example TaskFaker::make(5, ['project_id'=>1, 'user_id' => 2]); + * @example TaskFaker::make(5, function($model, $faker, $uniqueFaker) { + * $model->setAttributes(['name' => $uniqueFaker->username, 'state'=>$faker->boolean(20)]); + * return $model; + * }); + */ + public static function make(int $number, $commonAttributes = [], ?UniqueGenerator $uniqueFaker = null):array + { + if ($number < 1) { + return []; + } + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + return array_map(function () use ($commonAttributes, $fakeBuilder) { + $model = $fakeBuilder->generateModel($commonAttributes); + return $model; + }, range(0, $number -1)); + } + + /** + * Generate, save and return multiple models + * @param int $number + * @param array|callable $commonAttributes + * @return \yii\db\ActiveRecord[]|array + * @example TaskFaker::save(5, ['project_id'=>1, 'user_id' => 2]); + * @example TaskFaker::save(5, function($model, $faker, $uniqueFaker) { + * $model->setAttributes(['name' => $uniqueFaker->username, 'state'=>$faker->boolean(20)]); + * return $model; + * }); + */ + public static function save(int $number, $commonAttributes = [], ?UniqueGenerator $uniqueFaker = null):array + { + if ($number < 1) { + return []; + } + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + return array_map(function () use ($commonAttributes, $fakeBuilder) { + $model = $fakeBuilder->generateModel($commonAttributes); + $model->save(); + return $model; + }, range(0, $number -1)); + } +} diff --git a/tests/unit/IssueFixTest.php b/tests/unit/IssueFixTest.php index 08d1a00c..ff2ebc7d 100644 --- a/tests/unit/IssueFixTest.php +++ b/tests/unit/IssueFixTest.php @@ -1044,4 +1044,46 @@ public function test87ImplementForJsonInIsrefpointertoschema() $this->checkFiles($actualFiles, $expectedFiles); $this->runActualMigrations(); } + + // https://github.com/php-openapi/yii2-openapi/issues/78 + public function test78PropertiesThatAreMarkedAsReadonlyAreNotReadOnly() + { + $testFile = Yii::getAlias("@specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/index.php"); + $this->runGenerator($testFile); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/78_properties_that_are_marked_as_readonly_are_not_read_only/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + } + + // https://github.com/php-openapi/yii2-openapi/issues/79 + public function test79ResponseStatusCodesAreNotTheCodesDefinedInSpec() + { + $testFile = Yii::getAlias("@specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/index.php"); + $this->runGenerator($testFile); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/79_response_status_codes_are_not_the_codes_defined_in_spec/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + } + + // https://github.com/php-openapi/yii2-openapi/issues/96 + public function test96ComponentSchemaShouldBeOptional() + { + $testFile = Yii::getAlias("@specs/issue_fix/96_component_schema_should_be_optional/index.php"); + $this->runGenerator($testFile); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/96_component_schema_should_be_optional/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + } }