From 04fafdbc2c7b8da96a6714f7e848a76d99f8f429 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Tue, 9 Jun 2020 14:48:33 +0800 Subject: [PATCH 1/7] Typehint for ApiInterface --- src/ApiInterface.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ApiInterface.php b/src/ApiInterface.php index dfc22e3..cc62577 100644 --- a/src/ApiInterface.php +++ b/src/ApiInterface.php @@ -9,11 +9,17 @@ */ interface ApiInterface { + /** + * @return mixed + */ public static function parameters(); + /** + * @return mixed + */ public static function requestBody(); - public static function responses(); + public static function responses(): array; - public static function permissions(); + public static function permissions(): array; } From 24281517c0dc3ef00c07285f023617e874097a76 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Tue, 2 Mar 2021 15:17:32 +0800 Subject: [PATCH 2/7] Upgraded phpdocumentor/reflection-docblock --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 99f8872..5fb8891 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ ], "require": { "php": ">=7.0", - "phpdocumentor/reflection-docblock": "^4.3", + "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", "justinrainbow/json-schema": "^5.2" }, "require-dev": { From 265fd6b5500bbeca545583f37f11cdb3432fbb46 Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 1 Apr 2021 18:17:14 +0800 Subject: [PATCH 3/7] Improved InputValidator to convert int to boolean --- src/InputValidator.php | 7 ++- src/constraints/TypeConstraint.php | 21 ++++++++ tests/TypeTest.php | 77 ++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/constraints/TypeConstraint.php diff --git a/src/InputValidator.php b/src/InputValidator.php index 1614dc4..664f121 100644 --- a/src/InputValidator.php +++ b/src/InputValidator.php @@ -3,7 +3,9 @@ namespace rethink\typedphp; use JsonSchema\Constraints\Constraint; +use JsonSchema\Constraints\Factory; use JsonSchema\Validator; +use rethink\typedphp\constraints\TypeConstraint; /** * Class InputValidator @@ -37,8 +39,11 @@ protected function validateInternal($definition, $data, &$result) } $schema = $definition['schema']; + + $factory = new Factory(); + $factory->setConstraintClass('type', TypeConstraint::class); - $validator = new Validator(); + $validator = new Validator($factory); $result = $data[$definition['name']]; diff --git a/src/constraints/TypeConstraint.php b/src/constraints/TypeConstraint.php new file mode 100644 index 0000000..baf8a66 --- /dev/null +++ b/src/constraints/TypeConstraint.php @@ -0,0 +1,21 @@ + ['a' => '1'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => true], + ], + ], + [ + [ + 'query' => ['a' => '0'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => false], + ], + ], + [ + [ + 'query' => ['a' => 1], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => true], + ], + ], + [ + [ + 'query' => ['a' => 0], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => false], + ], + ], // missing required input field [ [ From 918bbc8f728d9e15d748e794c1b2c928c792329b Mon Sep 17 00:00:00 2001 From: Jin Hu Date: Thu, 1 Apr 2021 20:28:18 +0800 Subject: [PATCH 4/7] Fixed break string bools --- src/constraints/TypeConstraint.php | 4 ++-- tests/TypeTest.php | 38 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/constraints/TypeConstraint.php b/src/constraints/TypeConstraint.php index baf8a66..45cd833 100644 --- a/src/constraints/TypeConstraint.php +++ b/src/constraints/TypeConstraint.php @@ -10,9 +10,9 @@ class TypeConstraint extends \JsonSchema\Constraints\TypeConstraint { protected function toBoolean($value) { - if ($value == 1) { + if ($value === 1 || $value === '1') { return true; - } elseif ($value == 0) { + } elseif ($value === 0 || $value === '0') { return false; } else { return parent::toBoolean($value); diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 24f82c6..682f6c2 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -718,6 +718,44 @@ public function inputDataCases() 'query' => ['a' => false], ], ], + [ + [ + 'query' => ['a' => 'true'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => true], + ], + ], + [ + [ + 'query' => ['a' => 'false'], + ], + [ + [ + 'name' => 'a', + 'in' => 'query', + 'required' => true, + 'schema' => [ + 'type' => 'boolean', + ], + ], + ], + [], + [ + 'query' => ['a' => false], + ], + ], // missing required input field [ [ From 361a3524d259cfddaca1f96e419908d658f4efd2 Mon Sep 17 00:00:00 2001 From: Devon Liu Date: Wed, 21 Dec 2022 15:43:44 +0800 Subject: [PATCH 5/7] Speed up parser by cache parsed result --- src/TypeParser.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/TypeParser.php b/src/TypeParser.php index a745916..65aefae 100644 --- a/src/TypeParser.php +++ b/src/TypeParser.php @@ -314,16 +314,28 @@ protected function parseMap(string $definition): array protected function parseString($definition) { + static $cached = []; $newDefinition = trim($definition, '?'); + + $key = $definition; + if (is_subclass_of($newDefinition, MapType::class)) { + $key = $newDefinition; + } + + if (isset($cached[$key])) { + return $cached[$key]; + } + if (is_subclass_of($newDefinition, ProductType::class)) { - return $this->parseObject($definition); + $cached[$key] = $this->parseObject($definition); } elseif (is_subclass_of($newDefinition, SumType::class)) { - return $this->parseEnum($definition); + $cached[$key]= $this->parseEnum($definition); } elseif (is_subclass_of($newDefinition, MapType::class)) { - return $this->parseMap($newDefinition); + $cached[$key] = $this->parseMap($newDefinition); } else { - return $this->parseScalar($definition); + $cached[$key] = $this->parseScalar($definition); } + return $cached[$key]; } /** From 95d31bd02650b7625f18e76a16721ce845d13b1e Mon Sep 17 00:00:00 2001 From: devonliu02 Date: Wed, 10 Apr 2024 14:52:01 +0800 Subject: [PATCH 6/7] Support scope type security --- composer.json | 2 +- src/DocGenerator.php | 121 ++++++++++++++++++++++++++++++-- src/security/ScopeInterface.php | 14 ++++ src/security/SecurityScheme.php | 19 +++++ 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 src/security/ScopeInterface.php create mode 100644 src/security/SecurityScheme.php diff --git a/composer.json b/composer.json index 5fb8891..0d90f2a 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "php": ">=7.0", + "php": ">=7.1", "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", "justinrainbow/json-schema": "^5.2" }, diff --git a/src/DocGenerator.php b/src/DocGenerator.php index c10610f..01d0904 100644 --- a/src/DocGenerator.php +++ b/src/DocGenerator.php @@ -2,10 +2,12 @@ namespace rethink\typedphp; -use ReflectionClass; use InvalidArgumentException; -use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlockFactory; +use ReflectionClass; +use rethink\typedphp\security\ScopeInterface; +use const PREG_SPLIT_NO_EMPTY; /** * Class DocGenerator @@ -63,6 +65,7 @@ public function buildApiObject($apiClass) 'operationId' => $this->getStaticProperty($class, 'op'), 'parameters' => $parameters ? $this->parser->parse($parameters) : [], 'responses' => (object)$this->buildResponses($apiClass, $class), + 'security' => $this->buildSecurity($apiClass), ]; if ($bodyDefinition = $this->buildRequestBody($apiClass, $class)) { @@ -120,17 +123,75 @@ private function parseContentType(\ReflectionClass $class) $docblock = DocBlockFactory::createInstance()->create($comment); $tags = $docblock->getTagsByName('content-type'); if (count($tags)) { - return trim((string) $tags[0]->getDescription()); + return trim((string)$tags[0]->getDescription()); } } return null; } + /** + * @param $apiClass + * @return array + * @link https://spec.openapis.org/oas/v3.0.1#security-requirement-object + */ + protected function buildSecurity($apiClass): array + { + if (method_exists($apiClass, 'scopes') === false) { + return []; + } + + $scopes = $apiClass::scopes(); + + // supported format: + // 1. Scope + // 2. [Scope1, Scope2] AND relation + // 3. [[Scope1, Scope2], [Scope3, Scope4]] OR relation + + // only 1 scope + if ($scopes instanceof ScopeInterface) { + return [ + [ + $scopes->schemeName() => [ + $scopes->name(), + ], + ] + ]; + } + + if (is_array($scopes)) { + // multiple scopes with AND relation + if (current($scopes) instanceof ScopeInterface) { + return [ + $this->buildForAndScopes(...$scopes) + ]; + } + // multiple scopes with OR relation + if (is_array(current($scopes))) { + $security = []; + foreach ($scopes as $scope) { + $security[] = $this->buildForAndScopes(...$scope); + } + return $security; + } + } + + throw new InvalidArgumentException('Invalid scopes definition.'); + } + + protected function buildForAndScopes(ScopeInterface ...$scopes): array + { + $result = []; + foreach ($scopes as $scope) { + $result[$scope->schemeName()][] = $scope->name(); + } + return $result; + } + protected function buildResponses($apiClass, \ReflectionClass $class) { $responses = []; - foreach ($apiClass::responses() as $code => $responseDefinition) { + foreach ($apiClass::responses() as $code => $responseDefinition) { if ($responseDefinition !== null) { $responses[$code] = [ @@ -186,6 +247,58 @@ public function generate() return [ 'paths' => (object)$this->buildPathsObject(), 'schemas' => (object)$this->parser->getSchemas(), + 'securitySchemes' => (object)$this->buildSecuritySchemes(), ]; } + + /** + * @return array + * @link https://spec.openapis.org/oas/v3.0.1#security-scheme-object + */ + protected function buildSecuritySchemes(): array + { + $scopes = []; + foreach ($this->apiClasses as $apiClass) { + if (method_exists($apiClass, 'scopes')) { + $scopes[] = $this->collectScopes($apiClass::scopes()); + } + } + $scopes = array_merge([], ...$scopes); + + $securitySchemes = []; + /** @var ScopeInterface $scope */ + foreach ($scopes as $scope) { + if (isset($securitySchemes[$scope->schemeName()])) { + $securitySchemes[$scope->schemeName()]['flows']['authorizationCode']['scopes'][$scope->name()] = $scope->description(); + } else { + $securitySchemes[$scope->schemeName()] = [ + 'type' => 'oauth2', + 'flows' => [ + 'clientCredentials' => [ + 'tokenUrl' => '', + 'scopes' => [ + $scope->name() => $scope->description(), + ], + ], + ], + ]; + } + } + return $securitySchemes; + } + + protected function collectScopes($scopes): array + { + if ($scopes instanceof ScopeInterface) { + return [$scopes]; + } + if (is_array($scopes)) { + $result = []; + foreach ($scopes as $scope) { + $result[] = $this->collectScopes($scope); + } + return array_merge([], ...$result); + } + throw new InvalidArgumentException('Invalid scopes definition.'); + } } diff --git a/src/security/ScopeInterface.php b/src/security/ScopeInterface.php new file mode 100644 index 0000000..baa3670 --- /dev/null +++ b/src/security/ScopeInterface.php @@ -0,0 +1,14 @@ + Date: Thu, 11 Apr 2024 20:35:50 +0800 Subject: [PATCH 7/7] typo --- src/DocGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DocGenerator.php b/src/DocGenerator.php index 01d0904..691a312 100644 --- a/src/DocGenerator.php +++ b/src/DocGenerator.php @@ -269,7 +269,7 @@ protected function buildSecuritySchemes(): array /** @var ScopeInterface $scope */ foreach ($scopes as $scope) { if (isset($securitySchemes[$scope->schemeName()])) { - $securitySchemes[$scope->schemeName()]['flows']['authorizationCode']['scopes'][$scope->name()] = $scope->description(); + $securitySchemes[$scope->schemeName()]['flows']['clientCredentials']['scopes'][$scope->name()] = $scope->description(); } else { $securitySchemes[$scope->schemeName()] = [ 'type' => 'oauth2',