diff --git a/benchmarks/TestBench.php b/benchmarks/TestBench.php new file mode 100644 index 000000000..866c7c996 --- /dev/null +++ b/benchmarks/TestBench.php @@ -0,0 +1,60 @@ +createApplication(); + } + + protected function getPackageProviders($app) + { + return [ + LaravelDataServiceProvider::class, + ]; + } + + #[Revs(5000), Iterations(5)] + public function benchUseStored() + { + for ($i = 0; $i < 100; $i++) { + $this->runStored(); + } + } + + protected function runStored(): array + { + return AcceptedTypesStorage::getAcceptedTypes(Collection::class); + } + + #[Revs(5000), Iterations(5)] + public function benchUseNative() + { + for ($i = 0; $i < 100; $i++) { + $this->runNative(); + } + } + + protected function runNative(): array + { + return ! class_exists(Collection::class) ? [] : array_unique([ + ...array_values(class_parents(Collection::class)), + ...array_values(class_implements(Collection::class)), + ]); + } +} diff --git a/composer.json b/composer.json index 22dd8648f..b2d151064 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require" : { - "php": "^8.1", + "php": "^8.2", "illuminate/contracts": "^10.0", "phpdocumentor/type-resolver": "^1.5", "spatie/laravel-package-tools": "^1.9.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5c688b1e1..435d719db 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -125,36 +125,11 @@ parameters: count: 1 path: src/Support/DataConfig.php - - - message: "#^Match expression does not handle remaining values\\: \\(class\\-string\\&literal\\-string\\)\\|\\(class\\-string\\&literal\\-string\\)$#" - count: 1 - path: src/Support/Factories/DataTypeFactory.php - - - - message: "#^Match expression does not handle remaining values\\: \\(class\\-string\\&literal\\-string\\)\\|\\(class\\-string\\&literal\\-string\\)$#" - count: 1 - path: src/Support/Factories/DataTypeFactory.php - - message: "#^Parameter \\#1 \\$storage of method SplObjectStorage\\\\:\\:removeAll\\(\\) expects SplObjectStorage\\, Spatie\\\\LaravelData\\\\Support\\\\Partials\\\\PartialsCollection given\\.$#" count: 1 path: src/Support/Transformation/DataContext.php - - - message: "#^Call to an undefined method ReflectionType\\:\\:getName\\(\\)\\.$#" - count: 2 - path: src/Support/Types/MultiType.php - - - - message: "#^Parameter \\#1 \\$type of static method Spatie\\\\LaravelData\\\\Support\\\\Types\\\\PartialType\\:\\:create\\(\\) expects ReflectionNamedType, ReflectionType given\\.$#" - count: 1 - path: src/Support/Types/MultiType.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: src/Support/Types/MultiType.php - - message: "#^Call to an undefined method DateTimeInterface\\:\\:setTimezone\\(\\)\\.$#" count: 1 diff --git a/src/Commands/DataStructuresCacheCommand.php b/src/Commands/DataStructuresCacheCommand.php index 565875b01..a5b228d24 100644 --- a/src/Commands/DataStructuresCacheCommand.php +++ b/src/Commands/DataStructuresCacheCommand.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Support\Caching\DataStructureCache; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataStructuresCacheCommand extends Command { @@ -18,7 +19,7 @@ class DataStructuresCacheCommand extends Command public function handle( DataStructureCache $dataStructureCache, - DataConfig $dataConfig + DataClassFactory $dataClassFactory, ): void { $this->components->info('Caching data structures...'); @@ -31,7 +32,7 @@ public function handle( $progressBar = $this->output->createProgressBar(count($dataClasses)); foreach ($dataClasses as $dataClassString) { - $dataClass = DataClass::create(new ReflectionClass($dataClassString)); + $dataClass = $dataClassFactory->build(new ReflectionClass($dataClassString)); $dataClass->prepareForCache(); diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 2cf6158d9..36edd69a9 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -85,6 +85,6 @@ protected function shouldBeCasted(DataProperty $property, mixed $value): bool return true; // Transform everything to data objects } - return $property->type->type->acceptsValue($value) === false; + return $property->type->acceptsValue($value) === false; } } diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index 680a50e46..b865068bf 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -32,7 +32,7 @@ public function handle( return; } - if ($property->type->isNullable()) { + if ($property->type->isNullable) { $properties[$property->name] = null; return; diff --git a/src/Exceptions/CannotFindDataClass.php b/src/Exceptions/CannotFindDataClass.php index cbfd9156a..3b38d2356 100644 --- a/src/Exceptions/CannotFindDataClass.php +++ b/src/Exceptions/CannotFindDataClass.php @@ -3,26 +3,26 @@ namespace Spatie\LaravelData\Exceptions; use Exception; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionProperty; class CannotFindDataClass extends Exception { - public static function noDataReferenceFound(string $class, string $propertyName): self + public static function forTypeable(ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable): self { - return new self("Property `{$propertyName}` in `{$class}` is not a data object or collection"); - } + if (is_string($typeable)) { + return new self("Cannot find data class for type `{$typeable}`"); + } - public static function missingDataCollectionAnotation(string $class, string $propertyName): self - { - return new self("Data collection property `{$propertyName}` in `{$class}` is missing an annotation with the type of data it represents"); - } + $class = $typeable->getDeclaringClass()->getName(); - public static function wrongDataCollectionAnnotation(string $class, string $propertyName): self - { - return new self("Data collection property `{$propertyName}` in `{$class}` has an annotation that isn't a data object or is missing an annotation"); - } + $name = match (true) { + $typeable instanceof ReflectionMethod => "method `{$class}::{{$typeable->getName()}`", + $typeable instanceof ReflectionProperty => "property `{$class}::{{$typeable->getName()}`", + $typeable instanceof ReflectionParameter => "parameter `{$class}::{$typeable->getDeclaringFunction()->getName()}::{$typeable->getName()}`", + }; - public static function cannotReadReflectionParameterDocblock(string $class, string $parameter): self - { - return new self("Data collection reflection parameter `{$parameter}` in `{$class}::__constructor` has an annotation that isn't a data object or is missing an annotation"); + return new self("Cannot find data class for {$name}"); } } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 4a75a109d..0152a0a24 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -20,13 +20,15 @@ use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; -use Spatie\LaravelData\Support\Types\PartialType; +use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; +use Spatie\LaravelData\Support\Factories\DataTypeFactory; class DataCollectableFromSomethingResolver { public function __construct( protected DataConfig $dataConfig, protected DataFromSomethingResolver $dataFromSomethingResolver, + protected DataReturnTypeFactory $dataReturnTypeFactory, ) { } @@ -37,8 +39,8 @@ public function execute( ?string $into = null, ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { $intoType = $into !== null - ? PartialType::createFromTypeString($into) - : PartialType::createFromValue($items); + ? $this->dataReturnTypeFactory->buildFromNamedType($into) + : $this->dataReturnTypeFactory->buildFromValue($items); $collectable = $this->createFromCustomCreationMethod($dataClass, $creationContext, $items, $into); @@ -46,21 +48,19 @@ public function execute( return $collectable; } - $intoDataTypeKind = $intoType->getDataTypeKind(); - $collectableMetaData = CollectableMetaData::fromOther($items); $normalizedItems = $this->normalizeItems($items, $dataClass, $creationContext); - return match ($intoDataTypeKind) { + return match ($intoType->kind) { DataTypeKind::Array => $this->normalizeToArray($normalizedItems), - DataTypeKind::Enumerable => new $intoType->name($this->normalizeToArray($normalizedItems)), - DataTypeKind::DataCollection => new $intoType->name($dataClass, $this->normalizeToArray($normalizedItems)), - DataTypeKind::DataPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), - DataTypeKind::DataCursorPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::Enumerable => new $intoType->type->name($this->normalizeToArray($normalizedItems)), + DataTypeKind::DataCollection => new $intoType->type->name($dataClass, $this->normalizeToArray($normalizedItems)), + DataTypeKind::DataPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::DataCursorPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), DataTypeKind::Paginator => $this->normalizeToPaginator($normalizedItems, $collectableMetaData), DataTypeKind::CursorPaginator => $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData), - default => CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name) + default => throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->type->name) }; } @@ -107,7 +107,7 @@ protected function createFromCustomCreationMethod( $payload = []; foreach ($method->parameters as $parameter) { - if ($parameter->isCreationContext) { + if ($parameter->type->type->isCreationContext()) { $payload[$parameter->name] = $creationContext; } else { $payload[$parameter->name] = $this->normalizeItems($items, $dataClass, $creationContext); diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index ac8f66d8e..33eb7e7de 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -63,7 +63,7 @@ public function execute(string $class, Collection $properties): BaseData } if ($property->computed - && $property->type->isNullable() + && $property->type->isNullable && $properties->get($property->name) === null ) { return; // Nullable properties get assigned null by default diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 5317121b8..e13eeb29a 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -95,7 +95,7 @@ protected function createFromCustomCreationMethod( } foreach ($method->parameters as $index => $parameter) { - if ($parameter->isCreationContext) { + if ($parameter->type->type->isCreationContext()) { $payloads[$index] = $creationContext; } } diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index da378f790..76dbf57bb 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -103,7 +103,7 @@ protected function resolveDataSpecificRules( DataRules $dataRules, ): void { $isOptionalAndEmpty = $dataProperty->type->isOptional && Arr::has($fullPayload, $propertyPath->get()) === false; - $isNullableAndEmpty = $dataProperty->type->isNullable() && Arr::get($fullPayload, $propertyPath->get()) === null; + $isNullableAndEmpty = $dataProperty->type->isNullable && Arr::get($fullPayload, $propertyPath->get()) === null; if ($isOptionalAndEmpty || $isNullableAndEmpty) { $this->resolveToplevelRules( diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index de9fc01ab..3509d2467 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -6,7 +6,7 @@ use Spatie\LaravelData\Exceptions\DataPropertyCanOnlyHaveOneType; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Types\MultiType; +use Spatie\LaravelData\Support\Types\CombinationType; use Traversable; class EmptyDataResolver @@ -37,11 +37,12 @@ public function execute(string $class, array $extra = []): array protected function getValueForProperty(DataProperty $property): mixed { $propertyType = $property->type; - if ($propertyType->isMixed()) { + + if ($propertyType->isMixed) { return null; } - if ($propertyType->type instanceof MultiType && $propertyType->type->acceptedTypesCount() > 1) { + if ($propertyType->type instanceof CombinationType && count($propertyType->type->types) > 1) { throw DataPropertyCanOnlyHaveOneType::create($property); } diff --git a/src/RuleInferrers/NullableRuleInferrer.php b/src/RuleInferrers/NullableRuleInferrer.php index 0eba9fd80..5946c51dc 100644 --- a/src/RuleInferrers/NullableRuleInferrer.php +++ b/src/RuleInferrers/NullableRuleInferrer.php @@ -14,7 +14,7 @@ public function handle( PropertyRules $rules, ValidationContext $context, ): PropertyRules { - if ($property->type->isNullable() && ! $rules->hasType(Nullable::class)) { + if ($property->type->isNullable && ! $rules->hasType(Nullable::class)) { $rules->prepend(new Nullable()); } diff --git a/src/RuleInferrers/RequiredRuleInferrer.php b/src/RuleInferrers/RequiredRuleInferrer.php index dfd72bf91..19c4f8d74 100644 --- a/src/RuleInferrers/RequiredRuleInferrer.php +++ b/src/RuleInferrers/RequiredRuleInferrer.php @@ -27,7 +27,7 @@ public function handle( protected function shouldAddRule(DataProperty $property, PropertyRules $rules): bool { - if ($property->type->isNullable() || $property->type->isOptional) { + if ($property->type->isNullable || $property->type->isOptional) { return false; } diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Casting/GlobalCastsCollection.php index 65fc03744..50eabb700 100644 --- a/src/Support/Casting/GlobalCastsCollection.php +++ b/src/Support/Casting/GlobalCastsCollection.php @@ -34,7 +34,7 @@ public function merge(self $casts): self public function findCastForValue(DataProperty $property): ?Cast { - foreach ($property->type->type->getAcceptedTypes() as $acceptedType => $baseTypes) { + foreach ($property->type->getAcceptedTypes() as $acceptedType => $baseTypes) { foreach ([$acceptedType, ...$baseTypes] as $type) { if ($cast = $this->casts[$type] ?? null) { return $cast; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 9b4bf41ff..5c1fba435 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -56,165 +56,6 @@ public function __construct( ) { } - public static function create(ReflectionClass $class): self - { - /** @var class-string $name */ - $name = $class->name; - - $attributes = static::resolveAttributes($class); - - $methods = collect($class->getMethods()); - - $constructor = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor()); - - $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); - - if ($constructor) { - $dataCollectablePropertyAnnotations = array_merge( - $dataCollectablePropertyAnnotations, - DataCollectableAnnotationReader::create()->getForMethod($constructor) - ); - } - - $properties = self::resolveProperties( - $class, - $constructor, - NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes), - $dataCollectablePropertyAnnotations, - ); - - $responsable = $class->implementsInterface(ResponsableData::class); - - $outputMappedProperties = new LazyDataStructureProperty( - fn () => $properties - ->map(fn (DataProperty $property) => $property->outputMappedName) - ->filter() - ->flip() - ->toArray() - ); - - return new self( - name: $class->name, - properties: $properties, - methods: self::resolveMethods($class), - constructorMethod: DataMethod::createConstructor($constructor, $properties), - isReadonly: method_exists($class, 'isReadOnly') && $class->isReadOnly(), - isAbstract: $class->isAbstract(), - appendable: $class->implementsInterface(AppendableData::class), - includeable: $class->implementsInterface(IncludeableData::class), - responsable: $responsable, - transformable: $class->implementsInterface(TransformableData::class), - validateable: $class->implementsInterface(ValidateableData::class), - wrappable: $class->implementsInterface(WrappableData::class), - emptyData: $class->implementsInterface(EmptyData::class), - attributes: $attributes, - dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, - allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, - allowedRequestExcludes: $responsable ? $name::allowedRequestExcludes() : null, - allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, - allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, - outputMappedProperties: $outputMappedProperties, - transformationFields: static::resolveTransformationFields($properties), - ); - } - - protected static function resolveAttributes( - ReflectionClass $class - ): Collection { - $attributes = collect($class->getAttributes()) - ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) - ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); - - $parent = $class->getParentClass(); - - if ($parent !== false) { - $attributes = $attributes->merge(static::resolveAttributes($parent)); - } - - return $attributes; - } - - protected static function resolveMethods( - ReflectionClass $reflectionClass, - ): Collection { - return collect($reflectionClass->getMethods()) - ->filter(fn (ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collect')) - ->reject(fn (ReflectionMethod $method) => in_array($method->name, ['from', 'collect', 'collection'])) - ->mapWithKeys( - fn (ReflectionMethod $method) => [$method->name => DataMethod::create($method)], - ); - } - - protected static function resolveProperties( - ReflectionClass $class, - ?ReflectionMethod $constructorMethod, - array $mappers, - array $dataCollectablePropertyAnnotations, - ): Collection { - $defaultValues = self::resolveDefaultValues($class, $constructorMethod); - - return collect($class->getProperties(ReflectionProperty::IS_PUBLIC)) - ->reject(fn (ReflectionProperty $property) => $property->isStatic()) - ->values() - ->mapWithKeys(fn (ReflectionProperty $property) => [ - $property->name => DataProperty::create( - $property, - array_key_exists($property->getName(), $defaultValues), - $defaultValues[$property->getName()] ?? null, - $mappers['inputNameMapper'], - $mappers['outputNameMapper'], - $dataCollectablePropertyAnnotations[$property->getName()] ?? null, - ), - ]); - } - - protected static function resolveDefaultValues( - ReflectionClass $class, - ?ReflectionMethod $constructorMethod, - ): array { - if (! $constructorMethod) { - return $class->getDefaultProperties(); - } - - $values = collect($constructorMethod->getParameters()) - ->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) - ->mapWithKeys(fn (ReflectionParameter $parameter) => [ - $parameter->name => $parameter->getDefaultValue(), - ]) - ->toArray(); - - return array_merge( - $class->getDefaultProperties(), - $values - ); - } - - /** - * @param Collection $properties - * - * @return LazyDataStructureProperty> - */ - protected static function resolveTransformationFields( - Collection $properties, - ): LazyDataStructureProperty { - $closure = fn () => $properties - ->reject(fn (DataProperty $property): bool => $property->hidden) - ->map(function (DataProperty $property): null|bool { - if ( - $property->type->kind->isDataCollectable() - || $property->type->kind->isDataObject() - || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) - ) { - return true; - } - - return null; - }) - ->all(); - - return new LazyDataStructureProperty($closure); - } - public function prepareForCache(): void { if($this->outputMappedProperties instanceof LazyDataStructureProperty) { diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index f58e9de79..1693fbfba 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -59,7 +59,7 @@ public function __construct( public function getDataClass(string $class): DataClass { - return $this->dataClasses[$class] ??= DataClass::create(new ReflectionClass($class)); + return $this->dataClasses[$class] ??= DataContainer::get()->dataClassFactory()->build(new ReflectionClass($class)); } /** diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index 3003a376d..c8167d321 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -7,6 +7,8 @@ use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; +use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataContainer { @@ -22,6 +24,8 @@ class DataContainer protected ?DataCollectableFromSomethingResolver $dataCollectableFromSomethingResolver = null; + protected ?DataClassFactory $dataClassFactory = null; + private function __construct() { } @@ -60,6 +64,11 @@ public function dataCollectableFromSomethingResolver(): DataCollectableFromSomet return $this->dataCollectableFromSomethingResolver ??= app(DataCollectableFromSomethingResolver::class); } + public function dataClassFactory(): DataClassFactory + { + return $this->dataClassFactory ??= app(DataClassFactory::class); + } + public function reset() { $this->transformedDataResolver = null; @@ -67,5 +76,6 @@ public function reset() $this->requestQueryStringPartialsResolver = null; $this->dataFromSomethingResolver = null; $this->dataCollectableFromSomethingResolver = null; + $this->dataClassFactory = null; } } diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index fac973ee8..50318ff75 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -3,11 +3,8 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; -use ReflectionMethod; -use ReflectionParameter; use Spatie\LaravelData\Enums\CustomCreationMethodType; -use Spatie\LaravelData\Support\Types\Type; -use Spatie\LaravelData\Support\Types\UndefinedType; +use Spatie\LaravelData\Support\OldTypes\OldType; /** * @property Collection $parameters @@ -20,84 +17,10 @@ public function __construct( public readonly bool $isStatic, public readonly bool $isPublic, public readonly CustomCreationMethodType $customCreationMethodType, - public readonly Type $returnType, + public readonly ?DataReturnType $returnType, ) { } - public static function create(ReflectionMethod $method): self - { - $returnType = Type::forReflection( - $method->getReturnType(), - $method->class, - ); - - return new self( - $method->name, - collect($method->getParameters())->map( - fn (ReflectionParameter $parameter) => DataParameter::create($parameter, $method->class), - ), - $method->isStatic(), - $method->isPublic(), - self::resolveCustomCreationMethodType($method, $returnType), - $returnType - ); - } - - public static function createConstructor(?ReflectionMethod $method, Collection $properties): ?self - { - if ($method === null) { - return null; - } - - $parameters = collect($method->getParameters()) - ->map(function (ReflectionParameter $parameter) use ($method, $properties) { - if (! $parameter->isPromoted()) { - return DataParameter::create($parameter, $method->class); - } - - if ($properties->has($parameter->name)) { - return $properties->get($parameter->name); - } - - return null; - }) - ->filter() - ->values(); - - return new self( - '__construct', - $parameters, - false, - $method->isPublic(), - CustomCreationMethodType::None, - new UndefinedType(), - ); - } - - protected static function resolveCustomCreationMethodType( - ReflectionMethod $method, - ?Type $returnType, - ): CustomCreationMethodType { - if (! $method->isStatic() - || ! $method->isPublic() - || $method->name === 'from' - || $method->name === 'collect' - || $method->name === 'collection' - ) { - return CustomCreationMethodType::None; - } - - if (str_starts_with($method->name, 'from')) { - return CustomCreationMethodType::Object; - } - - if (str_starts_with($method->name, 'collect') && ! $returnType instanceof UndefinedType) { - return CustomCreationMethodType::Collection; - } - - return CustomCreationMethodType::None; - } - public function accepts(mixed ...$input): bool { /** @var Collection $parameters */ @@ -106,7 +29,7 @@ public function accepts(mixed ...$input): bool : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); $parameters = $parameters->reject( - fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->isCreationContext + fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->type->type->isCreationContext() ); if (count($input) > $parameters->count()) { @@ -126,7 +49,7 @@ public function accepts(mixed ...$input): bool if ( $parameter instanceof DataProperty - && ! $parameter->type->type->acceptsValue($input[$index]) + && ! $parameter->type->acceptsValue($input[$index]) ) { return false; } @@ -144,6 +67,6 @@ public function accepts(mixed ...$input): bool public function returns(string $type): bool { - return $this->returnType->acceptsType($type); + return $this->returnType?->acceptsType($type) ?? false; } } diff --git a/src/Support/DataParameter.php b/src/Support/DataParameter.php index 8643763da..b0034911f 100644 --- a/src/Support/DataParameter.php +++ b/src/Support/DataParameter.php @@ -4,8 +4,8 @@ use ReflectionParameter; use Spatie\LaravelData\Support\Creation\CreationContext; -use Spatie\LaravelData\Support\Types\SingleType; -use Spatie\LaravelData\Support\Types\Type; +use Spatie\LaravelData\Support\OldTypes\SingleOldType; +use Spatie\LaravelData\Support\OldTypes\OldType; class DataParameter { @@ -14,27 +14,7 @@ public function __construct( public readonly bool $isPromoted, public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, - public readonly Type $type, - // TODO: would be better if we refactor this to type, together with Castable, Lazy, etc - public readonly bool $isCreationContext, + public readonly DataType $type, ) { } - - public static function create( - ReflectionParameter $parameter, - string $class, - ): self { - $hasDefaultValue = $parameter->isDefaultValueAvailable(); - - $type = Type::forReflection($parameter->getType(), $class); - - return new self( - $parameter->name, - $parameter->isPromoted(), - $hasDefaultValue, - $hasDefaultValue ? $parameter->getDefaultValue() : null, - $type, - $type instanceof SingleType && $type->type->name === CreationContext::class - ); - } } diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 1a8b35c8f..661f284d6 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -16,6 +16,7 @@ use Spatie\LaravelData\Resolvers\NameMappersResolver; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Factories\DataTypeFactory; +use Spatie\LaravelData\Support\Factories\OldDataTypeFactory; use Spatie\LaravelData\Transformers\Transformer; /** @@ -41,59 +42,4 @@ public function __construct( public readonly Collection $attributes, ) { } - - public static function create( - ReflectionProperty $property, - bool $hasDefaultValue = false, - mixed $defaultValue = null, - ?NameMapper $classInputNameMapper = null, - ?NameMapper $classOutputNameMapper = null, - ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, - ): self { - $attributes = collect($property->getAttributes()) - ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) - ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); - - $mappers = NameMappersResolver::create()->execute($attributes); - - $inputMappedName = match (true) { - $mappers['inputNameMapper'] !== null => $mappers['inputNameMapper']->map($property->name), - $classInputNameMapper !== null => $classInputNameMapper->map($property->name), - default => null, - }; - - $outputMappedName = match (true) { - $mappers['outputNameMapper'] !== null => $mappers['outputNameMapper']->map($property->name), - $classOutputNameMapper !== null => $classOutputNameMapper->map($property->name), - default => null, - }; - - $computed = $attributes->contains( - fn (object $attribute) => $attribute instanceof Computed - ); - - $hidden = $attributes->contains( - fn (object $attribute) => $attribute instanceof Hidden - ); - - return new self( - name: $property->name, - className: $property->class, - type: DataTypeFactory::create()->build($property, $classDefinedDataCollectableAnnotation), - validate: ! $attributes->contains( - fn (object $attribute) => $attribute instanceof WithoutValidation - ) && ! $computed, - computed: $computed, - hidden: $hidden, - isPromoted: $property->isPromoted(), - isReadonly: $property->isReadOnly(), - hasDefaultValue: $property->isPromoted() ? $hasDefaultValue : $property->hasDefaultValue(), - defaultValue: $property->isPromoted() ? $defaultValue : $property->getDefaultValue(), - cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(), - transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer || $attribute instanceof WithCastAndTransformer)?->get(), - inputMappedName: $inputMappedName, - outputMappedName: $outputMappedName, - attributes: $attributes, - ); - } } diff --git a/src/Support/DataReturnType.php b/src/Support/DataReturnType.php new file mode 100644 index 000000000..3ad50e1c1 --- /dev/null +++ b/src/Support/DataReturnType.php @@ -0,0 +1,20 @@ +type->acceptsType($type); + } +} diff --git a/src/Support/DataType.php b/src/Support/DataType.php index ab669c054..be41ef0e2 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -2,37 +2,78 @@ namespace Spatie\LaravelData\Support; -use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\DataTypeKind; +use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Support\Types\Type; +/** + * @template T of Type + */ class DataType { /** - * @param Type $type - * @param string|null $lazyType - * @param bool $isOptional - * @param DataTypeKind $kind - * @param class-string|null $dataClass - * @param string|null $dataCollectableClass + * @param class-string|null $lazyType */ public function __construct( public readonly Type $type, - public readonly ?string $lazyType, public readonly bool $isOptional, + public readonly bool $isNullable, + public readonly bool $isMixed, + public readonly ?string $lazyType, + // @note for now we have a one data type per type rule + // Meaning a type can be a data object of some type, data collection of some type or something else + // If we want to support multiple types in the future all we need to do is replace calls to these + // properties and handle everything correctly public readonly DataTypeKind $kind, public readonly ?string $dataClass, public readonly ?string $dataCollectableClass, ) { + + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + return $this->type->findAcceptedTypeForBaseType($class); } - public function isNullable(): bool + public function acceptsType(string $type): bool { - return $this->type->isNullable; + if ($this->isMixed) { + return true; + } + + return $this->type->acceptsType($type); } - public function isMixed(): bool + public function getAcceptedTypes(): array { - return $this->type->isMixed; + if($this->isMixed) { + return []; + } + + return $this->type->getAcceptedTypes(); + } + + public function acceptsValue(mixed $value): bool + { + if ($this->isMixed) { + return true; + } + + if ($this->isNullable && $value === null) { + return true; + } + + $type = gettype($value); + + $type = match ($type) { + 'integer' => 'int', + 'boolean' => 'bool', + 'double' => 'float', + 'object' => $value::class, + default => $type, + }; + + return $this->type->acceptsType($type); } } diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php new file mode 100644 index 000000000..d233691f0 --- /dev/null +++ b/src/Support/Factories/DataClassFactory.php @@ -0,0 +1,200 @@ + $name */ + $name = $reflectionClass->name; + + $attributes = $this->resolveAttributes($reflectionClass); + + $methods = collect($reflectionClass->getMethods()); + + $constructorReflectionMethod = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor()); + + $dataCollectablePropertyAnnotations = $this->dataCollectableAnnotationReader->getForClass($reflectionClass); + + if ($constructorReflectionMethod) { + $dataCollectablePropertyAnnotations = array_merge( + $dataCollectablePropertyAnnotations, + $this->dataCollectableAnnotationReader->getForMethod($constructorReflectionMethod) + ); + } + + $properties = $this->resolveProperties( + $reflectionClass, + $constructorReflectionMethod, + NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes), + $dataCollectablePropertyAnnotations, + ); + + $responsable = $reflectionClass->implementsInterface(ResponsableData::class); + + $outputMappedProperties = new LazyDataStructureProperty( + fn () => $properties + ->map(fn (DataProperty $property) => $property->outputMappedName) + ->filter() + ->flip() + ->toArray() + ); + + $constructor = $constructorReflectionMethod + ? $this->methodFactory->buildConstructor($constructorReflectionMethod, $reflectionClass, $properties) + : null; + + return new DataClass( + name: $reflectionClass->name, + properties: $properties, + methods: $this->resolveMethods($reflectionClass), + constructorMethod: $constructor, + isReadonly: method_exists($reflectionClass, 'isReadOnly') && $reflectionClass->isReadOnly(), + isAbstract: $reflectionClass->isAbstract(), + appendable: $reflectionClass->implementsInterface(AppendableData::class), + includeable: $reflectionClass->implementsInterface(IncludeableData::class), + responsable: $responsable, + transformable: $reflectionClass->implementsInterface(TransformableData::class), + validateable: $reflectionClass->implementsInterface(ValidateableData::class), + wrappable: $reflectionClass->implementsInterface(WrappableData::class), + emptyData: $reflectionClass->implementsInterface(EmptyData::class), + attributes: $attributes, + dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, + allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, + allowedRequestExcludes: $responsable ? $name::allowedRequestExcludes() : null, + allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, + allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, + outputMappedProperties: $outputMappedProperties, + transformationFields: static::resolveTransformationFields($properties), + ); + } + + protected function resolveAttributes( + ReflectionClass $reflectionClass + ): Collection { + $attributes = collect($reflectionClass->getAttributes()) + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + + $parent = $reflectionClass->getParentClass(); + + if ($parent !== false) { + $attributes = $attributes->merge(static::resolveAttributes($parent)); + } + + return $attributes; + } + + protected function resolveMethods( + ReflectionClass $reflectionClass, + ): Collection { + return collect($reflectionClass->getMethods()) + ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'from') || str_starts_with($reflectionMethod->name, 'collect')) + ->reject(fn (ReflectionMethod $reflectionMethod) => in_array($reflectionMethod->name, ['from', 'collect', 'collection'])) + ->mapWithKeys( + fn (ReflectionMethod $reflectionMethod) => [$reflectionMethod->name => $this->methodFactory->build($reflectionMethod, $reflectionClass)], + ); + } + + protected function resolveProperties( + ReflectionClass $reflectionClass, + ?ReflectionMethod $constructorReflectionMethod, + array $mappers, + array $dataCollectablePropertyAnnotations, + ): Collection { + $defaultValues = $this->resolveDefaultValues($reflectionClass, $constructorReflectionMethod); + + return collect($reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC)) + ->reject(fn (ReflectionProperty $property) => $property->isStatic()) + ->values() + ->mapWithKeys(fn (ReflectionProperty $property) => [ + $property->name => $this->propertyFactory->build( + $property, + $reflectionClass, + array_key_exists($property->getName(), $defaultValues), + $defaultValues[$property->getName()] ?? null, + $mappers['inputNameMapper'], + $mappers['outputNameMapper'], + $dataCollectablePropertyAnnotations[$property->getName()] ?? null, + ), + ]); + } + + protected function resolveDefaultValues( + ReflectionClass $reflectionClass, + ?ReflectionMethod $constructorReflectionMethod, + ): array { + if (! $constructorReflectionMethod) { + return $reflectionClass->getDefaultProperties(); + } + + $values = collect($constructorReflectionMethod->getParameters()) + ->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) + ->mapWithKeys(fn (ReflectionParameter $parameter) => [ + $parameter->name => $parameter->getDefaultValue(), + ]) + ->toArray(); + + return array_merge( + $reflectionClass->getDefaultProperties(), + $values + ); + } + + /** + * @param Collection $properties + * + * @return LazyDataStructureProperty> + */ + protected function resolveTransformationFields( + Collection $properties, + ): LazyDataStructureProperty { + $closure = fn () => $properties + ->reject(fn (DataProperty $property): bool => $property->hidden) + ->map(function (DataProperty $property): null|bool { + if ( + $property->type->kind->isDataCollectable() + || $property->type->kind->isDataObject() + || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) + ) { + return true; + } + + return null; + }) + ->all(); + + return new LazyDataStructureProperty($closure); + } +} diff --git a/src/Support/Factories/DataMethodFactory.php b/src/Support/Factories/DataMethodFactory.php new file mode 100644 index 000000000..c1f386211 --- /dev/null +++ b/src/Support/Factories/DataMethodFactory.php @@ -0,0 +1,98 @@ +getReturnType() + ? $this->returnTypeFactory->build($reflectionMethod->getReturnType()) + : null; + + return new DataMethod( + name: $reflectionMethod->name, + parameters: collect($reflectionMethod->getParameters())->map( + fn (ReflectionParameter $parameter) => $this->parameterFactory->build($parameter, $reflectionClass), + ), + isStatic: $reflectionMethod->isStatic(), + isPublic: $reflectionMethod->isPublic(), + customCreationMethodType: $this->resolveCustomCreationMethodType($reflectionMethod, $returnType), + returnType: $returnType + ); + } + + public function buildConstructor( + ReflectionMethod $reflectionMethod, + ReflectionClass $reflectionClass, + Collection $properties + ): DataMethod { + $parameters = collect($reflectionMethod->getParameters()) + ->map(function (ReflectionParameter $parameter) use ($reflectionClass, $properties) { + if (! $parameter->isPromoted()) { + return $this->parameterFactory->build($parameter, $reflectionClass); + } + + if ($properties->has($parameter->name)) { + return $properties->get($parameter->name); + } + + return null; + }) + ->filter() + ->values(); + + return new DataMethod( + name: '__construct', + parameters: $parameters, + isStatic: false, + isPublic: $reflectionMethod->isPublic(), + customCreationMethodType: CustomCreationMethodType::None, + returnType: null, + ); + } + + protected function resolveCustomCreationMethodType( + ReflectionMethod $method, + ?DataReturnType $returnType, + ): CustomCreationMethodType { + if (! $method->isStatic() + || ! $method->isPublic() + || $method->name === 'from' + || $method->name === 'collect' + || $method->name === 'collection' + ) { + return CustomCreationMethodType::None; + } + + if (str_starts_with($method->name, 'from')) { + return CustomCreationMethodType::Object; + } + + if (str_starts_with($method->name, 'collect') && $returnType?->kind->isDataCollectable()) { + return CustomCreationMethodType::Collection; + } + + return CustomCreationMethodType::None; + } +} diff --git a/src/Support/Factories/DataParameterFactory.php b/src/Support/Factories/DataParameterFactory.php new file mode 100644 index 000000000..3c747c024 --- /dev/null +++ b/src/Support/Factories/DataParameterFactory.php @@ -0,0 +1,34 @@ +isDefaultValueAvailable(); + + return new DataParameter( + $reflectionParameter->name, + $reflectionParameter->isPromoted(), + $hasDefaultValue, + $hasDefaultValue ? $reflectionParameter->getDefaultValue() : null, + $this->typeFactory->build( + $reflectionParameter->getType(), + $reflectionClass, + $reflectionParameter, + ), + ); + } +} diff --git a/src/Support/Factories/DataPropertyFactory.php b/src/Support/Factories/DataPropertyFactory.php new file mode 100644 index 000000000..509a9d7f1 --- /dev/null +++ b/src/Support/Factories/DataPropertyFactory.php @@ -0,0 +1,89 @@ +getAttributes()) + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + + $mappers = NameMappersResolver::create()->execute($attributes); + + $inputMappedName = match (true) { + $mappers['inputNameMapper'] !== null => $mappers['inputNameMapper']->map($reflectionProperty->name), + $classInputNameMapper !== null => $classInputNameMapper->map($reflectionProperty->name), + default => null, + }; + + $outputMappedName = match (true) { + $mappers['outputNameMapper'] !== null => $mappers['outputNameMapper']->map($reflectionProperty->name), + $classOutputNameMapper !== null => $classOutputNameMapper->map($reflectionProperty->name), + default => null, + }; + + $computed = $attributes->contains( + fn (object $attribute) => $attribute instanceof Computed + ); + + $hidden = $attributes->contains( + fn (object $attribute) => $attribute instanceof Hidden + ); + + $validate = ! $attributes->contains( + fn (object $attribute) => $attribute instanceof WithoutValidation + ) && ! $computed; + + return new DataProperty( + name: $reflectionProperty->name, + className: $reflectionProperty->class, + type: $this->typeFactory->build( + $reflectionProperty->getType(), + $reflectionClass, + $reflectionProperty, + $attributes, + $classDefinedDataCollectableAnnotation + ), + validate: $validate, + computed: $computed, + hidden: $hidden, + isPromoted: $reflectionProperty->isPromoted(), + isReadonly: $reflectionProperty->isReadOnly(), + hasDefaultValue: $reflectionProperty->isPromoted() ? $hasDefaultValue : $reflectionProperty->hasDefaultValue(), + defaultValue: $reflectionProperty->isPromoted() ? $defaultValue : $reflectionProperty->getDefaultValue(), + cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(), + transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer || $attribute instanceof WithCastAndTransformer)?->get(), + inputMappedName: $inputMappedName, + outputMappedName: $outputMappedName, + attributes: $attributes, + ); + } +} diff --git a/src/Support/Factories/DataReturnTypeFactory.php b/src/Support/Factories/DataReturnTypeFactory.php new file mode 100644 index 000000000..91ca4929a --- /dev/null +++ b/src/Support/Factories/DataReturnTypeFactory.php @@ -0,0 +1,53 @@ + */ + public static array $store = []; + + public function build(ReflectionType $type): DataReturnType + { + if (! $type instanceof ReflectionNamedType) { + throw new TypeError('At the moment return types can only be of one type'); + } + + return $this->buildFromNamedType($type->getName()); + } + + public function buildFromNamedType(string $name): DataReturnType + { + if (array_key_exists($name, self::$store)) { + return self::$store[$name]; + } + + $builtIn = in_array($name, ['array', 'bool', 'float', 'int', 'string', 'mixed', 'null']); + + ['acceptedTypes' => $acceptedTypes, 'kind' => $kind] = AcceptedTypesStorage::getAcceptedTypesAndKind($name); + + return static::$store[$name] = new DataReturnType( + type: new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: null, + dataCollectableClass: null, + ), + kind: $kind, + ); + } + + public function buildFromValue(mixed $value): DataReturnType + { + return self::buildFromNamedType(get_debug_type($value)); + } +} diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index 680ef05ad..4e53a374b 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -2,189 +2,185 @@ namespace Spatie\LaravelData\Support\Factories; -use Illuminate\Support\Arr; +use Exception; +use Illuminate\Support\Collection; +use ReflectionClass; use ReflectionIntersectionType; +use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; +use ReflectionType; use ReflectionUnionType; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotFindDataClass; -use Spatie\LaravelData\Exceptions\InvalidDataType; +use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\Factories\Concerns\RequiresTypeInformation; +use Spatie\LaravelData\Support\Lazy\ClosureLazy; +use Spatie\LaravelData\Support\Lazy\ConditionalLazy; +use Spatie\LaravelData\Support\Lazy\DefaultLazy; +use Spatie\LaravelData\Support\Lazy\InertiaLazy; +use Spatie\LaravelData\Support\Lazy\RelationalLazy; use Spatie\LaravelData\Support\Types\IntersectionType; -use Spatie\LaravelData\Support\Types\PartialType; -use Spatie\LaravelData\Support\Types\SingleType; -use Spatie\LaravelData\Support\Types\UndefinedType; +use Spatie\LaravelData\Support\Types\NamedType; +use Spatie\LaravelData\Support\Types\Storage\AcceptedTypesStorage; +use Spatie\LaravelData\Support\Types\Type; use Spatie\LaravelData\Support\Types\UnionType; use TypeError; class DataTypeFactory { - public static function create(): self - { - return new self(); + public function __construct( + protected DataCollectableAnnotationReader $dataCollectableAnnotationReader, + ) { } public function build( - ReflectionParameter|ReflectionProperty $property, + ?ReflectionType $reflectionType, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes = null, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, ): DataType { - $type = $property->getType(); - - $class = match ($property::class) { - ReflectionParameter::class => $property->getDeclaringClass()?->name, - ReflectionProperty::class => $property->class, - }; - - return match (true) { - $type === null => $this->buildForEmptyType(), - $type instanceof ReflectionNamedType => $this->buildForNamedType( - $property, - $type, - $class, + $properties = match (true) { + $reflectionType === null => $this->inferPropertiesForNoneType(), + $reflectionType instanceof ReflectionNamedType => $this->inferPropertiesForSingleType( + $reflectionType, + $reflectionClass, + $typeable, + $attributes, $classDefinedDataCollectableAnnotation ), - $type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType => $this->buildForMultiType( - $property, - $type, - $class, + $reflectionType instanceof ReflectionUnionType || $reflectionType instanceof ReflectionIntersectionType => $this->inferPropertiesForCombinationType( + $reflectionType, + $reflectionClass, + $typeable, + $attributes, $classDefinedDataCollectableAnnotation ), default => throw new TypeError('Invalid reflection type') }; + + return new DataType( + type: $properties['type'], + isOptional: $properties['isOptional'], + isNullable: $reflectionType?->allowsNull() ?? true, + isMixed: $properties['isMixed'], + lazyType: $properties['lazyType'], + kind: $properties['kind'], + dataClass: $properties['dataClass'], + dataCollectableClass: $properties['dataCollectableClass'], + ); } - protected function buildForEmptyType(): DataType + /** + * @return array{ + * type: NamedType, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + */ + protected function inferPropertiesForNoneType(): array { - return new DataType( - new UndefinedType(), - null, - false, - DataTypeKind::Default, - null, - null + $type = new NamedType( + name: 'mixed', + builtIn: true, + acceptedTypes: [], + kind: DataTypeKind::Default, + dataClass: null, + dataCollectableClass: null, ); + + return [ + 'type' => $type, + 'isMixed' => true, + 'isOptional' => false, + 'lazyType' => null, + 'kind' => DataTypeKind::Default, + 'dataClass' => null, + 'dataCollectableClass' => null, + ]; } - protected function buildForNamedType( - ReflectionParameter|ReflectionProperty $reflectionProperty, + /** + * @return array{ + * type: NamedType, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + * + */ + protected function inferPropertiesForSingleType( ReflectionNamedType $reflectionType, - ?string $class, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, - ): DataType { - $type = SingleType::create($reflectionType, $class); - - if ($type->type->isLazy()) { - throw InvalidDataType::onlyLazy($reflectionProperty); - } - - if ($type->type->isOptional()) { - throw InvalidDataType::onlyOptional($reflectionProperty); - } - - $kind = DataTypeKind::Default; - $dataClass = null; - $dataCollectableClass = null; - - if (! $type->isMixed) { - [ - 'kind' => $kind, - 'dataClass' => $dataClass, - 'dataCollectableClass' => $dataCollectableClass, - ] = $this->resolveDataSpecificProperties( - $reflectionProperty, - $type->type, + ): array { + return [ + ...$this->inferPropertiesForNamedType( + $reflectionType->getName(), + $reflectionType->isBuiltin(), + $reflectionClass, + $typeable, + $attributes, $classDefinedDataCollectableAnnotation - ); - } - - return new DataType( - type: $type, - lazyType: null, - isOptional: false, - kind: $kind, - dataClass: $dataClass, - dataCollectableClass: $dataCollectableClass - ); + ), + 'isOptional' => false, + 'lazyType' => null, + ]; } - protected function buildForMultiType( - ReflectionParameter|ReflectionProperty $reflectionProperty, - ReflectionUnionType|ReflectionIntersectionType $multiReflectionType, - ?string $class, + /** + * @return array{ + * type: NamedType, + * isMixed: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + */ + protected function inferPropertiesForNamedType( + string $name, + bool $builtIn, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, - ): DataType { - $type = match ($multiReflectionType::class) { - ReflectionUnionType::class => UnionType::create($multiReflectionType, $class), - ReflectionIntersectionType::class => IntersectionType::create($multiReflectionType, $class), - }; - - $isOptional = false; - $kind = DataTypeKind::Default; - $dataClass = null; - $dataCollectableClass = null; - $lazyType = null; - - - foreach ($type->types as $subType) { - if($subType->isLazy()) { - $lazyType = $subType->name; - } - - $isOptional = $isOptional || $subType->isOptional(); - - if (($subType->builtIn === false || $subType->name === 'array') - && $subType->isLazy() === false - && $subType->isOptional() === false - ) { - if ($kind !== DataTypeKind::Default) { - continue; - } - - [ - 'kind' => $kind, - 'dataClass' => $dataClass, - 'dataCollectableClass' => $dataCollectableClass, - ] = $this->resolveDataSpecificProperties( - $reflectionProperty, - $subType, - $classDefinedDataCollectableAnnotation - ); - } - } - - if ($kind->isDataObject() && $type->acceptedTypesCount() > 1) { - throw InvalidDataType::unionWithData($reflectionProperty); + ): array { + if ($name === 'self' || $name === 'static') { + $name = is_string($reflectionClass) ? $reflectionClass : $reflectionClass->getName(); } - if ($kind->isDataCollectable() && $type->acceptedTypesCount() > 1) { - throw InvalidDataType::unionWithDataCollection($reflectionProperty); - } + $isMixed = $name === 'mixed'; - return new DataType( - type: $type, - lazyType: $lazyType, - isOptional: $isOptional, - kind: $kind, - dataClass: $dataClass, - dataCollectableClass: $dataCollectableClass, - ); - } - - protected function resolveDataSpecificProperties( - ReflectionParameter|ReflectionProperty $reflectionProperty, - PartialType $partialType, - ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, - ): array { - $kind = $partialType->getDataTypeKind(); + ['acceptedTypes' => $acceptedTypes, 'kind' => $kind] = AcceptedTypesStorage::getAcceptedTypesAndKind($name); if ($kind === DataTypeKind::Default) { return [ - 'kind' => DataTypeKind::Default, + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: null, + dataCollectableClass: null, + ), + 'isMixed' => $isMixed, + 'kind' => $kind, 'dataClass' => null, 'dataCollectableClass' => null, ]; @@ -192,47 +188,177 @@ protected function resolveDataSpecificProperties( if ($kind === DataTypeKind::DataObject) { return [ - 'kind' => DataTypeKind::DataObject, - 'dataClass' => $partialType->name, + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: $name, + dataCollectableClass: null, + ), + 'isMixed' => $isMixed, + 'kind' => $kind, + 'dataClass' => $name, 'dataCollectableClass' => null, ]; } - $dataClass = null; - - $attributes = $reflectionProperty instanceof ReflectionProperty - ? $reflectionProperty->getAttributes(DataCollectionOf::class) - : []; + /** @var ?DataCollectionOf $dataCollectionOfAttribute */ + $dataCollectionOfAttribute = $attributes?->first( + fn (object $attribute) => $attribute instanceof DataCollectionOf + ); - if ($attribute = Arr::first($attributes)) { - $dataClass = $attribute->getArguments()[0]; - } + $dataClass = $dataCollectionOfAttribute?->class; $dataClass ??= $classDefinedDataCollectableAnnotation?->dataClass; - $dataClass ??= $reflectionProperty instanceof ReflectionProperty - ? DataCollectableAnnotationReader::create()->getForProperty($reflectionProperty)?->dataClass + $dataClass ??= $typeable instanceof ReflectionProperty + ? $this->dataCollectableAnnotationReader->getForProperty($typeable)?->dataClass : null; if ($dataClass !== null) { return [ + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: $dataClass, + dataCollectableClass: $name, + ), + 'isMixed' => $isMixed, 'kind' => $kind, 'dataClass' => $dataClass, - 'dataCollectableClass' => $partialType->name, + 'dataCollectableClass' => $name, ]; } if (in_array($kind, [DataTypeKind::Array, DataTypeKind::Paginator, DataTypeKind::CursorPaginator, DataTypeKind::Enumerable])) { return [ + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: DataTypeKind::Default, + dataClass: null, + dataCollectableClass: null, + ), + 'isMixed' => $isMixed, 'kind' => DataTypeKind::Default, 'dataClass' => null, 'dataCollectableClass' => null, ]; } - throw CannotFindDataClass::missingDataCollectionAnotation( - $reflectionProperty instanceof ReflectionProperty ? $reflectionProperty->class : 'unknown', - $reflectionProperty->name - ); + throw CannotFindDataClass::forTypeable($typeable); + } + + /** + * @return array{ + * type: Type, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + * + */ + protected function inferPropertiesForCombinationType( + ReflectionUnionType|ReflectionIntersectionType $reflectionType, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + ): array { + $isMixed = false; + $isOptional = false; + $lazyType = null; + + $kind = null; + $dataClass = null; + $dataCollectableClass = null; + + $subTypes = []; + + foreach ($reflectionType->getTypes() as $reflectionSubType) { + if ($reflectionSubType::class === ReflectionUnionType::class || $reflectionSubType::class === ReflectionIntersectionType::class) { + $properties = $this->inferPropertiesForCombinationType( + $reflectionSubType, + $reflectionClass, + $typeable, + $attributes, + $classDefinedDataCollectableAnnotation + ); + + $isMixed = $isMixed || $properties['isMixed']; + $isOptional = $isOptional || $properties['isOptional']; + $lazyType = $lazyType ?? $properties['lazyType']; + + $kind ??= $properties['kind']; + $dataClass ??= $properties['dataClass']; + $dataCollectableClass ??= $properties['dataCollectableClass']; + + $subTypes[] = $properties['type']; + + continue; + } + + /** @var ReflectionNamedType $reflectionSubType */ + + $name = $reflectionSubType->getName(); + + if ($name === Optional::class) { + $isOptional = true; + + continue; + } + + if ($name === 'null') { + continue; + } + + if (in_array($name, [Lazy::class, DefaultLazy::class, ClosureLazy::class, ConditionalLazy::class, RelationalLazy::class, InertiaLazy::class])) { + $lazyType = $name; + + continue; + } + + $properties = $this->inferPropertiesForNamedType( + $reflectionSubType->getName(), + $reflectionSubType->isBuiltin(), + $reflectionClass, + $typeable, + $attributes, + $classDefinedDataCollectableAnnotation + ); + + $isMixed = $isMixed || $properties['isMixed']; + + $kind ??= $properties['kind']; + $dataClass ??= $properties['dataClass']; + $dataCollectableClass ??= $properties['dataCollectableClass']; + + $subTypes[] = $properties['type']; + } + + $type = match (true) { + count($subTypes) === 0 => throw new Exception('Invalid reflected type'), + count($subTypes) === 1 => $subTypes[0], + $reflectionType::class === ReflectionUnionType::class => new UnionType($subTypes), + $reflectionType::class === ReflectionIntersectionType::class => new IntersectionType($subTypes), + default => throw new Exception('Invalid reflected type'), + }; + + return [ + 'type' => $type, + 'isMixed' => $isMixed, + 'isOptional' => $isOptional, + 'lazyType' => $lazyType, + 'kind' => $kind, + 'dataClass' => $dataClass, + 'dataCollectableClass' => $dataCollectableClass, + ]; } } diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 504d14c19..801542554 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -114,7 +114,7 @@ protected function resolveTypeForProperty( default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') }; - if ($dataProperty->type->isNullable()) { + if ($dataProperty->type->isNullable) { return new Nullable($collectionType); } diff --git a/src/Support/Types/CombinationType.php b/src/Support/Types/CombinationType.php new file mode 100644 index 000000000..18bb95fba --- /dev/null +++ b/src/Support/Types/CombinationType.php @@ -0,0 +1,32 @@ + $types + */ + public function __construct( + public readonly array $types, + ) { + } + + public function getAcceptedTypes(): array + { + $types = []; + + foreach ($this->types as $type) { + foreach ($type->getAcceptedTypes() as $name => $acceptedTypes) { + $types[$name] = $acceptedTypes; + } + } + + return $types; + } + + public function isCreationContext(): bool + { + return false; + } +} diff --git a/src/Support/Types/IntersectionType.php b/src/Support/Types/IntersectionType.php index e60408d83..295bcc681 100644 --- a/src/Support/Types/IntersectionType.php +++ b/src/Support/Types/IntersectionType.php @@ -2,14 +2,10 @@ namespace Spatie\LaravelData\Support\Types; -class IntersectionType extends MultiType +class IntersectionType extends CombinationType { public function acceptsType(string $type): bool { - if ($this->isMixed) { - return true; - } - foreach ($this->types as $subType) { if (! $subType->acceptsType($type)) { return false; diff --git a/src/Support/Types/MultiType.php b/src/Support/Types/MultiType.php deleted file mode 100644 index 2d2e8cf55..000000000 --- a/src/Support/Types/MultiType.php +++ /dev/null @@ -1,66 +0,0 @@ -allowsNull(); - $isMixed = false; - $types = []; - - foreach ($multiType->getTypes() as $type) { - if ($type->getName() === 'null') { - continue; - } - - if ($type->getName() === 'mixed') { - $isMixed = true; - } - - $types[] = PartialType::create($type, $class); - } - - return new static( - $isNullable, - $isMixed, - $types - ); - } - - public function getAcceptedTypes(): array - { - $types = []; - - foreach ($this->types as $type) { - $types[$type->name] = $type->acceptedTypes; - } - - return $types; - } - - public function acceptedTypesCount(): int - { - return count(array_filter( - $this->types, - fn (PartialType $subType) => ! $subType->isLazy() && ! $subType->isOptional() - )); - } -} diff --git a/src/Support/Types/NamedType.php b/src/Support/Types/NamedType.php new file mode 100644 index 000000000..7666d187f --- /dev/null +++ b/src/Support/Types/NamedType.php @@ -0,0 +1,75 @@ + $acceptedTypes + * @param DataTypeKind $kind + * @param class-string|null $dataClass + * @param string|class-string|null $dataCollectableClass + */ + public function __construct( + public readonly string $name, + public readonly bool $builtIn, + public readonly array $acceptedTypes, + public readonly DataTypeKind $kind, + public readonly ?string $dataClass, + public readonly ?string $dataCollectableClass, + ) { + $this->isCastable = in_array(Castable::class, $this->acceptedTypes); + } + + public function acceptsType(string $type): bool + { + if ($type === $this->name) { + return true; + } + + if ($this->builtIn) { + return false; + } + + if (in_array($this->name, [$type, ...AcceptedTypesStorage::getAcceptedTypes($type)], true)) { + return true; + } + + return false; + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + if ($class === $this->name) { + return $class; + } + + if (in_array($class, $this->acceptedTypes)) { + return $this->name; + } + + return null; + } + + public function getAcceptedTypes(): array + { + return [ + $this->name => $this->acceptedTypes, + ]; + } + + public function isCreationContext(): bool + { + return $this->name === CreationContext::class; + } +} diff --git a/src/Support/Types/PartialType.php b/src/Support/Types/PartialType.php deleted file mode 100644 index c79a89388..000000000 --- a/src/Support/Types/PartialType.php +++ /dev/null @@ -1,137 +0,0 @@ -getName(); - - if ($typeName === 'mixed' || $type->isBuiltin()) { - return new self($typeName, true, []); - } - - if ($typeName === 'self' || $typeName === 'static') { - $typeName = $class; - } - - return new self( - name: $typeName, - builtIn: $type->isBuiltin(), - acceptedTypes: self::resolveAcceptedTypes($typeName) - ); - } - - public static function createFromTypeString(string $type): self - { - $builtIn = in_array($type, ['float', 'bool', 'int', 'array', 'string', 'mixed']); - - return new self( - name: $type, - builtIn: $builtIn, - acceptedTypes: ! $builtIn - ? self::resolveAcceptedTypes($type) - : [] - ); - } - - public static function createFromValue(mixed $value): self - { - return self::createFromTypeString(get_debug_type($value)); - } - - public function acceptsType(string $type): bool - { - if ($type === $this->name) { - return true; - } - - if ($this->builtIn) { - return false; - } - - // TODO: move this to some store for caching? - $baseTypes = class_exists($type) - ? array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]) - : []; - - if (in_array($this->name, [$type, ...$baseTypes], true)) { - return true; - } - - return false; - } - - public function findAcceptedTypeForBaseType(string $class): ?string - { - if ($class === $this->name) { - return $class; - } - - if (in_array($class, $this->acceptedTypes)) { - return $this->name; - } - - return null; - } - - public function isLazy(): bool - { - return $this->name === Lazy::class || in_array(Lazy::class, $this->acceptedTypes); - } - - public function isOptional(): bool - { - return $this->name === Optional::class || in_array(Optional::class, $this->acceptedTypes); - } - - public function getDataTypeKind(): DataTypeKind - { - return match (true) { - in_array(BaseData::class, $this->acceptedTypes) => DataTypeKind::DataObject, - $this->name === 'array' => DataTypeKind::Array, - in_array(Enumerable::class, $this->acceptedTypes) => DataTypeKind::Enumerable, - in_array(DataCollection::class, $this->acceptedTypes) || $this->name === DataCollection::class => DataTypeKind::DataCollection, - in_array(PaginatedDataCollection::class, $this->acceptedTypes) || $this->name === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, - in_array(CursorPaginatedDataCollection::class, $this->acceptedTypes) || $this->name === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, - in_array(Paginator::class, $this->acceptedTypes) || in_array(AbstractPaginator::class, $this->acceptedTypes) => DataTypeKind::Paginator, - in_array(CursorPaginator::class, $this->acceptedTypes) || in_array(AbstractCursorPaginator::class, $this->acceptedTypes) => DataTypeKind::CursorPaginator, - default => DataTypeKind::Default, - }; - } - - protected static function resolveAcceptedTypes(string $type): array - { - return array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]); - } -} diff --git a/src/Support/Types/SingleType.php b/src/Support/Types/SingleType.php deleted file mode 100644 index d81f19fd4..000000000 --- a/src/Support/Types/SingleType.php +++ /dev/null @@ -1,58 +0,0 @@ -getName() === 'null') { - throw new Exception('Cannot create a single null type'); - } - - return new self( - isNullable: $reflectionType->allowsNull(), - isMixed: $reflectionType->getName() === 'mixed', - type: PartialType::create($reflectionType, $class) - ); - } - - public function acceptsType(string $type): bool - { - if ($this->isMixed) { - return true; - } - - return $this->type->acceptsType($type); - } - - public function findAcceptedTypeForBaseType(string $class): ?string - { - return $this->type->findAcceptedTypeForBaseType($class); - } - - public function getAcceptedTypes(): array - { - if ($this->isMixed) { - return []; - } - - return [ - $this->type->name => $this->type->acceptedTypes, - ]; - } -} diff --git a/src/Support/Types/Storage/AcceptedTypesStorage.php b/src/Support/Types/Storage/AcceptedTypesStorage.php new file mode 100644 index 000000000..4af27c926 --- /dev/null +++ b/src/Support/Types/Storage/AcceptedTypesStorage.php @@ -0,0 +1,74 @@ + */ + public static array $acceptedTypes = []; + + /** @var array */ + public static array $acceptedKinds = []; + + /** @return array{acceptedTypes:string[], kind: DataTypeKind} */ + public static function getAcceptedTypesAndKind(string $name): array + { + $acceptedTypes = static::getAcceptedTypes($name); + + return [ + 'acceptedTypes' => $acceptedTypes, + 'kind' => static::$acceptedKinds[$name] ??= static::resolveDataTypeKind($name, $acceptedTypes), + ]; + } + + /** @return string[] */ + public static function getAcceptedTypes(string $name): array + { + return static::$acceptedTypes[$name] ??= static::resolveAcceptedTypes($name); + } + + /** @return string[] */ + protected static function resolveAcceptedTypes(string $name): array + { + if (! class_exists($name)) { + return []; + } + + return array_unique([ + ...array_values(class_parents($name)), + ...array_values(class_implements($name)), + ]); + } + + protected static function resolveDataTypeKind(string $name, array $acceptedTypes): DataTypeKind + { + return match (true) { + in_array(BaseData::class, $acceptedTypes) => DataTypeKind::DataObject, + $name === 'array' => DataTypeKind::Array, + in_array(Enumerable::class, $acceptedTypes) => DataTypeKind::Enumerable, + in_array(DataCollection::class, $acceptedTypes) || $name === DataCollection::class => DataTypeKind::DataCollection, + in_array(PaginatedDataCollection::class, $acceptedTypes) || $name === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, + in_array(CursorPaginatedDataCollection::class, $acceptedTypes) || $name === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, + in_array(Paginator::class, $acceptedTypes) || in_array(AbstractPaginator::class, $acceptedTypes) => DataTypeKind::Paginator, + in_array(CursorPaginator::class, $acceptedTypes) || in_array(AbstractCursorPaginator::class, $acceptedTypes) => DataTypeKind::CursorPaginator, + default => DataTypeKind::Default, + }; + } + + public static function reset(): void + { + static::$acceptedTypes = []; + static::$acceptedKinds = []; + } +} diff --git a/src/Support/Types/Type.php b/src/Support/Types/Type.php index 9259b8da9..3ec7d984f 100644 --- a/src/Support/Types/Type.php +++ b/src/Support/Types/Type.php @@ -2,54 +2,16 @@ namespace Spatie\LaravelData\Support\Types; -use ReflectionIntersectionType; -use ReflectionNamedType; -use ReflectionType; -use ReflectionUnionType; - abstract class Type { - public function __construct( - public readonly bool $isNullable, - public readonly bool $isMixed, - ) { - } - - public static function forReflection( - ?ReflectionType $type, - string $class, - ): self { - return match (true) { - $type instanceof ReflectionNamedType => SingleType::create($type, $class), - $type instanceof ReflectionUnionType => UnionType::create($type, $class), - $type instanceof ReflectionIntersectionType => IntersectionType::create($type, $class), - default => new UndefinedType(), - }; - } - abstract public function acceptsType(string $type): bool; abstract public function findAcceptedTypeForBaseType(string $class): ?string; - // TODO: remove this? + /** + * @return array> + */ abstract public function getAcceptedTypes(): array; - public function acceptsValue(mixed $value): bool - { - if ($this->isNullable && $value === null) { - return true; - } - - $type = gettype($value); - - $type = match ($type) { - 'integer' => 'int', - 'boolean' => 'bool', - 'double' => 'float', - 'object' => $value::class, - default => $type, - }; - - return $this->acceptsType($type); - } + abstract public function isCreationContext(): bool; } diff --git a/src/Support/Types/UndefinedType.php b/src/Support/Types/UndefinedType.php deleted file mode 100644 index 3792fe9f3..000000000 --- a/src/Support/Types/UndefinedType.php +++ /dev/null @@ -1,26 +0,0 @@ -isMixed) { - return true; - } - foreach ($this->types as $subType) { if ($subType->acceptsType($type)) { return true; diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 239abea90..5354f2240 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; it('can cast date times', function () { $caster = new DateTimeInterfaceCast('d-m-Y H:i:s'); @@ -23,9 +24,10 @@ public DateTimeImmutable $dateTimeImmutable; }; + expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -34,7 +36,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -43,7 +45,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -52,7 +54,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -69,7 +71,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -86,7 +88,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'int')), + FakeDataStructureFactory::property($class, 'int'), '1994-05-16 12:20:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -108,7 +110,7 @@ }; expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -117,7 +119,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -126,7 +128,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -135,7 +137,7 @@ ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -158,7 +160,7 @@ }; expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -167,7 +169,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -176,7 +178,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -185,7 +187,7 @@ ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() diff --git a/tests/Casts/EnumCastTest.php b/tests/Casts/EnumCastTest.php index a9ded990f..6b2858746 100644 --- a/tests/Casts/EnumCastTest.php +++ b/tests/Casts/EnumCastTest.php @@ -4,6 +4,7 @@ use Spatie\LaravelData\Casts\Uncastable; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\Enums\DummyUnitEnum; @@ -18,7 +19,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), 'foo', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -33,7 +34,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), 'bar', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -48,7 +49,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), 'foo', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -63,7 +64,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'int')), + FakeDataStructureFactory::property($class, 'int'), 'foo', collect(), CreationContextFactory::createFromConfig($class::class)->get(), diff --git a/tests/Factories/FakeDataStructureFactory.php b/tests/Factories/FakeDataStructureFactory.php new file mode 100644 index 000000000..82c325cf4 --- /dev/null +++ b/tests/Factories/FakeDataStructureFactory.php @@ -0,0 +1,92 @@ +build($class); + } + + public static function method( + ReflectionMethod $method, + ): DataMethod + { + $factory = static::$methodFactory ??= app(DataMethodFactory::class); + + return $factory->build($method, $method->getDeclaringClass()); + } + + public static function constructor( + ReflectionMethod $method, + Collection $properties + ): DataMethod + { + $factory = static::$methodFactory ??= app(DataMethodFactory::class); + + return $factory->buildConstructor($method, $method->getDeclaringClass(), $properties); + } + + public static function property( + object $class, + string $name, + ): DataProperty { + $reflectionClass = new ReflectionClass($class); + $reflectionProperty = new ReflectionProperty($class, $name); + + $factory = static::$propertyFactory ??= app(DataPropertyFactory::class); + + return $factory->build($reflectionProperty, $reflectionClass); + } + + public static function parameter( + ReflectionParameter $parameter, + ): DataParameter { + $factory = static::$parameterFactory ??= app(DataParameterFactory::class); + + return $factory->build($parameter, $parameter->getDeclaringClass()); + } + + public static function returnType( + ReflectionMethod $method, + ): ?DataType { + $factory = static::$returnTypeFactory ??= app(DataReturnTypeFactory::class); + + return $factory->build($method->getReturnType()); + } +} diff --git a/tests/RuleInferrers/RequiredRuleInferrerTest.php b/tests/RuleInferrers/RequiredRuleInferrerTest.php index c6dfd078a..654a7127a 100644 --- a/tests/RuleInferrers/RequiredRuleInferrerTest.php +++ b/tests/RuleInferrers/RequiredRuleInferrerTest.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\Support\Validation\RuleDenormalizer; use Spatie\LaravelData\Support\Validation\ValidationContext; use Spatie\LaravelData\Support\Validation\ValidationPath; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; /** @@ -24,7 +25,7 @@ */ function getProperty(object $class) { - $dataClass = DataClass::create(new ReflectionClass($class)); + $dataClass = FakeDataStructureFactory::class($class); return $dataClass->properties->first(); } diff --git a/tests/Support/Caching/CachedDataConfigTest.php b/tests/Support/Caching/CachedDataConfigTest.php index 87f5b2b66..885acb733 100644 --- a/tests/Support/Caching/CachedDataConfigTest.php +++ b/tests/Support/Caching/CachedDataConfigTest.php @@ -6,6 +6,7 @@ use Spatie\LaravelData\Support\Caching\DataStructureCache; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; it('will use a cached data config if available', function () { @@ -41,7 +42,7 @@ }); it('will load cached data classes', function () { - $dataClass = DataClass::create(new ReflectionClass(SimpleData::class)); + $dataClass = FakeDataStructureFactory::class(SimpleData::class); $dataClass->prepareForCache(); $mock = Mockery::mock( diff --git a/tests/Support/DataClassTest.php b/tests/Support/DataClassTest.php index 61724a0cc..458af352a 100644 --- a/tests/Support/DataClassTest.php +++ b/tests/Support/DataClassTest.php @@ -8,13 +8,14 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataMethod; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMapper; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; it('keeps track of a global map from attribute', function () { - $dataClass = DataClass::create(new ReflectionClass(DataWithMapper::class)); + $dataClass = FakeDataStructureFactory::class(DataWithMapper::class); expect($dataClass->properties->get('casedProperty')->inputMappedName) ->toEqual('cased_property') @@ -23,7 +24,7 @@ }); it('will provide information about special methods', function () { - $class = DataClass::create(new ReflectionClass(SimpleData::class)); + $class = FakeDataStructureFactory::class(SimpleData::class); expect($class->methods)->toHaveKey('fromString') ->and($class->methods->get('fromString')) @@ -31,7 +32,7 @@ }); it('will provide information about the constructor', function () { - $class = DataClass::create(new ReflectionClass(SimpleData::class)); + $class = FakeDataStructureFactory::class(SimpleData::class); expect($class->constructorMethod) ->not->toBeNull() @@ -52,7 +53,7 @@ public function __construct( }; /** @var \Spatie\LaravelData\Support\DataProperty[] $properties */ - $properties = DataClass::create(new ReflectionClass($dataClass::class))->properties->values(); + $properties = FakeDataStructureFactory::class($dataClass::class)->properties->values(); expect($properties[0]) ->name->toEqual('property') @@ -97,7 +98,7 @@ public function __construct( } } - $dataClass = DataClass::create(new ReflectionClass(TestRecursiveAttributesChildData::class)); + $dataClass = FakeDataStructureFactory::class(TestRecursiveAttributesChildData::class); expect($dataClass->attributes) ->toHaveCount(3) diff --git a/tests/Support/DataMethodTest.php b/tests/Support/DataMethodTest.php index 23772d391..22b9e80ad 100644 --- a/tests/Support/DataMethodTest.php +++ b/tests/Support/DataMethodTest.php @@ -5,9 +5,9 @@ use Illuminate\Support\Enumerable; use Spatie\LaravelData\Data; use Spatie\LaravelData\Enums\CustomCreationMethodType; -use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMultipleArgumentCreationMethod; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -21,9 +21,9 @@ public function __construct( } }; - $method = DataMethod::createConstructor( + $method = FakeDataStructureFactory::constructor( new ReflectionMethod($class, '__construct'), - collect(['promotedProperty' => DataProperty::create(new ReflectionProperty($class, 'promotedProperty'))]) + collect(['promotedProperty' => FakeDataStructureFactory::property($class, 'promotedProperty')]), ); expect($method) @@ -44,7 +44,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->name->toEqual('fromString') @@ -63,7 +63,7 @@ public static function collectArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectArray')); expect($method) ->name->toEqual('collectArray') @@ -74,9 +74,7 @@ public static function collectArray( ->and($method->parameters[0])->toBeInstanceOf(DataParameter::class); expect($method->returnType) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toBe(['array' => []]); + ->type->getAcceptedTypes()->toBe(['array' => []]); }); it('can create a data method from a magic collect method with nullable return type', function () { @@ -87,15 +85,13 @@ public static function collectArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectArray')); expect($method) ->customCreationMethodType->toBe(CustomCreationMethodType::Collection); expect($method->returnType) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toBe(['array' => []]); + ->type->getAcceptedTypes()->toBe(['array' => []]); }); it('will not create a magical collection method when no return type specified', function () { @@ -106,15 +102,12 @@ public static function collectArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectArray')); expect($method) ->customCreationMethodType->toBe(CustomCreationMethodType::None); - expect($method->returnType) - ->isNullable->toBeTrue() - ->isMixed->toBeTrue() - ->getAcceptedTypes()->toBe([]); + expect($method->returnType)->toBeNull(); }); it('correctly accepts single values as magic creation method', function () { @@ -125,7 +118,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts('Hello')->toBeTrue() @@ -140,13 +133,13 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method->accepts(new SimpleData('Hello')))->toBeTrue(); }); it('correctly accepts multiple values as magic creation method', function () { - $method = DataMethod::create(new ReflectionMethod(DataWithMultipleArgumentCreationMethod::class, 'fromMultiple')); + $method = FakeDataStructureFactory::method(new ReflectionMethod(DataWithMultipleArgumentCreationMethod::class, 'fromMultiple')); expect($method) ->accepts('Hello', 42)->toBeTrue() @@ -165,7 +158,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts(new SimpleData('Hello'))->toBeTrue() @@ -180,7 +173,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts('Hello')->toBeTrue() @@ -196,7 +189,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts('Hello')->toBeTrue() @@ -213,7 +206,7 @@ public static function collectCollection( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollection')); expect($method->returns(Collection::class))->toBeTrue(); }); @@ -226,7 +219,7 @@ public static function collectCollection( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollection')); expect($method->returns(EloquentCollection::class))->toBeTrue(); }); @@ -239,7 +232,7 @@ public static function collectCollectionToArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollectionToArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollectionToArray')); expect($method->returns('array'))->toBeTrue(); }); @@ -253,7 +246,7 @@ public static function collectCollection( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollection')); expect($method->returns(Enumerable::class))->toBeFalse(); }); diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index bd23f78dd..6c3da41b9 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -4,7 +4,9 @@ use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataParameter; -use Spatie\LaravelData\Support\Types\Type; +use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\OldTypes\OldType; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; it('can create a data parameter', function () { @@ -20,57 +22,57 @@ public function __construct( }; $reflection = new ReflectionParameter([$class::class, '__construct'], 'nonPromoted'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('nonPromoted') ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'withoutType'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('withoutType') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'property'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('property') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'creationContext'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('creationContext') ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeTrue(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeTrue(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'propertyWithDefault'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('propertyWithDefault') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeTrue() ->defaultValue->toEqual('hello') - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); }); diff --git a/tests/Support/DataPropertyTest.php b/tests/Support/DataPropertyTest.php index 54f096fb7..0485a4660 100644 --- a/tests/Support/DataPropertyTest.php +++ b/tests/Support/DataPropertyTest.php @@ -13,6 +13,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Factories\DataPropertyFactory; use Spatie\LaravelData\Tests\Fakes\CastTransformers\FakeCastTransformer; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -24,8 +25,9 @@ function resolveHelper( mixed $defaultValue = null ): DataProperty { $reflectionProperty = new ReflectionProperty($class, 'property'); + $reflectionClass = new ReflectionClass($class); - return DataProperty::create($reflectionProperty, $hasDefaultValue, $defaultValue); + return app(DataPropertyFactory::class)->build($reflectionProperty, $reflectionClass, $hasDefaultValue, $defaultValue); } it('can get the cast attribute with arguments', function () { diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php new file mode 100644 index 000000000..c68b5ca42 --- /dev/null +++ b/tests/Support/DataReturnTypeTest.php @@ -0,0 +1,119 @@ +getReturnType(); + + expect($factory->build($reflection))->toEqual($expected); + + expect($factory->buildFromNamedType($typeName))->toEqual($expected); + + expect($factory->buildFromValue($value))->toEqual($expected); +})->with(function (){ + yield 'array' => [ + 'methodName' => 'array', + 'typeName' => 'array', + 'value' => [], + new DataReturnType( + type: new NamedType('array', true, [], DataTypeKind::Array, null, null), + kind: DataTypeKind::Array, + ), + ]; + + yield 'collection' => [ + 'methodName' => 'collection', + 'typeName' => Collection::class, + 'value' => collect(), + new DataReturnType( + type: new NamedType(Collection::class, false, [ + ArrayAccess::class, + CanBeEscapedWhenCastToString::class, + Enumerable::class, + Traversable::class, + Stringable::class, + JsonSerializable::class, + Jsonable::class, + IteratorAggregate::class, + Countable::class, + Arrayable::class, + ], DataTypeKind::Enumerable, null, null), + kind: DataTypeKind::Enumerable, + ), + ]; + + yield 'data collection' => [ + 'methodName' => 'dataCollection', + 'typeName' => DataCollection::class, + 'value' => new DataCollection(SimpleData::class, []), + new DataReturnType( + type: new NamedType(DataCollection::class, false, [ + DataCollectable::class, + ArrayAccess::class, + Traversable::class, + ContextableData::class, + Castable::class, + Arrayable::class, + Jsonable::class, + JsonSerializable::class, + Countable::class, + IteratorAggregate::class, + WrappableData::class, + IncludeableData::class, + TransformableData::class, + ResponsableData::class, + BaseDataCollectable::class, + Responsable::class + + ], DataTypeKind::DataCollection, null, null), + kind: DataTypeKind::DataCollection, + ), + ]; +}); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index 0d22cab0e..09e098317 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -27,12 +27,15 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; +use Spatie\LaravelData\Support\Types\IntersectionType; +use Spatie\LaravelData\Support\Types\NamedType; +use Spatie\LaravelData\Support\Types\UnionType; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -40,7 +43,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType { - $class = DataClass::create(new ReflectionClass($class)); + $class = FakeDataStructureFactory::class($class); return $class->properties->get($property)->type; } @@ -52,15 +55,21 @@ function resolveDataType(object $class, string $property = 'property'): DataType expect($type) ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeTrue() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->lazyType->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toBe([]); expect($type->type) - ->isMixed->toBeTrue() - ->isNullable->toBeTrue() - ->getAcceptedTypes()->toBe([]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('mixed') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a type with definition', function () { @@ -69,16 +78,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); expect($type->type) - ->isMixed->toBeFalse() - ->isNullable->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a nullable type with definition', function () { @@ -87,16 +102,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectionClass->toBeNull(); + ->dataCollectionClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); expect($type->type) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a union type definition', function () { @@ -105,16 +126,17 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string', 'int']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string', 'int']); + ->toBeInstanceOf(UnionType::class); }); it('can deduce a nullable union type definition', function () { @@ -123,16 +145,17 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string', 'int']); expect($type->type) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string', 'int']); + ->toBeInstanceOf(UnionType::class); }); it('can deduce an intersection type definition', function () { @@ -141,19 +164,42 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys([ + DateTime::class, + DateTimeImmutable::class, + ]); expect($type->type) - ->isNullable->toBeFalse() + ->toBeInstanceOf(IntersectionType::class); +}); + +it('can deduce a nullable intersection type definition', function () { + $type = resolveDataType(new class () { + public (DateTime & DateTimeImmutable)|null $property; + }); + + expect($type) + ->isOptional->toBeFalse() + ->isNullable->toBeTrue() ->isMixed->toBeFalse() + ->lazyType->toBeNull() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->getAcceptedTypes()->toHaveKeys([ DateTime::class, DateTimeImmutable::class, ]); + + expect($type->type) + ->toBeInstanceOf(IntersectionType::class); }); it('can deduce a mixed type', function () { @@ -162,16 +208,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeTrue() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toBeEmpty(); expect($type->type) - ->isNullable->toBeTrue() - ->isMixed->toBeTrue() - ->getAcceptedTypes()->toHaveKeys([]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('mixed') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a lazy type', function () { @@ -180,16 +232,23 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); + expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce an optional type', function () { @@ -198,40 +257,46 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeTrue() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); -test('a type cannot be optional alone', function () { - resolveDataType(new class () { - public Optional $property; - }); -})->throws(InvalidDataType::class); - it('can deduce a data type', function () { $type = resolveDataType(new class () { public SimpleData $property; }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataObject) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(SimpleData::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataObject) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBeNull(); }); it('can deduce a data union type', function () { @@ -240,16 +305,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataObject) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(SimpleData::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataObject) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBeNull(); }); it('can deduce a data collection type', function () { @@ -259,16 +330,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class); + ->dataCollectableClass->toBe(DataCollection::class) + ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(DataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); }); it('can deduce a data collection union type', function () { @@ -278,16 +355,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class); + ->dataCollectableClass->toBe(DataCollection::class) + ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(DataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); }); it('can deduce a paginated data collection type', function () { @@ -297,16 +380,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(PaginatedDataCollection::class); + ->dataCollectableClass->toBe(PaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(PaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(PaginatedDataCollection::class); }); it('can deduce a paginated data collection union type', function () { @@ -316,16 +405,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(PaginatedDataCollection::class); + ->dataCollectableClass->toBe(PaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(PaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(PaginatedDataCollection::class); }); it('can deduce a cursor paginated data collection type', function () { @@ -335,16 +430,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); }); it('can deduce a cursor paginated data collection union type', function () { @@ -354,16 +455,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); }); it('can deduce an array data collection type', function () { @@ -373,16 +480,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe('array'); + ->dataCollectableClass->toBe('array') + ->getAcceptedTypes()->toHaveKeys(['array']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['array']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('array') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array'); }); it('can deduce an array data collection union type', function () { @@ -392,16 +505,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe('array'); + ->dataCollectableClass->toBe('array') + ->getAcceptedTypes()->toHaveKeys(['array']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['array']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('array') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array'); }); it('can deduce an enumerable data collection type', function () { @@ -411,16 +530,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(Collection::class); + ->dataCollectableClass->toBe(Collection::class) + ->getAcceptedTypes()->toHaveKeys([Collection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([Collection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(Collection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Enumerable) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(Collection::class); }); it('can deduce an enumerable data collection union type', function () { @@ -430,16 +555,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(Collection::class); + ->dataCollectableClass->toBe(Collection::class) + ->getAcceptedTypes()->toHaveKeys([Collection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([Collection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(Collection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Enumerable) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(Collection::class); }); it('can deduce a paginator data collection type', function () { @@ -449,16 +580,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(LengthAwarePaginator::class); + ->dataCollectableClass->toBe(LengthAwarePaginator::class) + ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(LengthAwarePaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Paginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(LengthAwarePaginator::class); }); it('can deduce a paginator data collection union type', function () { @@ -468,16 +605,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(LengthAwarePaginator::class); + ->dataCollectableClass->toBe(LengthAwarePaginator::class) + ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(LengthAwarePaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Paginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(LengthAwarePaginator::class); }); it('can deduce a cursor paginator data collection type', function () { @@ -487,16 +630,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginator::class); + ->dataCollectableClass->toBe(CursorPaginator::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::CursorPaginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginator::class); }); it('can deduce a cursor paginator data collection union type', function () { @@ -506,41 +655,47 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginator::class); + ->dataCollectableClass->toBe(CursorPaginator::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::CursorPaginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginator::class); }); it('cannot have multiple data types', function () { resolveDataType(new class () { public SimpleData|ComplicatedData $property; }); -})->throws(InvalidDataType::class); +})->skip('Do we want to always check this?')->throws(InvalidDataType::class); it('cannot combine a data object and another type', function () { resolveDataType(new class () { public SimpleData|int $property; }); -})->throws(InvalidDataType::class); +})->skip('Do we want to always check this?')->throws(InvalidDataType::class); it('cannot combine a data collection and another type', function () { resolveDataType(new class () { #[DataCollectionOf(SimpleData::class)] public DataCollection|int $property; }); -})->throws(InvalidDataType::class); +})->skip('Do we want to always check this?')->throws(InvalidDataType::class); it( 'will resolve the base types for accepted types', function (object $class, array $expected) { - expect(resolveDataType($class)->type->getAcceptedTypes())->toEqualCanonicalizing($expected); + expect(resolveDataType($class)->getAcceptedTypes())->toEqualCanonicalizing($expected); } )->with(function () { yield 'no type' => [ @@ -619,7 +774,7 @@ function (object $class, array $expected) { it( 'can check if a data type accepts a type', function (object $class, string $type, bool $accepts) { - expect(resolveDataType($class))->type->acceptsType($type)->toEqual($accepts); + expect(resolveDataType($class))->acceptsType($type)->toEqual($accepts); } )->with(function () { // Base types @@ -772,7 +927,7 @@ function (object $class, string $type, bool $accepts) { it( 'can check if a data type accepts a value', function (object $class, mixed $value, bool $accepts) { - expect(resolveDataType($class))->type->acceptsValue($value)->toEqual($accepts); + expect(resolveDataType($class))->acceptsValue($value)->toEqual($accepts); } )->with(function () { yield [ @@ -844,7 +999,6 @@ function (object $class, mixed $value, bool $accepts) { 'can find accepted type for a base type', function (object $class, string $type, ?string $expectedType) { expect(resolveDataType($class)) - ->type ->findAcceptedTypeForBaseType($type) ->toEqual($expectedType); } diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index 865bbbb2d..cc260c11a 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -5,6 +5,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; it('can transform dates', function () { @@ -22,7 +23,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), new Carbon('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -30,7 +31,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), new CarbonImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -38,7 +39,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), new DateTime('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -46,7 +47,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), new DateTimeImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -68,7 +69,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), new Carbon('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -76,7 +77,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), new CarbonImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -84,7 +85,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), new DateTime('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -92,7 +93,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), new DateTimeImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -114,7 +115,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), new Carbon('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -122,7 +123,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), new CarbonImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -130,7 +131,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), new DateTime('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -138,7 +139,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), new DateTimeImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -156,7 +157,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), Carbon::createFromFormat('!Y-m-d', '1994-05-19'), TransformationContextFactory::create()->get($class) ) diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php index 32784cd01..8046c2775 100644 --- a/tests/Transformers/EnumTransformerTest.php +++ b/tests/Transformers/EnumTransformerTest.php @@ -3,6 +3,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Transformers\EnumTransformer; @@ -15,7 +16,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), $class->enum, TransformationContextFactory::create()->get($class) )