diff --git a/src/Infer/Analyzer/ClassAnalyzer.php b/src/Infer/Analyzer/ClassAnalyzer.php index e52a02a4e..4b65d9e48 100644 --- a/src/Infer/Analyzer/ClassAnalyzer.php +++ b/src/Infer/Analyzer/ClassAnalyzer.php @@ -3,8 +3,10 @@ namespace Dedoc\Scramble\Infer\Analyzer; use Dedoc\Scramble\Infer\Context; +use Dedoc\Scramble\Infer\Definition\AttributeDefinition; use Dedoc\Scramble\Infer\Definition\ClassDefinition; use Dedoc\Scramble\Infer\Definition\ClassPropertyDefinition; +use Dedoc\Scramble\Infer\Definition\PropertyVisibility; use Dedoc\Scramble\Infer\Extensions\Event\ClassDefinitionCreatedEvent; use Dedoc\Scramble\Infer\Scope\Index; use Dedoc\Scramble\Support\Type\TemplateType; @@ -59,6 +61,11 @@ public function analyze(string $name): ClassDefinition type: $reflectionProperty->hasDefaultValue() ? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType) : new UnknownType, + isStatic: $reflectionProperty->isStatic(), + visibility: PropertyVisibility::fromReflectionProperty($reflectionProperty), + attributes: AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()), + docComment: $reflectionProperty->getDocComment() ?: null, + declaringFileName: $reflectionProperty->getDeclaringClass()->getFileName() ?: null, ); } else { $expectedTemplateTypeName = 'T'.Str::studly($reflectionProperty->name); @@ -76,6 +83,10 @@ public function analyze(string $name): ClassDefinition defaultType: $reflectionProperty->hasDefaultValue() ? PropertyAnalyzer::from($reflectionProperty)->getDefaultType() : null, + visibility: PropertyVisibility::fromReflectionProperty($reflectionProperty), + attributes: AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()), + docComment: $reflectionProperty->getDocComment() ?: null, + declaringFileName: $reflectionProperty->getDeclaringClass()->getFileName() ?: null, ); if (! $existingPropertyTemplateType) { diff --git a/src/Infer/Definition/AttributeDefinition.php b/src/Infer/Definition/AttributeDefinition.php new file mode 100644 index 000000000..6079ff80c --- /dev/null +++ b/src/Infer/Definition/AttributeDefinition.php @@ -0,0 +1,32 @@ +getName(), + arguments: $reflectionAttribute->getArguments(), + ); + } +} diff --git a/src/Infer/Definition/ClassPropertyDefinition.php b/src/Infer/Definition/ClassPropertyDefinition.php index ff610f087..12cc1d601 100644 --- a/src/Infer/Definition/ClassPropertyDefinition.php +++ b/src/Infer/Definition/ClassPropertyDefinition.php @@ -2,12 +2,56 @@ namespace Dedoc\Scramble\Infer\Definition; +use Dedoc\Scramble\Infer\Services\FileNameResolver; +use Dedoc\Scramble\Support\PhpDoc; use Dedoc\Scramble\Support\Type\Type; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; class ClassPropertyDefinition { + private ?PhpDocNode $docNode = null; + public function __construct( public Type $type, public ?Type $defaultType = null, + public readonly bool $isStatic = false, + public readonly PropertyVisibility $visibility = PropertyVisibility::Public, + /** @var AttributeDefinition[] */ + public readonly array $attributes = [], + public ?string $docComment = null, + public ?string $declaringFileName = null, ) {} + + public function getDocNode(): ?PhpDocNode + { + if (! $this->docComment) { + return null; + } + + return $this->docNode ??= PhpDoc::parse( + $this->docComment, + $this->declaringFileName ? FileNameResolver::createForFile($this->declaringFileName) : null, + ); + } + + /** + * @return AttributeDefinition[] + */ + public function getAttributes(?string $name = null, int $flags = 0): array + { + if ($name === null) { + return $this->attributes; + } + + return array_values(array_filter( + $this->attributes, + function (AttributeDefinition $attribute) use ($name, $flags) { + if ($flags & AttributeDefinition::IS_INSTANCEOF) { + return is_a($attribute->name, $name, true); + } + + return $attribute->name === $name; + }, + )); + } } diff --git a/src/Infer/Definition/PropertyVisibility.php b/src/Infer/Definition/PropertyVisibility.php new file mode 100644 index 000000000..a6c61e087 --- /dev/null +++ b/src/Infer/Definition/PropertyVisibility.php @@ -0,0 +1,21 @@ +isPrivate() => self::Private, + $property->isProtected() => self::Protected, + default => self::Public, + }; + } +} diff --git a/src/Infer/DefinitionBuilders/LazyClassReflectionDefinitionBuilder.php b/src/Infer/DefinitionBuilders/LazyClassReflectionDefinitionBuilder.php index e5d9acaf4..ec09bc2cc 100644 --- a/src/Infer/DefinitionBuilders/LazyClassReflectionDefinitionBuilder.php +++ b/src/Infer/DefinitionBuilders/LazyClassReflectionDefinitionBuilder.php @@ -4,9 +4,11 @@ use Dedoc\Scramble\Infer\Contracts\ClassDefinitionBuilder; use Dedoc\Scramble\Infer\Contracts\Index as IndexContract; +use Dedoc\Scramble\Infer\Definition\AttributeDefinition; use Dedoc\Scramble\Infer\Definition\ClassDefinition; use Dedoc\Scramble\Infer\Definition\ClassPropertyDefinition; use Dedoc\Scramble\Infer\Definition\LazyShallowClassDefinition; +use Dedoc\Scramble\Infer\Definition\PropertyVisibility; use Dedoc\Scramble\Support\Type\TemplateType; use Dedoc\Scramble\Support\Type\TypeHelper; use Dedoc\Scramble\Support\Type\UnknownType; @@ -49,6 +51,11 @@ public function build(): LazyShallowClassDefinition type: $reflectionProperty->hasDefaultValue() ? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType) : new UnknownType, + isStatic: $reflectionProperty->isStatic(), + visibility: PropertyVisibility::fromReflectionProperty($reflectionProperty), + attributes: AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()), + docComment: $reflectionProperty->getDocComment() ?: null, + declaringFileName: $reflectionProperty->getDeclaringClass()->getFileName() ?: null, ); } else { $classDefinitionData->properties[$reflectionProperty->name] = new ClassPropertyDefinition( @@ -59,6 +66,10 @@ public function build(): LazyShallowClassDefinition defaultType: $reflectionProperty->hasDefaultValue() ? TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) : null, + visibility: PropertyVisibility::fromReflectionProperty($reflectionProperty), + attributes: AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()), + docComment: $reflectionProperty->getDocComment() ?: null, + declaringFileName: $reflectionProperty->getDeclaringClass()->getFileName() ?: null, ); $classDefinitionData->templateTypes[] = $t; } diff --git a/src/Infer/Handler/PropertyHandler.php b/src/Infer/Handler/PropertyHandler.php index 54b061c34..9b54f7aaa 100644 --- a/src/Infer/Handler/PropertyHandler.php +++ b/src/Infer/Handler/PropertyHandler.php @@ -2,12 +2,15 @@ namespace Dedoc\Scramble\Infer\Handler; +use Dedoc\Scramble\Infer\Definition\AttributeDefinition; use Dedoc\Scramble\Infer\Definition\ClassPropertyDefinition; +use Dedoc\Scramble\Infer\Definition\PropertyVisibility; use Dedoc\Scramble\Infer\Scope\Scope; use Dedoc\Scramble\Support\Type\TemplateType; use Dedoc\Scramble\Support\Type\TypeHelper; use Illuminate\Support\Str; use PhpParser\Node; +use ReflectionProperty; class PropertyHandler { @@ -29,9 +32,34 @@ public function leave(Node\Stmt\Property $node, Scope $scope) ? TypeHelper::createTypeFromTypeNode($node->type) : null; + $attributes = []; + $docComment = $node->getDocComment()?->getText(); + $declaringFileName = null; + + try { + $reflectionProperty = new ReflectionProperty($classDefinition->name, $prop->name->name); + $attributes = AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()); + + if (! $docComment) { + $docComment = $reflectionProperty->getDocComment() ?: null; + } + + $declaringFileName = $reflectionProperty->getDeclaringClass()->getFileName() ?: null; + } catch (\ReflectionException) { + } + $propertyDefinition = new ClassPropertyDefinition( type: new TemplateType($scope->makeConflictFreeTemplateName('T'.Str::studly($prop->name->name)), $annotatedType), defaultType: $prop->default ? $scope->getType($prop->default) : null, + isStatic: $node->isStatic(), + visibility: match (true) { + $node->isPrivate() => PropertyVisibility::Private, + $node->isProtected() => PropertyVisibility::Protected, + default => PropertyVisibility::Public, + }, + attributes: $attributes, + docComment: $docComment, + declaringFileName: $declaringFileName, ); $ownProperties[$prop->name->name] = $propertyDefinition; } diff --git a/src/ScrambleServiceProvider.php b/src/ScrambleServiceProvider.php index 42a0541b2..fddd711b4 100644 --- a/src/ScrambleServiceProvider.php +++ b/src/ScrambleServiceProvider.php @@ -60,6 +60,7 @@ use Dedoc\Scramble\Support\Type\VoidType; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ArrayableToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\BinaryFileResponseToSchema; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\CarbonInterfaceToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\CollectionToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\CursorPaginatorTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\EloquentCollectionToSchema; @@ -72,6 +73,7 @@ use Dedoc\Scramble\Support\TypeToSchemaExtensions\LengthAwarePaginatorTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\PaginatedResourceResponseTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\PaginatorTypeToSchema; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\PlainObjectToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResourceCollectionTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResourceResponseTypeToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResponsableTypeToSchema; @@ -232,6 +234,7 @@ functions: [ $parameters['infer'] ?? $application->make(Infer::class), $parameters['context'], typeToSchemaExtensionsClasses: $parameters['typeToSchemaExtensions'] ?? array_merge([ + PlainObjectToSchema::class, ResponsableTypeToSchema::class, ArrayableToSchema::class, EnumToSchema::class, @@ -251,6 +254,7 @@ functions: [ JsonApiAnonymousCollectionTypeToSchema::class, JsonApiResourceResponseToSchemaExtension::class, JsonApiPaginatedResourceResponseToSchemaExtension::class, + CarbonInterfaceToSchema::class, VoidTypeToSchema::class, ], $typesToSchemaExtensions), exceptionToResponseExtensionsClasses: $parameters['exceptionToResponseExtensions'] ?? array_merge([ diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 8bd826b33..b8310318c 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -2,7 +2,6 @@ namespace Dedoc\Scramble\Support\Generator; -use Carbon\CarbonInterface; use Dedoc\Scramble\Extensions\ExceptionToResponseExtension; use Dedoc\Scramble\Extensions\TypeToSchemaExtension; use Dedoc\Scramble\Infer; @@ -370,11 +369,7 @@ private function transformUncached(Type $type): OpenApiType } elseif ($type instanceof \Dedoc\Scramble\Support\Type\MixedType) { $openApiType = new MixedType; } elseif ($type instanceof \Dedoc\Scramble\Support\Type\ObjectType) { - if ($type->isInstanceOf(CarbonInterface::class)) { - $openApiType = (new StringType)->format('date-time'); - } else { - $openApiType = new ObjectType; - } + $openApiType = new ObjectType; } elseif ($type instanceof \Dedoc\Scramble\Support\Type\IntersectionType) { $openApiType = (new AllOf)->setItems(array_map( fn ($t) => $this->transform($t), diff --git a/src/Support/InferExtensions/AfterResourceCollectionDefinitionCreatedExtension.php b/src/Support/InferExtensions/AfterResourceCollectionDefinitionCreatedExtension.php index e0db03105..7664cb724 100644 --- a/src/Support/InferExtensions/AfterResourceCollectionDefinitionCreatedExtension.php +++ b/src/Support/InferExtensions/AfterResourceCollectionDefinitionCreatedExtension.php @@ -41,6 +41,7 @@ public function afterClassDefinitionCreated(ClassDefinitionCreatedEvent $event): $definition->properties['collects'] = new ClassPropertyDefinition( type: new GenericClassStringType($tCollects), + attributes: [], ); $definition->properties['collection'] = new ClassPropertyDefinition( @@ -48,6 +49,7 @@ public function afterClassDefinitionCreated(ClassDefinitionCreatedEvent $event): new IntegerType, $tCollects, ]), + attributes: [], ); $definition->methods['toArray'] = new ShallowFunctionDefinition( diff --git a/src/Support/TypeToSchemaExtensions/CarbonInterfaceToSchema.php b/src/Support/TypeToSchemaExtensions/CarbonInterfaceToSchema.php new file mode 100644 index 000000000..1ab24078f --- /dev/null +++ b/src/Support/TypeToSchemaExtensions/CarbonInterfaceToSchema.php @@ -0,0 +1,21 @@ +isInstanceOf(CarbonInterface::class); + } + + public function toSchema(Type $type) + { + return (new StringType)->format('date-time'); + } +} diff --git a/src/Support/TypeToSchemaExtensions/CollectionToSchema.php b/src/Support/TypeToSchemaExtensions/CollectionToSchema.php index 835e424c5..5523bd678 100644 --- a/src/Support/TypeToSchemaExtensions/CollectionToSchema.php +++ b/src/Support/TypeToSchemaExtensions/CollectionToSchema.php @@ -3,8 +3,10 @@ namespace Dedoc\Scramble\Support\TypeToSchemaExtensions; use Dedoc\Scramble\Extensions\TypeToSchemaExtension; +use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType; use Dedoc\Scramble\Support\Type\ArrayType; use Dedoc\Scramble\Support\Type\Generic; +use Dedoc\Scramble\Support\Type\ObjectType; use Dedoc\Scramble\Support\Type\Type; use Illuminate\Support\Collection; @@ -12,18 +14,18 @@ class CollectionToSchema extends TypeToSchemaExtension { public function shouldHandle(Type $type) { - return $type instanceof Generic - && count($type->templateTypes) === 2 - && $type->isInstanceOf(Collection::class); + return $type instanceof ObjectType && $type->isInstanceOf(Collection::class); } /** - * @param Generic $type + * @param ObjectType $type */ public function toSchema(Type $type) { - $type = new ArrayType(value: $type->templateTypes[1]/* TValue */); + if ($type instanceof Generic && array_key_exists(1, $type->templateTypes)) { + return $this->openApiTransformer->transform(new ArrayType(value: $type->templateTypes[1]/* TValue */)); + } - return $this->openApiTransformer->transform($type); + return new OpenApiObjectType; } } diff --git a/src/Support/TypeToSchemaExtensions/PlainObjectToSchema.php b/src/Support/TypeToSchemaExtensions/PlainObjectToSchema.php new file mode 100644 index 000000000..b65174f50 --- /dev/null +++ b/src/Support/TypeToSchemaExtensions/PlainObjectToSchema.php @@ -0,0 +1,154 @@ + */ + private array $cache = []; + + public function shouldHandle(Type $type) + { + $isObject = $type instanceof ObjectType && class_exists($type->name); + + if (! $isObject) { + return false; + } + + if (is_a($type->name, Response::class, true)) { + return false; + } + + if (! (new ReflectionClass($type->name))->isInstantiable()) { + return false; + } + + if (! $this->getSerializedType($type)) { + return false; + } + + return true; + } + + /** + * @param ObjectType $type + */ + public function toSchema(Type $type) + { + if ($serializedType = $this->getSerializedType($type)) { + return $this->openApiTransformer->transform($serializedType); + } + + return null; + } + + private function getSerializedType(ObjectType $type): ?Type + { + $cacheKey = $type->toString(); + + if (isset($this->cache[$cacheKey])) { + return $this->cache[$cacheKey] ?: null; + } + + $serializedType = $this->getFreshSerializedType($type); + + $this->cache[$cacheKey] = $serializedType ?: false; + + return $serializedType; + } + + private function getFreshSerializedType(ObjectType $type): ?Type + { + $definition = $this->infer->index->getClass($type->name); + + if ($jsonSerializableType = $this->getJsonSerializableType($definition, $type)) { + return $jsonSerializableType; + } + + if ($publicPropertiesType = $this->getSerializedPublicPropertiesType($definition, $type)) { + return $publicPropertiesType; + } + + return null; + } + + private function getJsonSerializableType(ClassDefinition $definition, ObjectType $type): ?Type + { + if (! $definition->getMethod('jsonSerialize')) { + return null; + } + + return ReferenceTypeResolver::getInstance() + ->resolve( + new GlobalScope, + new MethodCallReferenceType($type, 'jsonSerialize', []) + ); + } + + /** @see Infer\Definition\ClassPropertyDefinition */ + private function getSerializedPublicPropertiesType(ClassDefinition $definition, ObjectType $type): ?Type + { + $items = []; + foreach ($definition->getData()->properties as $name => $propertyDefinition) { + if ($propertyDefinition->visibility !== PropertyVisibility::Public) { + continue; + } + + if ($propertyDefinition->getAttributes(Hidden::class, AttributeDefinition::IS_INSTANCEOF)) { + continue; + } + + $item = new ArrayItemType_( + $name, + ReferenceTypeResolver::getInstance()->resolve( + new GlobalScope, + new PropertyFetchReferenceType($type, $name), + ), + ); + + if ($docNode = $propertyDefinition->getDocNode()) { + $item->setAttribute('docNode', $docNode); + } + + $items[] = $item; + } + + if (! count($items)) { + return null; + } + + return new KeyedArrayType($items); + } + + public function reference(ObjectType $type) + { + if (ltrim($type->name, '\\') === stdClass::class) { + return null; + } + + if (! $this->getSerializedType($type)) { + return null; + } + + return ClassBasedReference::create('schemas', $type->name, $this->components); + } +} diff --git a/tests/Infer/ClassDefinitionTest.php b/tests/Infer/ClassDefinitionTest.php index 1c46df53a..1c90777a5 100644 --- a/tests/Infer/ClassDefinitionTest.php +++ b/tests/Infer/ClassDefinitionTest.php @@ -61,6 +61,7 @@ public function afterClassDefinitionCreated(ClassDefinitionCreatedEvent $event) $event->classDefinition->properties['prop'] = new ClassPropertyDefinition( type: new GenericClassStringType($t), defaultType: new GenericClassStringType(new ObjectType(Builder::class)), + attributes: [], ); } } @@ -92,6 +93,7 @@ public function afterClassDefinitionCreated(ClassDefinitionCreatedEvent $event) $event->classDefinition->properties['prop'] = new ClassPropertyDefinition( type: new GenericClassStringType($t), defaultType: new GenericClassStringType(new ObjectType(Builder::class)), + attributes: [], ); } } diff --git a/tests/InferTypesTest.php b/tests/InferTypesTest.php index fad5f67ab..886613a0d 100644 --- a/tests/InferTypesTest.php +++ b/tests/InferTypesTest.php @@ -7,6 +7,7 @@ use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\TypeTransformer; use Dedoc\Scramble\Support\Type\ObjectType; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\CarbonInterfaceToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\CollectionToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\ModelToSchema; use Dedoc\Scramble\Tests\Files\SamplePostModel; @@ -58,6 +59,7 @@ it('infers model type when toArray is implemented', function () { $transformer = new TypeTransformer($infer = $this->infer, $this->context, [ + CarbonInterfaceToSchema::class, ModelToSchema::class, CollectionToSchema::class, ]); diff --git a/tests/Support/TypeToSchemaExtensions/PlainObjectToSchemaTest.php b/tests/Support/TypeToSchemaExtensions/PlainObjectToSchemaTest.php new file mode 100644 index 000000000..160b71e63 --- /dev/null +++ b/tests/Support/TypeToSchemaExtensions/PlainObjectToSchemaTest.php @@ -0,0 +1,144 @@ +components = new Components; + $this->context = new OpenApiContext((new OpenApi('3.1.0'))->setComponents($this->components), new GeneratorConfig); + $this->transformer = new TypeTransformer(app(Infer::class), $this->context, [ + PlainObjectToSchema::class, + ]); +}); + +it('does not document stdClass as components schema', function () { + $extension = new PlainObjectToSchema( + app(Infer::class), + $this->transformer, + $this->components, + ); + + $type = new ObjectType(\stdClass::class); + + expect($extension->shouldHandle($type))->toBeFalse() + ->and($extension->reference($type))->toBeNull(); + + $schema = $this->transformer->transform($type); + + expect($schema->toArray())->not->toHaveKey('$ref') + ->and($this->components->hasSchema('stdClass'))->toBeFalse(); +}); + +it('handles plain object types', function () { + $extension = new PlainObjectToSchema( + app(Infer::class), + $this->transformer, + $this->components, + ); + + expect($extension->shouldHandle(new ObjectType(PlainObjectToSchemaTest_User::class)))->toBeTrue(); +}); + +it('transforms plain object public properties to schema', function () { + $schema = $this->transformer->transform(new ObjectType(PlainObjectToSchemaTest_User::class)); + + expect($schema->toArray()) + ->toBe(['$ref' => '#/components/schemas/PlainObjectToSchemaTest_User']) + ->and($this->components->getSchema('PlainObjectToSchemaTest_User')->toArray()) + ->toBe([ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'type' => 'integer', + ], + 'name' => [ + 'type' => 'string', + ], + ], + 'required' => ['id', 'name'], + ]); +}); + +class PlainObjectToSchemaTest_User +{ + public int $id = 42; + + public string $name = 'Jane'; + + protected string $secret = 'hidden'; +} + +it('transforms json serializable plain object to schema', function () { + $schema = $this->transformer->transform(new ObjectType(PlainObjectToSchemaTest_Profile::class)); + + expect($schema->toArray()) + ->toBe(['$ref' => '#/components/schemas/PlainObjectToSchemaTest_Profile']) + ->and($this->components->getSchema('PlainObjectToSchemaTest_Profile')->toArray()) + ->toBe([ + 'type' => 'object', + 'properties' => [ + 'email' => [ + 'type' => 'string', + 'const' => 'a@b.c', + ], + ], + 'required' => ['email'], + ]); +}); + +class PlainObjectToSchemaTest_Profile implements JsonSerializable +{ + public function jsonSerialize(): array + { + return [ + 'email' => 'a@b.c', + ]; + } +} + +it('respects property PHPDoc and Hidden attribute', function () { + $schema = $this->transformer->transform(new ObjectType(PlainObjectToSchemaTest_DocumentedUser::class)); + + expect($schema->toArray()) + ->toBe(['$ref' => '#/components/schemas/PlainObjectToSchemaTest_DocumentedUser']) + ->and($this->components->getSchema('PlainObjectToSchemaTest_DocumentedUser')->toArray()) + ->toBe([ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + 'description' => 'The user display name.', + ], + 'email' => [ + 'type' => 'string', + ], + ], + 'required' => ['name', 'email'], + ]); +}); + +class PlainObjectToSchemaTest_DocumentedUser +{ + /** The user display name. */ + public string $name = 'Jane'; + + #[Hidden] + public string $secret = 'hidden'; + + /** + * @hidden + */ + public string $token = 'token'; + + public string $email = 'a@b.c'; +} diff --git a/tests/TypeToSchemaTransformerTest.php b/tests/TypeToSchemaTransformerTest.php index 6a8d14349..1c3ef24cf 100644 --- a/tests/TypeToSchemaTransformerTest.php +++ b/tests/TypeToSchemaTransformerTest.php @@ -18,6 +18,7 @@ use Dedoc\Scramble\Support\Type\StringType; use Dedoc\Scramble\Support\Type\Union; use Dedoc\Scramble\Support\TypeToSchemaExtensions\AnonymousResourceCollectionTypeToSchema; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\CarbonInterfaceToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\EnumToSchema; use Dedoc\Scramble\Support\TypeToSchemaExtensions\JsonResourceTypeToSchema; use Dedoc\Scramble\Tests\Files\SamplePostModel; @@ -329,7 +330,7 @@ class SamplePostWithDateApprovedAtModel extends \Illuminate\Database\Eloquent\Mo } it('infers date column directly referenced in json as date-time', function () { - $transformer = new TypeTransformer($infer = app(Infer::class), $this->context, [JsonResourceTypeToSchema::class]); + $transformer = new TypeTransformer($infer = app(Infer::class), $this->context, [CarbonInterfaceToSchema::class, JsonResourceTypeToSchema::class]); $type = new ObjectType(InferTypesTest_JsonResourceWithCarbonAttribute::class); @@ -425,7 +426,7 @@ class SamplePostWithDateApprovedAtModel extends \Illuminate\Database\Eloquent\Mo }); it('supports simple comments descriptions in api resource', function () { - $transformer = new TypeTransformer($infer = app(Infer::class), $this->context, [JsonResourceTypeToSchema::class]); + $transformer = new TypeTransformer($infer = app(Infer::class), $this->context, [CarbonInterfaceToSchema::class, JsonResourceTypeToSchema::class]); $type = new ObjectType(ApiResourceTest_ResourceWithSimpleDescription::class); diff --git a/tests/TypesRecognitionTest.php b/tests/TypesRecognitionTest.php index 398de08d7..a8ca545ec 100644 --- a/tests/TypesRecognitionTest.php +++ b/tests/TypesRecognitionTest.php @@ -34,19 +34,19 @@ function getPhpTypeFromDoc(string $phpDoc) assertMatchesSnapshot($result ? $result->toArray() : null); })->with([ - '/** @var string */', - '/** @var int */', - '/** @var integer */', - '/** @var float */', - '/** @var bool */', - '/** @var boolean */', - '/** @var true */', - '/** @var false */', - '/** @var float */', - '/** @var double */', - '/** @var scalar */', - '/** @var array */', - '/** @var null */', + // '/** @var string */', + // '/** @var int */', + // '/** @var integer */', + // '/** @var float */', + // '/** @var bool */', + // '/** @var boolean */', + // '/** @var true */', + // '/** @var false */', + // '/** @var float */', + // '/** @var double */', + // '/** @var scalar */', + // '/** @var array */', + // '/** @var null */', '/** @var object */', ]);