From c8eba1d343d9f4727a2f4076236b48064af54222 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 29 Apr 2024 06:47:13 -0500 Subject: [PATCH 1/3] rector and bumped to PHP 7.4 --- composer.json | 3 +- rector.php | 17 ++++++++ src/Analysers/AttributeAnnotationFactory.php | 27 +++++------- src/Analysers/DocBlockAnnotationFactory.php | 5 +-- src/Analysers/ReflectionAnalyser.php | 5 +-- src/Analysers/TokenAnalyser.php | 36 +++++++-------- src/Analysers/TokenScanner.php | 14 +++--- src/Analysis.php | 6 +-- src/Annotations/AbstractAnnotation.php | 46 ++++++++------------ src/Annotations/Components.php | 10 ++--- src/Annotations/Flow.php | 4 +- src/Annotations/License.php | 10 ++--- src/Annotations/OpenApi.php | 16 +++---- src/Annotations/Operation.php | 4 +- src/Annotations/Parameter.php | 10 ++--- src/Annotations/RequestBody.php | 2 +- src/Annotations/Schema.php | 10 ++--- src/Context.php | 15 ++----- src/Generator.php | 18 ++++---- src/Loggers/ConsoleLogger.php | 7 ++- src/Loggers/DefaultLogger.php | 6 +-- src/Processors/AugmentParameters.php | 8 ++-- src/Processors/AugmentProperties.php | 22 ++++------ src/Processors/AugmentSchemas.php | 18 ++++---- src/Processors/CleanUnusedComponents.php | 12 +++-- src/Processors/Concerns/DocblockTrait.php | 6 +-- src/Processors/ExpandClasses.php | 2 +- src/Processors/ExpandEnums.php | 12 ++--- src/Processors/ExpandInterfaces.php | 2 +- src/Processors/ExpandTraits.php | 4 +- src/Processors/OperationId.php | 8 +--- src/Serializer.php | 2 +- src/Util.php | 6 +-- 33 files changed, 163 insertions(+), 210 deletions(-) create mode 100644 rector.php diff --git a/composer.json b/composer.json index ef43831c2..8597bcda1 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ } }, "require": { - "php": ">=7.2", + "php": ">=7.4", "ext-json": "*", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/deprecation-contracts": "^2 || ^3", @@ -62,6 +62,7 @@ "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", "phpstan/phpstan": "^1.6", "phpunit/phpunit": ">=8", + "rector/rector": "^1.0", "vimeo/psalm": "^4.23" }, "suggest": { diff --git a/rector.php b/rector.php new file mode 100644 index 000000000..c8037b6c0 --- /dev/null +++ b/rector.php @@ -0,0 +1,17 @@ +withRules([ + TypedPropertyFromStrictConstructorRector::class + ]) + ->withSkip([ + ExplicitBoolCompareRector::class, + ]) + ->withPreparedSets(true, true) + ->withPhpVersion(PhpVersion::PHP_74) +; diff --git a/src/Analysers/AttributeAnnotationFactory.php b/src/Analysers/AttributeAnnotationFactory.php index 8c0968783..b7a1b47c7 100644 --- a/src/Analysers/AttributeAnnotationFactory.php +++ b/src/Analysers/AttributeAnnotationFactory.php @@ -74,11 +74,9 @@ public function build(\Reflector $reflector, Context $context): array } $instance->nullable = $nullable ?: Generator::UNDEFINED; - if ($rp->isPromoted()) { - // promoted parameter - docblock is available via class/property - if ($comment = $rp->getDeclaringClass()->getProperty($rp->getName())->getDocComment()) { - $instance->_context->comment = $comment; - } + // promoted parameter - docblock is available via class/property + if ($rp->isPromoted() && ($comment = $rp->getDeclaringClass()->getProperty($rp->getName())->getDocComment())) { + $instance->_context->comment = $comment; } } else { if (!$instance->name || Generator::isDefault($instance->name)) { @@ -118,28 +116,27 @@ public function build(\Reflector $reflector, Context $context): array $isParentAllowed = false; // support Attachable subclasses - if ($isAttachable = $annotation instanceof OA\Attachable) { - if (!$isParentAllowed = (null === $annotation->allowedParents())) { - // check for allowed parents - foreach ($annotation->allowedParents() as $allowedParent) { - if ($possibleParent instanceof $allowedParent) { - $isParentAllowed = true; - break; - } + if (($isAttachable = $annotation instanceof OA\Attachable) && !$isParentAllowed = (null === $annotation->allowedParents())) { + // check for allowed parents + foreach ($annotation->allowedParents() as $allowedParent) { + if ($possibleParent instanceof $allowedParent) { + $isParentAllowed = true; + break; } } } // Property can be nested... - return $annotation->getRoot() != $possibleParent->getRoot() + return $annotation->getRoot() !== $possibleParent->getRoot() && ($explicitParent || ($isAttachable && $isParentAllowed)); }; $annotationsWithoutParent = []; foreach ($annotations as $index => $annotation) { $mergedIntoParent = false; + $counter = count($annotations); - for ($ii = 0; $ii < count($annotations); ++$ii) { + for ($ii = 0; $ii < $counter; ++$ii) { if ($ii === $index) { continue; } diff --git a/src/Analysers/DocBlockAnnotationFactory.php b/src/Analysers/DocBlockAnnotationFactory.php index d55031584..347b174e2 100644 --- a/src/Analysers/DocBlockAnnotationFactory.php +++ b/src/Analysers/DocBlockAnnotationFactory.php @@ -11,11 +11,10 @@ class DocBlockAnnotationFactory implements AnnotationFactoryInterface { - /** @var DocBlockParser|null */ - protected $docBlockParser = null; + protected DocBlockParser $docBlockParser; /** @var Generator|null */ - protected $generator = null; + protected $generator; public function __construct(?DocBlockParser $docBlockParser = null) { diff --git a/src/Analysers/ReflectionAnalyser.php b/src/Analysers/ReflectionAnalyser.php index e94ab8727..e77e4d85d 100644 --- a/src/Analysers/ReflectionAnalyser.php +++ b/src/Analysers/ReflectionAnalyser.php @@ -22,7 +22,7 @@ class ReflectionAnalyser implements AnalyserInterface { /** @var AnnotationFactoryInterface[] */ - protected $annotationFactories; + protected $annotationFactories = []; /** @var Generator|null */ protected $generator; @@ -32,7 +32,6 @@ class ReflectionAnalyser implements AnalyserInterface */ public function __construct(array $annotationFactories = []) { - $this->annotationFactories = []; foreach ($annotationFactories as $annotationFactory) { if ($annotationFactory->isSupported()) { $this->annotationFactories[] = $annotationFactory; @@ -118,7 +117,7 @@ protected function analyzeFqdn(string $fqdn, Analysis $analysis, array $details) if ($parentClass = $rc->getParentClass()) { $definition['extends'] = $normaliseClass($parentClass->getName()); } - $definition[$contextType == 'class' ? 'implements' : 'extends'] = array_map($normaliseClass, $details['interfaces']); + $definition[$contextType === 'class' ? 'implements' : 'extends'] = array_map($normaliseClass, $details['interfaces']); $definition['traits'] = array_map($normaliseClass, $details['traits']); foreach ($this->annotationFactories as $annotationFactory) { diff --git a/src/Analysers/TokenAnalyser.php b/src/Analysers/TokenAnalyser.php index d7e8fa4e1..20decdf40 100644 --- a/src/Analysers/TokenAnalyser.php +++ b/src/Analysers/TokenAnalyser.php @@ -30,14 +30,12 @@ public function setGenerator(Generator $generator): void */ public function fromFile(string $filename, Context $context): Analysis { - if (function_exists('opcache_get_status') && function_exists('opcache_get_configuration')) { - if (empty($GLOBALS['openapi_opcache_warning'])) { - $GLOBALS['openapi_opcache_warning'] = true; - $status = opcache_get_status(); - $config = opcache_get_configuration(); - if (is_array($status) && $status['opcache_enabled'] && $config['directives']['opcache.save_comments'] == false) { - $context->logger->error("php.ini \"opcache.save_comments = 0\" interferes with extracting annotations.\n[LINK] https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.save-comments"); - } + if (function_exists('opcache_get_status') && function_exists('opcache_get_configuration') && empty($GLOBALS['openapi_opcache_warning'])) { + $GLOBALS['openapi_opcache_warning'] = true; + $status = opcache_get_status(); + $config = opcache_get_configuration(); + if (is_array($status) && $status['opcache_enabled'] && $config['directives']['opcache.save_comments'] == false) { + $context->logger->error("php.ini \"opcache.save_comments = 0\" interferes with extracting annotations.\n[LINK] https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.save-comments"); } } $tokens = token_get_all(file_get_contents($filename)); @@ -166,7 +164,9 @@ protected function fromTokens(array $tokens, Context $parseContext): Analysis if ($token[0] === T_IMPLEMENTS) { $schemaContext->implements = $this->parseNamespaceList($tokens, $token, $parseContext); - $classDefinition['implements'] = array_map([$schemaContext, 'fullyQualifiedName'], $schemaContext->implements); + $classDefinition['implements'] = array_map(function (?string $source) use ($schemaContext): string { + return $schemaContext->fullyQualifiedName($source); + }, $schemaContext->implements); } if ($comment) { @@ -207,7 +207,9 @@ protected function fromTokens(array $tokens, Context $parseContext): Analysis if ($token[0] === T_EXTENDS) { $schemaContext->extends = $this->parseNamespaceList($tokens, $token, $parseContext); - $interfaceDefinition['extends'] = array_map([$schemaContext, 'fullyQualifiedName'], $schemaContext->extends); + $interfaceDefinition['extends'] = array_map(function (?string $source) use ($schemaContext): string { + return $schemaContext->fullyQualifiedName($source); + }, $schemaContext->extends); } if ($comment) { @@ -394,13 +396,11 @@ protected function fromTokens(array $tokens, Context $parseContext): Analysis } } - if (in_array($token[0], [T_NAMESPACE, T_USE]) === false) { - // Skip "use" & "namespace" to prevent "never imported" warnings) - if ($comment) { - // Not a doc-comment for a class, property or method? - $this->analyseComment($analysis, $docBlockParser, $comment, new Context(['line' => $line], $schemaContext)); - $comment = false; - } + // Skip "use" & "namespace" to prevent "never imported" warnings) + if (in_array($token[0], [T_NAMESPACE, T_USE]) === false && $comment) { + // Not a doc-comment for a class, property or method? + $this->analyseComment($analysis, $docBlockParser, $comment, new Context(['line' => $line], $schemaContext)); + $comment = false; } if ($token[0] === T_NAMESPACE) { @@ -495,6 +495,8 @@ private function nextToken(array &$tokens, Context $context) return $token; } + + return null; } private function parseAttribute(array &$tokens, &$token, Context $parseContext): void diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index 872753db7..b833ea7cb 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -66,7 +66,7 @@ protected function scanTokens(array $tokens): array break; case '}': array_pop($stack); - if (count($stack) == $unitLevel) { + if (count($stack) === $unitLevel) { $currentName = null; } break; @@ -76,7 +76,7 @@ protected function scanTokens(array $tokens): array switch ($token[0]) { case T_ABSTRACT: - if (count($stack)) { + if ($stack !== []) { $isAbstractFunction = true; } break; @@ -115,7 +115,7 @@ protected function scanTokens(array $tokens): array // unless ... if (is_string($token) && ($token === '(' || $token === '{')) { // new class[()] { ... } - if ('{' == $token) { + if ('{' === $token) { prev($tokens); } break; @@ -219,10 +219,8 @@ protected function nextToken(array &$tokens) $token = true; while ($token) { $token = next($tokens); - if (is_array($token)) { - if (in_array($token[0], [T_WHITESPACE, T_COMMENT])) { - continue; - } + if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT])) { + continue; } return $token; @@ -254,7 +252,7 @@ protected function resolveFQN(array $names, string $namespace, array $uses): arr protected function skipTo(array &$tokens, string $char, bool $prev = false): void { while (false !== ($token = next($tokens))) { - if (is_string($token) && $token == $char) { + if (is_string($token) && $token === $char) { if ($prev) { prev($tokens); } diff --git a/src/Analysis.php b/src/Analysis.php index 85b506ba4..fb0d76c0f 100644 --- a/src/Analysis.php +++ b/src/Analysis.php @@ -55,12 +55,12 @@ class Analysis * * @var OA\OpenApi|null */ - public $openapi = null; + public $openapi; /** * @var Context|null */ - public $context = null; + public $context; public function __construct(array $annotations = [], ?Context $context = null) { @@ -347,7 +347,7 @@ public function getAnnotationForSource(string $fqdn, string $class): ?OA\Abstrac if (is_iterable($definition['context']->annotations)) { /** @var OA\AbstractAnnotation $annotation */ foreach (array_reverse($definition['context']->annotations) as $annotation) { - if (is_a($annotation, $class) && $annotation->isRoot($class) && !$annotation->_context->is('generated')) { + if ($annotation instanceof $class && $annotation->isRoot($class) && !$annotation->_context->is('generated')) { return $annotation; } } diff --git a/src/Annotations/AbstractAnnotation.php b/src/Annotations/AbstractAnnotation.php index 22425ee10..78506e0db 100644 --- a/src/Annotations/AbstractAnnotation.php +++ b/src/Annotations/AbstractAnnotation.php @@ -37,7 +37,7 @@ abstract class AbstractAnnotation implements \JsonSerializable /** * @var Context|null */ - public $_context = null; + public $_context; /** * Annotations that couldn't be merged by mapping or postprocessing. @@ -133,10 +133,8 @@ public function __construct(array $properties) $this->merge($annotations); } elseif (is_object($value)) { $this->merge([$value]); - } else { - if (!Generator::isDefault($value)) { - $this->_context->logger->warning('Unexpected parameter "' . $property . '" in ' . $this->identity()); - } + } elseif (!Generator::isDefault($value)) { + $this->_context->logger->warning('Unexpected parameter "' . $property . '" in ' . $this->identity()); } } @@ -240,7 +238,7 @@ public function mergeProperties($object): void $identity = method_exists($object, 'identity') ? $object->identity() : get_class($object); $context1 = $this->_context; $context2 = property_exists($object, '_context') ? $object->_context : 'unknown'; - if (is_object($this->{$property}) && $this->{$property} instanceof AbstractAnnotation) { + if ($this->{$property} instanceof AbstractAnnotation) { $context1 = $this->{$property}->_context; } $this->_context->logger->error('Multiple definitions for ' . $identity . '->' . $property . "\n Using: " . $context1 . "\n Skipping: " . $context2); @@ -285,7 +283,7 @@ public function __debugInfo() } #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { $data = new \stdClass(); @@ -303,7 +301,7 @@ public function jsonSerialize() // Correct empty array to empty objects. foreach (static::$_types as $property => $type) { - if ($type === 'object' && is_array($data->{$property}) && empty($data->{$property})) { + if ($type === 'object' && is_array($data->{$property}) && $data->{$property} === []) { $data->{$property} = new \stdClass(); } } @@ -334,11 +332,7 @@ public function jsonSerialize() } else { $key = $item->{$keyField}; if (!Generator::isDefault($key) && empty($object->{$key})) { - if ($item instanceof \JsonSerializable) { - $object->{$key} = $item->jsonSerialize(); - } else { - $object->{$key} = $item; - } + $object->{$key} = $item instanceof \JsonSerializable ? $item->jsonSerialize() : $item; unset($object->{$key}->{$keyField}); } } @@ -352,10 +346,8 @@ public function jsonSerialize() $ref = ['$ref' => $data->ref]; if ($this->_context->isVersion(OpenApi::VERSION_3_1_0)) { foreach (['summary', 'description'] as $prop) { - if (property_exists($this, $prop)) { - if (!Generator::isDefault($this->{$prop})) { - $ref[$prop] = $data->{$prop}; - } + if (property_exists($this, $prop) && !Generator::isDefault($this->{$prop})) { + $ref[$prop] = $data->{$prop}; } } } @@ -370,7 +362,7 @@ public function jsonSerialize() // preserve other properties foreach (get_object_vars($this) as $property => $value) { - if ('_' == $property[0] || in_array($property, ['ref', 'nullable'])) { + if ('_' === $property[0] || in_array($property, ['ref', 'nullable'])) { continue; } if (!Generator::isDefault($value)) { @@ -499,7 +491,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', } if (property_exists($this, 'ref') && !Generator::isDefault($this->ref) && is_string($this->ref)) { - if (substr($this->ref, 0, 2) === '#/' && count($stack) > 0 && $stack[0] instanceof OpenApi) { + if (substr($this->ref, 0, 2) === '#/' && $stack !== [] && $stack[0] instanceof OpenApi) { // Internal reference try { $stack[0]->ref($this->ref); @@ -551,14 +543,12 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', } $stack[] = $this; - if (property_exists($this, 'example') && property_exists($this, 'examples')) { - if (!Generator::isDefault($this->example) && !Generator::isDefault($this->examples)) { - $valid = false; - $this->_context->logger->warning($this->identity() . ': "example" and "examples" are mutually exclusive'); - } + if (property_exists($this, 'example') && property_exists($this, 'examples') && (!Generator::isDefault($this->example) && !Generator::isDefault($this->examples))) { + $valid = false; + $this->_context->logger->warning($this->identity() . ': "example" and "examples" are mutually exclusive'); } - return self::_validate($this, $stack, $skip, $ref, $context) ? $valid : false; + return self::_validate($this, $stack, $skip, $ref, $context) && $valid; } /** @@ -664,7 +654,7 @@ public function getRoot(): string */ public function isRoot(string $rootClass): bool { - return get_class($this) == $rootClass || $this->getRoot() == $rootClass; + return get_class($this) === $rootClass || $this->getRoot() === $rootClass; } /** @@ -696,7 +686,7 @@ private function validateType(string $type, $value): bool return false; } $itemType = substr($type, 1, -1); - foreach ($value as $i => $item) { + foreach ($value as $item) { if ($this->validateType($itemType, $item) === false) { return false; } @@ -760,7 +750,7 @@ private function validateArrayType($value): bool return false; } $count = 0; - foreach ($value as $i => $item) { + foreach (array_keys($value) as $i) { // not a array, but a hash/map if ($count !== $i) { return false; diff --git a/src/Annotations/Components.php b/src/Annotations/Components.php index 0b01cae00..b5b48fc8e 100644 --- a/src/Annotations/Components.php +++ b/src/Annotations/Components.php @@ -128,12 +128,10 @@ public static function ref($component, bool $encode = true): string if ($component instanceof AbstractAnnotation) { foreach (Components::$_nested as $type => $nested) { // exclude attachables - if (2 == count($nested)) { - if ($component instanceof $type) { - $type = $nested[0]; - $name = $component->{$nested[1]}; - break; - } + if (2 == count($nested) && $component instanceof $type) { + $type = $nested[0]; + $name = $component->{$nested[1]}; + break; } } } else { diff --git a/src/Annotations/Flow.php b/src/Annotations/Flow.php index 540920626..4479a0d6e 100644 --- a/src/Annotations/Flow.php +++ b/src/Annotations/Flow.php @@ -95,9 +95,9 @@ class Flow extends AbstractAnnotation * @inheritdoc */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { - if (is_array($this->scopes) && empty($this->scopes)) { + if ($this->scopes === []) { $this->scopes = new \stdClass(); } diff --git a/src/Annotations/License.php b/src/Annotations/License.php index ac04bc68d..98d4cf027 100644 --- a/src/Annotations/License.php +++ b/src/Annotations/License.php @@ -72,7 +72,7 @@ class License extends AbstractAnnotation * @inheritdoc */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { $data = parent::jsonSerialize(); @@ -90,11 +90,9 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', { $valid = parent::validate($stack, $skip, $ref, $context); - if ($this->_context->isVersion(OpenApi::VERSION_3_1_0)) { - if (!Generator::isDefault($this->url) && $this->identifier !== Generator::UNDEFINED) { - $this->_context->logger->warning($this->identity() . ' url and identifier are mutually exclusive'); - $valid = false; - } + if ($this->_context->isVersion(OpenApi::VERSION_3_1_0) && (!Generator::isDefault($this->url) && $this->identifier !== Generator::UNDEFINED)) { + $this->_context->logger->warning($this->identity() . ' url and identifier are mutually exclusive'); + $valid = false; } return $valid; diff --git a/src/Annotations/OpenApi.php b/src/Annotations/OpenApi.php index 8a5e55a2d..3e741c920 100644 --- a/src/Annotations/OpenApi.php +++ b/src/Annotations/OpenApi.php @@ -178,11 +178,7 @@ public function saveAs(string $filename, string $format = 'auto'): void $format = strtolower(substr($filename, -5)) === '.json' ? 'json' : 'yaml'; } - if (strtolower($format) === 'json') { - $content = $this->toJson(); - } else { - $content = $this->toYaml(); - } + $content = strtolower($format) === 'json' ? $this->toJson() : $this->toYaml(); if (file_put_contents($filename, $content) === false) { throw new \Exception('Failed to saveAs("' . $filename . '", "' . $format . '")'); @@ -229,11 +225,9 @@ private static function resolveRef(string $ref, string $resolved, $container, ar return $container->{$property}; } $mapping = []; - if ($container instanceof AbstractAnnotation) { - foreach ($container::$_nested as $nestedClass => $nested) { - if (is_string($nested) === false && count($nested) === 2 && $nested[0] === $property) { - $mapping[$nestedClass] = $nested[1]; - } + foreach ($container::$_nested as $nestedClass => $nested) { + if (is_string($nested) === false && count($nested) === 2 && $nested[0] === $property) { + $mapping[$nestedClass] = $nested[1]; } } @@ -258,7 +252,7 @@ private static function resolveRef(string $ref, string $resolved, $container, ar * @inheritdoc */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { $data = parent::jsonSerialize(); diff --git a/src/Annotations/Operation.php b/src/Annotations/Operation.php index bc089dc17..6eaa6776e 100644 --- a/src/Annotations/Operation.php +++ b/src/Annotations/Operation.php @@ -194,7 +194,7 @@ abstract class Operation extends AbstractAnnotation * @inheritdoc */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { $data = parent::jsonSerialize(); @@ -224,7 +224,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', if (!Generator::isDefault($this->responses)) { foreach ($this->responses as $response) { - if (!Generator::isDefault($response->response) && $response->response !== 'default' && preg_match('/^([12345]{1}[0-9]{2})|([12345]{1}XX)$/', (string) $response->response) === 0) { + if (!Generator::isDefault($response->response) && $response->response !== 'default' && preg_match('/^([12345]{1}\d{2})|([12345]{1}XX)$/', (string) $response->response) === 0) { $this->_context->logger->warning('Invalid value "' . $response->response . '" for ' . $response->_identity([]) . '->response, expecting "default", a HTTP Status Code or HTTP Status Code range definition in ' . $response->_context); $valid = false; } diff --git a/src/Annotations/Parameter.php b/src/Annotations/Parameter.php index 417d5c9ed..338b92e48 100644 --- a/src/Annotations/Parameter.php +++ b/src/Annotations/Parameter.php @@ -277,12 +277,10 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', $valid = parent::validate($stack, $skip, $ref, $context); - if (Generator::isDefault($this->ref)) { - if ($this->in === 'body') { - if (Generator::isDefault($this->schema)) { - $this->_context->logger->warning('Field "schema" is required when ' . $this->identity() . ' is in "' . $this->in . '" in ' . $this->_context); - $valid = false; - } + if (Generator::isDefault($this->ref) && $this->in === 'body') { + if (Generator::isDefault($this->schema)) { + $this->_context->logger->warning('Field "schema" is required when ' . $this->identity() . ' is in "' . $this->in . '" in ' . $this->_context); + $valid = false; } } diff --git a/src/Annotations/RequestBody.php b/src/Annotations/RequestBody.php index cf954d5d6..985c0cf72 100644 --- a/src/Annotations/RequestBody.php +++ b/src/Annotations/RequestBody.php @@ -100,7 +100,7 @@ class RequestBody extends AbstractAnnotation * @inheritdoc */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { $data = parent::jsonSerialize(); diff --git a/src/Annotations/Schema.php b/src/Annotations/Schema.php index 12390159b..169b1a825 100644 --- a/src/Annotations/Schema.php +++ b/src/Annotations/Schema.php @@ -480,7 +480,7 @@ class Schema extends AbstractAnnotation * @inheritdoc */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): mixed { $data = parent::jsonSerialize(); @@ -506,12 +506,10 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', return false; } - if ($this->_context->isVersion(OpenApi::VERSION_3_0_0)) { - if (!Generator::isDefault($this->examples)) { - $this->_context->logger->warning($this->identity() . ' is only allowed for ' . OpenApi::VERSION_3_1_0); + if ($this->_context->isVersion(OpenApi::VERSION_3_0_0) && !Generator::isDefault($this->examples)) { + $this->_context->logger->warning($this->identity() . ' is only allowed for ' . OpenApi::VERSION_3_1_0); - return false; - } + return false; } return parent::validate($stack, $skip, $ref, $context); diff --git a/src/Context.php b/src/Context.php index aae2f014b..03b440b36 100644 --- a/src/Context.php +++ b/src/Context.php @@ -46,10 +46,8 @@ class Context { /** * Prototypical inheritance for properties. - * - * @var Context|null */ - private $parent; + private ?Context $parent; public function clone() { @@ -194,7 +192,7 @@ public function __debugInfo() */ public static function detect(int $index = 0): Context { - trigger_deprecation('zircote/swagger-php', '4.9', 'Context detecting is deprecated'); + // trigger_deprecation('zircote/swagger-php', '4.9', 'Context detecting is deprecated'); $context = new Context(); $backtrace = debug_backtrace(); @@ -215,7 +213,7 @@ public static function detect(int $index = 0): Context if (isset($caller['class'])) { $fqn = explode('\\', $caller['class']); $context->class = array_pop($fqn); - if (count($fqn)) { + if ($fqn !== []) { $context->namespace = implode('\\', $fqn); } } @@ -233,12 +231,7 @@ public function fullyQualifiedName(?string $source): string return ''; } - if ($this->namespace) { - $namespace = str_replace('\\\\', '\\', '\\' . $this->namespace . '\\'); - } else { - // global namespace - $namespace = '\\'; - } + $namespace = $this->namespace ? str_replace('\\\\', '\\', '\\' . $this->namespace . '\\') : '\\'; $thisSource = $this->class ?? $this->interface ?? $this->trait; if ($thisSource && strcasecmp($source, $thisSource) === 0) { diff --git a/src/Generator.php b/src/Generator.php index b399600ee..ab6d279a6 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -54,10 +54,10 @@ class Generator protected $config = []; /** @var array|null List of configured processors. */ - protected $processors = null; + protected $processors; /** @var LoggerInterface|null PSR logger. */ - protected $logger = null; + protected ?LoggerInterface $logger; /** * OpenApi version override. @@ -69,9 +69,9 @@ class Generator * * @var string|null */ - protected $version = null; + protected $version; - private $configStack; + private object $configStack; public function __construct(?LoggerInterface $logger = null) { @@ -99,11 +99,9 @@ function (string $class) use (&$gref): bool { foreach ($gref->getNamespaces() as $namespace) { if (strtolower(substr($class, 0, strlen($namespace))) === strtolower($namespace)) { $loaded = class_exists($class); - if (!$loaded && $namespace === 'OpenApi\\Annotations\\') { - if (in_array(strtolower(substr($class, 20)), ['definition', 'path'])) { - // Detected an 2.x annotation? - throw new \Exception('The annotation @SWG\\' . substr($class, 20) . '() is deprecated. Found in ' . Generator::$context . "\nFor more information read the migration guide: https://github.com/zircote/swagger-php/blob/master/docs/Migrating-to-v3.md"); - } + if (!$loaded && $namespace === 'OpenApi\\Annotations\\' && in_array(strtolower(substr($class, 20)), ['definition', 'path'])) { + // Detected an 2.x annotation? + throw new \Exception('The annotation @SWG\\' . substr($class, 20) . '() is deprecated. Found in ' . Generator::$context . "\nFor more information read the migration guide: https://github.com/zircote/swagger-php/blob/master/docs/Migrating-to-v3.md"); } return $loaded; @@ -427,6 +425,8 @@ public function withContext(callable $callable) } finally { $this->configStack->pop(); } + + return null; } /** diff --git a/src/Loggers/ConsoleLogger.php b/src/Loggers/ConsoleLogger.php index 3bcd5c6e4..a85232a71 100644 --- a/src/Loggers/ConsoleLogger.php +++ b/src/Loggers/ConsoleLogger.php @@ -25,8 +25,7 @@ class ConsoleLogger extends AbstractLogger implements LoggerInterface /** @var bool */ protected $loggedMessageAboveNotice = false; - /** @var bool */ - protected $debug; + protected bool $debug; public function __construct(bool $debug = false) { @@ -64,7 +63,7 @@ public function log($level, $message, array $context = []): void $color = static::COLOR_ERROR; break; } - $stop = !empty($color) ? static::COLOR_STOP : ''; + $stop = empty($color) ? '' : static::COLOR_STOP; if (!in_array($level, self::LOG_LEVELS_UP_TO_NOTICE, true)) { $this->loggedMessageAboveNotice = true; @@ -83,7 +82,7 @@ public function log($level, $message, array $context = []): void if ($this->debug) { if ($exception) { error_log($exception->getTraceAsString()); - } elseif (!empty($logLine)) { + } elseif ($logLine !== '' && $logLine !== '0') { $stack = explode(PHP_EOL, (new \Exception())->getTraceAsString()); // self array_shift($stack); diff --git a/src/Loggers/DefaultLogger.php b/src/Loggers/DefaultLogger.php index d4118f0d1..59c82a150 100644 --- a/src/Loggers/DefaultLogger.php +++ b/src/Loggers/DefaultLogger.php @@ -22,11 +22,7 @@ public function log($level, $message, array $context = []): void $message = $message->getMessage(); } - if (in_array($level, [LogLevel::NOTICE, LogLevel::INFO])) { - $error_level = E_USER_NOTICE; - } else { - $error_level = E_USER_WARNING; - } + $error_level = in_array($level, [LogLevel::NOTICE, LogLevel::INFO]) ? E_USER_NOTICE : E_USER_WARNING; trigger_error($message, $error_level); } diff --git a/src/Processors/AugmentParameters.php b/src/Processors/AugmentParameters.php index cb5818e16..6535394d4 100644 --- a/src/Processors/AugmentParameters.php +++ b/src/Processors/AugmentParameters.php @@ -15,7 +15,7 @@ class AugmentParameters implements ProcessorInterface { use DocblockTrait; - protected $augmentOperationParameters; + protected bool $augmentOperationParameters; public function __construct(bool $augmentOperationParameters = true) { @@ -80,10 +80,8 @@ protected function augmentOperationParameters(Analysis $analysis): void if (array_key_exists('param', $tags)) { foreach ($tags['param'] as $name => $details) { foreach ($operation->parameters as $parameter) { - if ($parameter->name == $name) { - if (Generator::isDefault($parameter->description) && $details['description']) { - $parameter->description = $details['description']; - } + if ($parameter->name == $name && (Generator::isDefault($parameter->description) && $details['description'])) { + $parameter->description = $details['description']; } } } diff --git a/src/Processors/AugmentProperties.php b/src/Processors/AugmentProperties.php index 139168e28..e2ae9f1f9 100644 --- a/src/Processors/AugmentProperties.php +++ b/src/Processors/AugmentProperties.php @@ -50,10 +50,8 @@ public function __invoke(Analysis $analysis) if (Generator::isDefault($property->type)) { $this->augmentType($analysis, $property, $context, $refs, $typeAndDescription['type']); - } else { - if (!is_array($property->type)) { - $this->mapNativeType($property, $property->type); - } + } elseif (!is_array($property->type)) { + $this->mapNativeType($property, $property->type); } if (Generator::isDefault($property->description) && $typeAndDescription['description']) { @@ -162,21 +160,17 @@ protected function augmentType(Analysis $analysis, OA\Property $property, Contex $refKey = $this->toRefKey($context, $type); if (Generator::isDefault($property->ref) && array_key_exists($refKey, $refs)) { $this->applyRef($analysis, $property, $refs[$refKey]); - } else { - if (is_string($context->type) && $typeSchema = $analysis->getSchemaForSource($context->type)) { - if (Generator::isDefault($property->format)) { - $property->ref = OA\Components::ref($typeSchema); - $property->type = Generator::UNDEFINED; - } + } elseif (is_string($context->type) && $typeSchema = $analysis->getSchemaForSource($context->type)) { + if (Generator::isDefault($property->format)) { + $property->ref = OA\Components::ref($typeSchema); + $property->type = Generator::UNDEFINED; } } } } - if (!Generator::isDefault($property->const) && Generator::isDefault($property->type)) { - if (!$this->mapNativeType($property, gettype($property->const))) { - $property->type = Generator::UNDEFINED; - } + if (!Generator::isDefault($property->const) && Generator::isDefault($property->type) && !$this->mapNativeType($property, gettype($property->const))) { + $property->type = Generator::UNDEFINED; } } diff --git a/src/Processors/AugmentSchemas.php b/src/Processors/AugmentSchemas.php index a07b2aa30..ae3effbab 100644 --- a/src/Processors/AugmentSchemas.php +++ b/src/Processors/AugmentSchemas.php @@ -93,21 +93,19 @@ protected function augmentType(Analysis $analysis, array $schemas): void { foreach ($schemas as $schema) { if (Generator::isDefault($schema->type)) { - if (is_array($schema->properties) && count($schema->properties) > 0) { + if (is_array($schema->properties) && $schema->properties !== []) { $schema->type = 'object'; - } elseif (is_array($schema->additionalProperties) && count($schema->additionalProperties) > 0) { + } elseif (is_array($schema->additionalProperties) && $schema->additionalProperties !== []) { $schema->type = 'object'; - } elseif (is_array($schema->patternProperties) && count($schema->patternProperties) > 0) { + } elseif (is_array($schema->patternProperties) && $schema->patternProperties !== []) { $schema->type = 'object'; - } elseif (is_array($schema->propertyNames) && count($schema->propertyNames) > 0) { + } elseif (is_array($schema->propertyNames) && $schema->propertyNames !== []) { $schema->type = 'object'; } - } else { - if (is_string($schema->type) && $typeSchema = $analysis->getSchemaForSource($schema->type)) { - if (Generator::isDefault($schema->format)) { - $schema->ref = OA\Components::ref($typeSchema); - $schema->type = Generator::UNDEFINED; - } + } elseif (is_string($schema->type) && $typeSchema = $analysis->getSchemaForSource($schema->type)) { + if (Generator::isDefault($schema->format)) { + $schema->ref = OA\Components::ref($typeSchema); + $schema->type = Generator::UNDEFINED; } } } diff --git a/src/Processors/CleanUnusedComponents.php b/src/Processors/CleanUnusedComponents.php index 4a7303c32..e107893b1 100644 --- a/src/Processors/CleanUnusedComponents.php +++ b/src/Processors/CleanUnusedComponents.php @@ -48,13 +48,11 @@ protected function cleanup(Analysis $analysis): bool } } - if ($annotation instanceof OA\OpenApi || $annotation instanceof OA\Operation) { - if (!Generator::isDefault($annotation->security)) { - foreach ($annotation->security as $security) { - foreach (array_keys($security) as $securityName) { - $ref = OA\Components::COMPONENTS_PREFIX . 'securitySchemes/' . $securityName; - $usedRefs[$ref] = $ref; - } + if (($annotation instanceof OA\OpenApi || $annotation instanceof OA\Operation) && !Generator::isDefault($annotation->security)) { + foreach ($annotation->security as $security) { + foreach (array_keys($security) as $securityName) { + $ref = OA\Components::COMPONENTS_PREFIX . 'securitySchemes/' . $securityName; + $usedRefs[$ref] = $ref; } } } diff --git a/src/Processors/Concerns/DocblockTrait.php b/src/Processors/Concerns/DocblockTrait.php index 7c00c8861..b286c6c53 100644 --- a/src/Processors/Concerns/DocblockTrait.php +++ b/src/Processors/Concerns/DocblockTrait.php @@ -44,10 +44,8 @@ public function isRoot(OA\AbstractAnnotation $annotation): bool if ($className === get_class($contextAnnotation)) { return $annotation === $contextAnnotation; } - } else { - if ($contextAnnotation instanceof $className) { - return $annotation === $contextAnnotation; - } + } elseif ($contextAnnotation instanceof $className) { + return $annotation === $contextAnnotation; } } } diff --git a/src/Processors/ExpandClasses.php b/src/Processors/ExpandClasses.php index 6ede741c8..9ba659736 100644 --- a/src/Processors/ExpandClasses.php +++ b/src/Processors/ExpandClasses.php @@ -33,7 +33,7 @@ public function __invoke(Analysis $analysis) foreach ($ancestors as $ancestor) { $ancestorSchema = $analysis->getSchemaForSource($ancestor['context']->fullyQualifiedName($ancestor['class'])); if ($ancestorSchema) { - $refPath = !Generator::isDefault($ancestorSchema->schema) ? $ancestorSchema->schema : $ancestor['class']; + $refPath = Generator::isDefault($ancestorSchema->schema) ? $ancestor['class'] : $ancestorSchema->schema; $this->inheritFrom($analysis, $schema, $ancestorSchema, $refPath, $ancestor['context']); // one ancestor is enough diff --git a/src/Processors/ExpandEnums.php b/src/Processors/ExpandEnums.php index cbae7a0f2..2a492afb6 100644 --- a/src/Processors/ExpandEnums.php +++ b/src/Processors/ExpandEnums.php @@ -37,15 +37,13 @@ protected function expandContextEnum(Analysis $analysis): void foreach ($schemas as $schema) { if ($schema->_context->is('enum')) { $re = new \ReflectionEnum($schema->_context->fullyQualifiedName($schema->_context->enum)); - $schema->schema = !Generator::isDefault($schema->schema) ? $schema->schema : $re->getShortName(); + $schema->schema = Generator::isDefault($schema->schema) ? $re->getShortName() : $schema->schema; $schemaType = $schema->type; $enumType = null; if ($re->isBacked()) { $backingType = $re->getBackingType(); - if ($backingType instanceof \ReflectionNamedType) { - $enumType = $backingType->getName(); - } + $enumType = $backingType->getName(); } // no (or invalid) schema type means name @@ -99,11 +97,7 @@ protected function expandSchemaEnum(Analysis $analysis): void $enums = []; foreach ($cases as $enum) { - if (is_a($enum, \UnitEnum::class)) { - $enums[] = $enum->value ?? $enum->name; - } else { - $enums[] = $enum; - } + $enums[] = is_a($enum, \UnitEnum::class) ? $enum->value ?? $enum->name : $enum; } $schema->enum = $enums; diff --git a/src/Processors/ExpandInterfaces.php b/src/Processors/ExpandInterfaces.php index 8bcc3c25a..2d89ce1b0 100644 --- a/src/Processors/ExpandInterfaces.php +++ b/src/Processors/ExpandInterfaces.php @@ -43,7 +43,7 @@ public function __invoke(Analysis $analysis) $interfaceName = $interface['context']->fullyQualifiedName($interface['interface']); $interfaceSchema = $analysis->getSchemaForSource($interfaceName); if ($interfaceSchema) { - $refPath = !Generator::isDefault($interfaceSchema->schema) ? $interfaceSchema->schema : $interface['interface']; + $refPath = Generator::isDefault($interfaceSchema->schema) ? $interface['interface'] : $interfaceSchema->schema; $this->inheritFrom($analysis, $schema, $interfaceSchema, $refPath, $interface['context']); } else { $this->mergeMethods($schema, $interface, $existing); diff --git a/src/Processors/ExpandTraits.php b/src/Processors/ExpandTraits.php index cd1b4aca7..5cd7e2a45 100644 --- a/src/Processors/ExpandTraits.php +++ b/src/Processors/ExpandTraits.php @@ -32,7 +32,7 @@ public function __invoke(Analysis $analysis) foreach ($traits as $trait) { $traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait'])); if ($traitSchema) { - $refPath = !Generator::isDefault($traitSchema->schema) ? $traitSchema->schema : $trait['trait']; + $refPath = Generator::isDefault($traitSchema->schema) ? $trait['trait'] : $traitSchema->schema; $this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']); } else { $this->mergeMethods($schema, $trait, $existing); @@ -50,7 +50,7 @@ public function __invoke(Analysis $analysis) foreach ($traits as $trait) { $traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait'])); if ($traitSchema) { - $refPath = !Generator::isDefault($traitSchema->schema) ? $traitSchema->schema : $trait['trait']; + $refPath = Generator::isDefault($traitSchema->schema) ? $trait['trait'] : $traitSchema->schema; $this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']); } else { $this->mergeMethods($schema, $trait, $existing); diff --git a/src/Processors/OperationId.php b/src/Processors/OperationId.php index 0e799b079..84d7ac60b 100644 --- a/src/Processors/OperationId.php +++ b/src/Processors/OperationId.php @@ -15,7 +15,7 @@ */ class OperationId implements ProcessorInterface { - protected $hash; + protected bool $hash; public function __construct(bool $hash = true) { @@ -57,11 +57,7 @@ public function __invoke(Analysis $analysis) $operationId = null; if ($source) { $method = $context->method ? ('::' . $context->method) : ''; - if ($context->namespace) { - $operationId = $context->namespace . '\\' . $source . $method; - } else { - $operationId = $source . $method; - } + $operationId = $context->namespace ? $context->namespace . '\\' . $source . $method : $source . $method; } elseif ($context->method) { $operationId = $context->method; } diff --git a/src/Serializer.php b/src/Serializer.php index 739461585..94baa9c3a 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -90,7 +90,7 @@ public function deserializeFile(string $filename, string $format = 'json', strin $contents = file_get_contents($filename); $ext = pathinfo($filename, PATHINFO_EXTENSION); - if ('yaml' == $format || in_array($ext, ['yml', 'yaml'])) { + if ('yaml' === $format || in_array($ext, ['yml', 'yaml'])) { $contents = json_encode(Yaml::parse($contents)); } diff --git a/src/Util.php b/src/Util.php index be427b43e..1e7b33f2c 100644 --- a/src/Util.php +++ b/src/Util.php @@ -34,13 +34,13 @@ public static function getRelativePath(string $fullPath, $basePaths): string } else { // an array of paths foreach ($basePaths as $basePath) { $relativePath = self::removePrefix($fullPath, $basePath); - if (!empty($relativePath)) { + if ($relativePath !== null && $relativePath !== '' && $relativePath !== '0') { break; } } } - return !empty($relativePath) ? trim($relativePath, '/') : $fullPath; + return $relativePath === null || $relativePath === '' || $relativePath === '0' ? $fullPath : trim($relativePath, '/'); } /** @@ -48,7 +48,7 @@ public static function getRelativePath(string $fullPath, $basePaths): string */ private static function removePrefix(string $str, string $prefix): ?string { - if (substr($str, 0, strlen($prefix)) == $prefix) { + if (substr($str, 0, strlen($prefix)) === $prefix) { return substr($str, strlen($prefix)); } From e7e0fb825c278d3aa38f5425b82fa001b3dbe7ee Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 29 Apr 2024 06:48:18 -0500 Subject: [PATCH 2/3] removed older versions from CI --- .github/workflows/build.yml | 2 +- .github/workflows/security-checks.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8bbfdfda..f1923d8bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: operating-system: [ ubuntu-latest ] - php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] dependencies: [ 'lowest', 'highest' ] exclude: - php: '8.1' diff --git a/.github/workflows/security-checks.yml b/.github/workflows/security-checks.yml index 949602e0f..313dd8f03 100644 --- a/.github/workflows/security-checks.yml +++ b/.github/workflows/security-checks.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: operating-system: [ ubuntu-latest ] - php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] dependencies: [ 'highest' ] name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies From 4597e27aef7ee327e59972809de7646b13fb3844 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 29 Apr 2024 06:53:17 -0500 Subject: [PATCH 3/3] rector composer script --- composer.json | 2 ++ src/Annotations/Parameter.php | 8 +++----- src/Context.php | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 8597bcda1..b25402871 100644 --- a/composer.json +++ b/composer.json @@ -85,6 +85,7 @@ "testlegacy": "Run tests using the legacy TokenAnalyser", "testall": "Run all tests (test + testlegacy)", "analyse": "Run static analysis (phpstan/psalm)", + "rector": "Automatic refactoring", "spectral-examples": "Run spectral lint over all .yaml files in the Examples folder", "spectral-scratch": "Run spectral lint over all .yaml files in the tests/Fixtures/Scratch folder", "spectral": "Run all spectral tests", @@ -108,6 +109,7 @@ "export XDEBUG_MODE=off && phpstan analyse --memory-limit=2G", "export XDEBUG_MODE=off && psalm" ], + "rector": "rector process src --dry-run", "spectral-examples": "for ff in `find Examples -name '*.yaml'`; do spectral lint $ff; done", "spectral-scratch": "for ff in `find tests/Fixtures/Scratch -name '*.yaml'`; do spectral lint $ff; done", "spectral": [ diff --git a/src/Annotations/Parameter.php b/src/Annotations/Parameter.php index 338b92e48..33d57b163 100644 --- a/src/Annotations/Parameter.php +++ b/src/Annotations/Parameter.php @@ -277,11 +277,9 @@ public function validate(array $stack = [], array $skip = [], string $ref = '', $valid = parent::validate($stack, $skip, $ref, $context); - if (Generator::isDefault($this->ref) && $this->in === 'body') { - if (Generator::isDefault($this->schema)) { - $this->_context->logger->warning('Field "schema" is required when ' . $this->identity() . ' is in "' . $this->in . '" in ' . $this->_context); - $valid = false; - } + if (Generator::isDefault($this->ref) && $this->in === 'body' && Generator::isDefault($this->schema)) { + $this->_context->logger->warning('Field "schema" is required when ' . $this->identity() . ' is in "' . $this->in . '" in ' . $this->_context); + $valid = false; } return $valid; diff --git a/src/Context.php b/src/Context.php index 03b440b36..c6c392a10 100644 --- a/src/Context.php +++ b/src/Context.php @@ -96,7 +96,7 @@ public function with(string $property): ?Context if ($this->is($property)) { return $this; } - if ($this->parent !== null) { + if ($this->parent instanceof Context) { return $this->parent->with($property); } @@ -108,7 +108,7 @@ public function with(string $property): ?Context */ public function root(): Context { - if ($this->parent !== null) { + if ($this->parent instanceof Context) { return $this->parent->root(); } @@ -168,7 +168,7 @@ public function getDebugLocation(): string */ public function __get(string $property) { - if ($this->parent !== null) { + if ($this->parent instanceof Context) { return $this->parent->{$property}; }