diff --git a/README.md b/README.md index a3b9986..5102593 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This package allows you to convert PHP classes to TypeScript. This class... ```php -/** @typescript */ +#[TypeScript] class User { public int $id; @@ -31,10 +31,10 @@ export type User = { Here's another example. ```php -class Languages extends Enum +enum Languages: string { - const TYPESCRIPT = 'typescript'; - const PHP = 'php'; + case TYPESCRIPT = 'typescript'; + case PHP = 'php'; } ``` diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..89aefb1 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,55 @@ +# Upgrading + +Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not +cover. We accept PRs to improve this guide. + +## Upgrading to v3 + +Version 3 is a complete rewrite of the package. That's why writing an upgrade guide is not that easy. The best way to +upgrade is to start reading the new docs and try to implement the new features. + +A few noticeable changes are: + +- Laravel installs now need to configure the package in a service provider instead of config file +- The package requires PHP 8.2 +- If you're using Laravel, v10 is minimally required +- Collectors were removed in favour of Transformers which decide whether a type should be transformed or not +- The transformer should now return a `Transformed` object when it can transform a type +- The transformer interface now should return `Untransformable` when it cannot transform the type +- The `DtoTransformer` was removed in favour of a more flexible transformer system where you can create your own transformers +- The `EnumTransformer` was rewritten to allow multiple types of enums to be transformed and multiple output structures +- All other enum transformers were removed +- The concept of `TypeProcessors` was removed, `ClassPropertyProcessor` is a kinda replacement for this +- The TypeReflectors were removed +- Support for inline types was removed +- If you were implementing your own attributes, you should now implement the `TypeScriptTypeAttributeContract` interface instead of `TypeScriptTransformableAttribute` +- The `RecordTypeScriptType` attribute was removed since deduction of these kinds of types is now done by the transformer +- The `TypeScriptTransformer` attribute was removed +- If you were implementing your own `Formatter`, please update the `format` method to now work on an array of files + +And so much more. Please read the docs for more information. + +## Upgrading to v2 + +- The package is now PHP 8 only +- The `ClassPropertyProcessor` interface was renamed to `TypeProcessor` and now takes a union of reflection objects +- In the config: + - `searchingPath` was renamed to `autoDiscoverTypes` + - `classPropertyReplacements` was renamed to `defaultTypeReplacements` +- Collectors now only have one method: `getTransformedType` which should + - return `null` when the collector cannot find a transformer + - return a `TransformedType` from a suitable transformer +- Transformers now only have one method: `transform` which should + - return `null` when the transformer cannot transform the class + - return a `TransformedType` if it can transform the class +- In Writers the `replaceMissingSymbols` method was removed and a `replacesSymbolsWithFullyQualifiedIdentifiers` with `bool` as return type was added +- The DTO transformer was completely rewritten, please take a look at the docs how to create you own +- The step classes are now renamed to actions + +Laravel +- In the Laravel config: + - `searching_path` is renamed to `auto_discover_types` + - `class_property_replacements` is renamed to `default_type_relacements` + - `writer` and `formatter` were added +- You should replace the `DefaultCollector::class` with the `DefaultCollector::class` +- It is not possible anymore to convert one file to TypeScript via command diff --git a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php index 9765514..200921f 100644 --- a/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php +++ b/src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php @@ -21,6 +21,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUndefined; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; class TranspileReflectionTypeToTypeScriptNodeAction { @@ -98,6 +99,10 @@ protected function reflectionNamedType( return new TypeScriptObject([]); } + if ($type->getName() === 'void') { + return new TypeScriptVoid(); + } + if (class_exists($type->getName()) || interface_exists($type->getName())) { return new TypeReference(new ClassStringReference($type->getName())); } diff --git a/src/Transformers/ClassTransformer.php b/src/Transformers/ClassTransformer.php index 283412b..03563ed 100644 --- a/src/Transformers/ClassTransformer.php +++ b/src/Transformers/ClassTransformer.php @@ -38,7 +38,7 @@ public function __construct( public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable { - if ($reflectionClass->isEnum()) { + if ($reflectionClass->isEnum() || $reflectionClass->isInterface()) { return Untransformable::create(); } diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index 7ee7f21..d64baaf 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -2,25 +2,26 @@ namespace Spatie\TypeScriptTransformer\Transformers; -use PHPStan\PhpDocParser\Ast\Type\TypeNode; use ReflectionClass; use ReflectionMethod; -use ReflectionProperty; +use ReflectionParameter; use Spatie\TypeScriptTransformer\Actions\TranspilePhpStanTypeToTypeScriptNodeAction; use Spatie\TypeScriptTransformer\Actions\TranspileReflectionTypeToTypeScriptNodeAction; -use Spatie\TypeScriptTransformer\Attributes\Hidden; -use Spatie\TypeScriptTransformer\Attributes\Optional; -use Spatie\TypeScriptTransformer\Attributes\TypeScriptTypeAttributeContract; use Spatie\TypeScriptTransformer\References\ReflectionClassReference; use Spatie\TypeScriptTransformer\Support\TransformationContext; use Spatie\TypeScriptTransformer\Transformed\Transformed; use Spatie\TypeScriptTransformer\Transformed\Untransformable; +use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedMethod; +use Spatie\TypeScriptTransformer\TypeResolvers\Data\ParsedNameAndType; use Spatie\TypeScriptTransformer\TypeResolvers\DocTypeResolver; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptAlias; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptIdentifier; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterface; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptInterfaceMethod; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptParameter; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptProperty; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; abstract class InterfaceTransformer implements Transformer { @@ -33,7 +34,7 @@ public function __construct( public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable { - if ($reflectionClass->isEnum()) { + if (! $reflectionClass->isInterface()) { return Untransformable::create(); } @@ -41,11 +42,14 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex return Untransformable::create(); } + $node = new TypeScriptInterface( + new TypeScriptIdentifier($context->name), + $this->getProperties($reflectionClass, $context), + $this->getMethods($reflectionClass, $context) + ); + return new Transformed( - new TypeScriptAlias( - new TypeScriptIdentifier($context->name), - $this->getTypeScriptNode($reflectionClass, $context) - ), + $node, new ReflectionClassReference($reflectionClass), $context->nameSpaceSegments, true, @@ -54,173 +58,96 @@ public function transform(ReflectionClass $reflectionClass, TransformationContex abstract protected function shouldTransform(ReflectionClass $reflection): bool; - protected function getTypeScriptNode( + /** @return TypeScriptInterfaceMethod[] */ + protected function getMethods( ReflectionClass $reflectionClass, TransformationContext $context, - ): TypeScriptNode { - if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass)) { - return $resolvedAttributeType; - } - - $constructorAnnotations = $reflectionClass->hasMethod('__construct') - ? $this->docTypeResolver->method($reflectionClass->getMethod('__construct'))?->parameters ?? [] - : []; - - $properties = []; - - foreach ($this->getMethods($reflectionClass) as $reflectionMethod) { - $property = $this->createProperty( - $reflectionClass, - $reflectionMethod, - $annotation?->type, - $context - ); - - if ($property === null) { - continue; - } - - $property = $this->runClassPropertyProcessors( - $reflectionMethod, - $annotation?->type, - $property - ); + ): array { + $methods = []; - if ($property !== null) { - $properties[] = $property; - } + foreach ($reflectionClass->getMethods() as $reflectionMethod) { + $methods[] = $this->getTypeScriptMethod($reflectionClass, $reflectionMethod, $context); } - return new Type($properties); + return $methods; } - protected function resolveTypeByAttribute( + /** @return TypeScriptProperty[] */ + protected function getProperties( ReflectionClass $reflectionClass, - ?ReflectionProperty $property = null, - ): ?TypeScriptNode { - $subject = $property ?? $reflectionClass; - - foreach ($subject->getAttributes() as $attribute) { - if (is_a($attribute->getName(), TypeScriptTypeAttributeContract::class, true)) { - /** @var TypeScriptTypeAttributeContract $attributeInstance */ - $attributeInstance = $attribute->newInstance(); - - return $attributeInstance->getType($reflectionClass); - } - } - - return null; - } - - protected function getMethods(ReflectionClass $reflection): array - { - return array_filter( - $reflection->getMethods(), - fn (ReflectionMethod $method) => ! $method->isStatic() - ); + TransformationContext $context, + ): array { + return []; } - protected function createProperty( + protected function getTypeScriptMethod( ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, - ?TypeNode $annotation, + ReflectionMethod $reflectionMethod, TransformationContext $context, - ): ?TypeScriptProperty { - $type = $this->resolveTypeForProperty( - $reflectionClass, - $reflectionProperty, - $annotation - ); + ): TypeScriptInterfaceMethod { + $annotation = $this->docTypeResolver->method($reflectionMethod); - $property = new TypeScriptProperty( - $reflectionProperty->getName(), - $type, - $this->isPropertyOptional( - $reflectionProperty, - $reflectionClass, - $type, - $context - ), - $this->isPropertyReadonly( - $reflectionProperty, + return new TypeScriptInterfaceMethod( + $reflectionMethod->getName(), + array_map(fn (ReflectionParameter $parameter) => $this->resolveMethodParameterType( $reflectionClass, - $type, - ) + $reflectionMethod, + $parameter, + $context, + $annotation->parameters[$parameter->getName()] ?? null + ), $reflectionMethod->getParameters()), + $this->resolveMethodReturnType($reflectionClass, $reflectionMethod, $context, $annotation) ); - - if ($this->isPropertyHidden($reflectionProperty, $reflectionClass, $property)) { - return null; - } - - return $property; } - protected function resolveTypeForProperty( + protected function resolveMethodReturnType( ReflectionClass $reflectionClass, - ReflectionProperty $reflectionProperty, - ?TypeNode $annotation, + ReflectionMethod $reflectionMethod, + TransformationContext $context, + ?ParsedMethod $annotation ): TypeScriptNode { - if ($resolvedAttributeType = $this->resolveTypeByAttribute($reflectionClass, $reflectionProperty)) { - return $resolvedAttributeType; - } - - if ($annotation) { + if ($annotation->returnType) { return $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( - $annotation, - $reflectionClass, + $annotation->returnType, + $reflectionClass ); } - if ($reflectionProperty->hasType()) { + $reflectionType = $reflectionMethod->getReturnType(); + + if ($reflectionType) { return $this->transpileReflectionTypeToTypeScriptTypeAction->execute( - $reflectionProperty->getType(), + $reflectionType, $reflectionClass ); } - return new TypeScriptUnknown(); + return new TypeScriptVoid(); } - protected function isPropertyOptional( - ReflectionProperty $reflectionProperty, + protected function resolveMethodParameterType( ReflectionClass $reflectionClass, - TypeScriptNode $type, + ReflectionMethod $reflectionMethod, + ReflectionParameter $reflectionParameter, TransformationContext $context, - ): bool { - return $context->optional || count($reflectionProperty->getAttributes(Optional::class)) > 0; - } - - protected function isPropertyReadonly( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, - TypeScriptNode $type, - ): bool { - return $reflectionProperty->isReadOnly() || $reflectionClass->isReadOnly(); - } - - protected function isPropertyHidden( - ReflectionProperty $reflectionProperty, - ReflectionClass $reflectionClass, - TypeScriptProperty $property, - ): bool { - return count($reflectionProperty->getAttributes(Hidden::class)) > 0; - } - - protected function runClassPropertyProcessors( - ReflectionProperty $reflectionProperty, - ?TypeNode $annotation, - TypeScriptProperty $property, - ): ?TypeScriptProperty { - $processors = $this->classPropertyProcessors; - - foreach ($processors as $processor) { - $property = $processor->execute($reflectionProperty, $annotation, $property); - - if ($property === null) { - return null; - } - } + ?ParsedNameAndType $annotation, + ): TypeScriptParameter { + $type = match (true) { + $annotation !== null => $this->transpilePhpStanTypeToTypeScriptTypeAction->execute( + $annotation->type, + $reflectionClass + ), + $reflectionParameter->hasType() => $this->transpileReflectionTypeToTypeScriptTypeAction->execute( + $reflectionParameter->getType(), + $reflectionClass + ), + default => new TypeScriptUnknown(), + }; - return $property; + return new TypeScriptParameter( + $reflectionParameter->getName(), + $type, + $reflectionParameter->isOptional() + ); } } diff --git a/src/TypeResolvers/Data/ParsedMethod.php b/src/TypeResolvers/Data/ParsedMethod.php index 122bda2..f321f1e 100644 --- a/src/TypeResolvers/Data/ParsedMethod.php +++ b/src/TypeResolvers/Data/ParsedMethod.php @@ -7,7 +7,7 @@ class ParsedMethod { /** - * @param array $parameters + * @param array $parameters */ public function __construct( public array $parameters, diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index d52c495..f8836c4 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -40,13 +40,13 @@ public function execute(bool $watch = false): void { /** * TODO: - * - Add interface implementation + tests + * - Add interface implementation + tests -> OK * - Split off Laravel specific code and test * - Split off data specific code and test * - Add support for watching files - * - Further write docs + check them + * - Further write docs + check them -> only Laravel specific stuff * - Check old Laravel tests if we missed something - * - Check in Flare whether everything is working as expected + * - Check in Flare whether everything is working as expected -> PR ready, needs fixing TS * - Release */ diff --git a/tests/Actions/DiscoverTypesActionTest.php b/tests/Actions/DiscoverTypesActionTest.php index c4a50d8..582627c 100644 --- a/tests/Actions/DiscoverTypesActionTest.php +++ b/tests/Actions/DiscoverTypesActionTest.php @@ -6,6 +6,7 @@ use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\OptionalAttributedClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\ReadonlyClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleClass; +use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\SimpleInterface; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\StringBackedEnum; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\TypeScriptAttributedClass; use Spatie\TypeScriptTransformer\Tests\Fakes\TypesToProvide\TypeScriptLocationAttributedClass; @@ -20,6 +21,7 @@ StringBackedEnum::class, HiddenAttributedClass::class, TypeScriptAttributedClass::class, + SimpleInterface::class, TypeScriptLocationAttributedClass::class, OptionalAttributedClass::class, ReadonlyClass::class, diff --git a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php index 922601d..d90d500 100644 --- a/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php +++ b/tests/Actions/TranspileReflectionTypeToTypeScriptNodeActionTest.php @@ -17,6 +17,7 @@ use Spatie\TypeScriptTransformer\TypeScript\TypeScriptString; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnion; use Spatie\TypeScriptTransformer\TypeScript\TypeScriptUnknown; +use Spatie\TypeScriptTransformer\TypeScript\TypeScriptVoid; it('can transpile php types', function ( string $property, @@ -138,3 +139,14 @@ new TypeReference(new ClassStringReference(Collection::class)), ]; }); + +it('can transpile a void return type', function () { + $transpiler = new TranspileReflectionTypeToTypeScriptNodeAction(); + + $typeScriptNode = $transpiler->execute( + (new ReflectionMethod(PhpTypesStub::class, 'voidReturn'))->getReturnType(), + new ReflectionClass(PhpTypesStub::class) + ); + + expect($typeScriptNode)->toBeInstanceOf(TypeScriptVoid::class); +}); diff --git a/tests/Fakes/PropertyTypes/PhpTypesStub.php b/tests/Fakes/PropertyTypes/PhpTypesStub.php index 5c4294a..b15d115 100644 --- a/tests/Fakes/PropertyTypes/PhpTypesStub.php +++ b/tests/Fakes/PropertyTypes/PhpTypesStub.php @@ -43,4 +43,9 @@ class PhpTypesStub extends stdClass public array $array; public Collection $reference; + + public function voidReturn(): void + { + + } } diff --git a/tests/Fakes/TypesToProvide/SimpleInterface.php b/tests/Fakes/TypesToProvide/SimpleInterface.php new file mode 100644 index 0000000..0c0aba7 --- /dev/null +++ b/tests/Fakes/TypesToProvide/SimpleInterface.php @@ -0,0 +1,25 @@ + + */ + public function withAnnotatedReturnType(): array; + + public function withParameters(string $param1, int $param2): void; + + public function withOptionalParameters(string $param1, int $param2 = 5): void; + + /** + * @param array $param1 + * @param array $param2 + */ + public function withAnnotatedParameters(array $param1, array $param2): void; +} diff --git a/tests/Support/AllInterfaceTransformer.php b/tests/Support/AllInterfaceTransformer.php new file mode 100644 index 0000000..99fccf9 --- /dev/null +++ b/tests/Support/AllInterfaceTransformer.php @@ -0,0 +1,14 @@ +