Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvanassche committed Jun 21, 2024
1 parent 0ee5696 commit 1c57886
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 155 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This package allows you to convert PHP classes to TypeScript.
This class...

```php
/** @typescript */
#[TypeScript]
class User
{
public int $id;
Expand All @@ -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';
}
```

Expand Down
55 changes: 55 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions src/Actions/TranspileReflectionTypeToTypeScriptNodeAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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()));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Transformers/ClassTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
219 changes: 73 additions & 146 deletions src/Transformers/InterfaceTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -33,19 +34,22 @@ public function __construct(

public function transform(ReflectionClass $reflectionClass, TransformationContext $context): Transformed|Untransformable
{
if ($reflectionClass->isEnum()) {
if (! $reflectionClass->isInterface()) {
return Untransformable::create();
}

if (! $this->shouldTransform($reflectionClass)) {
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,
Expand All @@ -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()
);
}
}
Loading

0 comments on commit 1c57886

Please sign in to comment.