Skip to content

Commit dd7eb46

Browse files
authored
Extract TypeResolver (Close #61); Support optional types for TS (Close #95) (#105)
* Extract TypeResolver (Close #61) * OptionalType for TS
1 parent c0cfd41 commit dd7eb46

19 files changed

+570
-287
lines changed

bin/default-config.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Riverwaysoft\PhpConverter\Config\PhpConverterConfig;
88
use Riverwaysoft\PhpConverter\Filter\PhpAttributeFilter;
99
use Riverwaysoft\PhpConverter\OutputGenerator\TypeScript\TypeScriptOutputGenerator;
10+
use Riverwaysoft\PhpConverter\OutputGenerator\TypeScript\TypeScriptTypeResolver;
1011
use Riverwaysoft\PhpConverter\OutputGenerator\UnknownTypeResolver\ClassNameTypeResolver;
1112
use Riverwaysoft\PhpConverter\OutputGenerator\UnknownTypeResolver\DateTimeTypeResolver;
1213
use Riverwaysoft\PhpConverter\OutputWriter\SingleFileOutputWriter\SingleFileOutputWriter;
@@ -18,9 +19,11 @@
1819

1920
$config->setOutputGenerator(new TypeScriptOutputGenerator(
2021
new SingleFileOutputWriter('generated.ts'),
21-
[
22-
new DateTimeTypeResolver(),
23-
new ClassNameTypeResolver(),
24-
],
22+
new TypeScriptTypeResolver(
23+
[
24+
new DateTimeTypeResolver(),
25+
new ClassNameTypeResolver(),
26+
]
27+
)
2528
));
2629
};

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
],
1010
"scripts": {
1111
"test": "vendor/bin/phpunit --no-coverage",
12+
"test:update-snapshots": "vendor/bin/phpunit -d --update-snapshots --no-coverage",
1213
"test:with-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit",
1314
"test:coverage-level": "vendor/bin/coverage-check ./coverage/clover.xml 85",
1415
"build-phar": "bin/build.sh",
1516
"build-phar:test": "php build/php-converter.phar --from=./tests/Fixtures/ --to=.",
16-
"test:update-snapshots": "vendor/bin/phpunit -d --update-snapshots --no-coverage",
1717
"cs": "vendor/bin/ecs check --fix",
1818
"test:cs": " vendor/bin/ecs check",
1919
"phpstan": "vendor/bin/phpstan analyse src tests bin",

src/Ast/DtoVisitor.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Riverwaysoft\PhpConverter\Dto\DtoEnumProperty;
1414
use Riverwaysoft\PhpConverter\Dto\DtoType;
1515
use Riverwaysoft\PhpConverter\Dto\ExpressionType;
16+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpOptionalType;
1617
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeFactory;
1718
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeInterface;
1819
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnionType;
@@ -79,20 +80,23 @@ private function createDtoType(Class_|Enum_ $node): void
7980
/** @var PhpTypeInterface|null $docBlockType */
8081
$docBlockType = null;
8182
$nativeType = $stmt->type;
83+
$propertyName = $stmt->props[0]->name->name;
8284

8385
if ($comment) {
8486
$docBlockType = $this->phpDocTypeParser->parseVarOrReturn($comment);
8587
}
8688

8789
if (!$nativeType && !$docBlockType) {
88-
throw new Exception(sprintf("Property %s#%s has no type. Please add PHP type", $node->name->name, $stmt->props[0]->name->name));
90+
throw new Exception(sprintf("Property %s#%s has no type. Please add PHP type", $node->name->name, $propertyName));
8991
}
9092

91-
$type = $docBlockType ?? $this->createSingleType($nativeType);
93+
$hasDefaultValue = !!$stmt->props[0]->default;
94+
95+
$type = $docBlockType ?? $this->createSingleType($nativeType, '', $hasDefaultValue);
9296

9397
$properties[] = new DtoClassProperty(
9498
type: $type,
95-
name: $stmt->props[0]->name->name,
99+
name: $propertyName,
96100
);
97101
}
98102

@@ -107,6 +111,7 @@ private function createDtoType(Class_|Enum_ $node): void
107111

108112
$paramType = $param->type;
109113
$paramName = $param->var->name;
114+
$hasDefaultValue = !!$param->default;
110115

111116
if ($classMethodComments) {
112117
if ($classMethodCommentsParsed === null) {
@@ -125,7 +130,7 @@ private function createDtoType(Class_|Enum_ $node): void
125130
throw new Exception(sprintf("Property %s#%s has no type. Please add PHP type", $node->name->name, $paramName));
126131
}
127132

128-
$type = $this->createSingleType($paramType, $param->getDocComment()?->getText());
133+
$type = $this->createSingleType($paramType, $param->getDocComment()?->getText(), $hasDefaultValue);
129134

130135
$properties[] = new DtoClassProperty(
131136
type: $type,
@@ -170,7 +175,12 @@ private function createDtoType(Class_|Enum_ $node): void
170175
private function createSingleType(
171176
Node\Name|Node\Identifier|Node\NullableType|Node\UnionType $param,
172177
?string $docComment = null,
178+
bool $hasDefaultValue = false,
173179
): PhpTypeInterface {
180+
if ($hasDefaultValue) {
181+
return new PhpOptionalType($this->createSingleType($param, $docComment));
182+
}
183+
174184
if ($docComment) {
175185
$docBlockType = $this->phpDocTypeParser->parseVarOrReturn($docComment);
176186
if ($docBlockType) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Riverwaysoft\PhpConverter\OutputGenerator;
6+
7+
use Riverwaysoft\PhpConverter\Dto\ApiClient\ApiEndpoint;
8+
use Riverwaysoft\PhpConverter\Dto\DtoList;
9+
10+
interface ApiEndpointGeneratorInterface
11+
{
12+
public function generate(ApiEndpoint $apiEndpoint, DtoList $dtoList): string;
13+
}

src/OutputGenerator/Dart/DartClassFactoryGenerator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Riverwaysoft\PhpConverter\Dto\DtoType;
1010
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpBaseType;
1111
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpListType;
12+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpOptionalType;
1213
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeInterface;
1314
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnionType;
1415
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnknownType;
@@ -61,6 +62,10 @@ private function resolveFactoryProperty(
6162
DtoList $dtoList,
6263
string $mapArgumentName = "json['%s']",
6364
): string {
65+
if ($type instanceof PhpOptionalType) {
66+
return $this->resolveFactoryProperty($propertyName, $type->getType(), $dto, $dtoList, $mapArgumentName);
67+
}
68+
6469
if ($type instanceof PhpUnionType && $type->isNullable()) {
6570
return sprintf(
6671
"json['{$propertyName}'] != null ? %s : null",

src/OutputGenerator/Dart/DartOutputGenerator.php

Lines changed: 11 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,23 @@
99
use Riverwaysoft\PhpConverter\Dto\DtoList;
1010
use Riverwaysoft\PhpConverter\Dto\DtoType;
1111
use Riverwaysoft\PhpConverter\Dto\ExpressionType;
12-
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpBaseType;
13-
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpListType;
14-
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeInterface;
12+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpOptionalType;
1513
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnionType;
1614
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnknownType;
1715
use Riverwaysoft\PhpConverter\OutputGenerator\OutputGeneratorInterface;
18-
use Riverwaysoft\PhpConverter\OutputGenerator\UnknownTypeResolver\UnknownTypeResolverInterface;
19-
use Riverwaysoft\PhpConverter\OutputGenerator\UnsupportedTypeException;
16+
use Riverwaysoft\PhpConverter\OutputWriter\OutputFile;
2017
use Riverwaysoft\PhpConverter\OutputWriter\OutputProcessor\OutputFilesProcessor;
2118
use Riverwaysoft\PhpConverter\OutputWriter\OutputWriterInterface;
22-
use Webmozart\Assert\Assert;
2319
use Exception;
2420
use function sprintf;
2521

2622
class DartOutputGenerator implements OutputGeneratorInterface
2723
{
2824
private DartEnumValidator $dartEnumValidator;
2925

30-
/** @param UnknownTypeResolverInterface[] $unknownTypeResolvers */
3126
public function __construct(
3227
private OutputWriterInterface $outputWriter,
33-
private array $unknownTypeResolvers = [],
28+
private DartTypeResolver $typeResolver,
3429
private ?OutputFilesProcessor $outputFilesProcessor = null,
3530
private ?DartClassFactoryGenerator $classFactoryGenerator = null,
3631
private ?DartEquitableGenerator $equitableGenerator = null,
@@ -39,6 +34,7 @@ public function __construct(
3934
$this->dartEnumValidator = new DartEnumValidator();
4035
}
4136

37+
/** @return OutputFile[] */
4238
public function generate(ConverterResult $converterResult): array
4339
{
4440
$this->outputWriter->reset();
@@ -88,7 +84,7 @@ private function convertToDartProperties(DtoType $dto, DtoList $dtoList): string
8884
$properties = $dto->getProperties();
8985

9086
foreach ($properties as $property) {
91-
$string .= sprintf("\n final %s %s;", $this->getDartTypeFromPhp($property->getType(), $dto, $dtoList), $property->getName());
87+
$string .= sprintf("\n final %s %s;", $this->typeResolver->getDartTypeFromPhp($property->getType(), $dto, $dtoList), $property->getName());
9288
}
9389

9490
return $string;
@@ -99,83 +95,21 @@ private function generateConstructor(DtoType $dtoType): string
9995
$string = '';
10096

10197
foreach ($dtoType->getProperties() as $property) {
98+
$type = $property->getType();
99+
if ($type instanceof PhpOptionalType) {
100+
$type = $type->getType();
101+
}
102+
102103
$string .= sprintf(
103104
"\n %sthis.%s,",
104-
$property->getType() instanceof PhpUnionType && $property->getType()->isNullable() ? '' : 'required ',
105+
$type instanceof PhpUnionType && $type->isNullable() ? '' : 'required ',
105106
$property->getName()
106107
);
107108
}
108109

109110
return sprintf("%s({%s\n });", $dtoType->getName(), $string);
110111
}
111112

112-
private function getDartTypeFromPhp(PhpTypeInterface $type, DtoType $dto, DtoList $dtoList): string
113-
{
114-
if ($type instanceof PhpUnionType) {
115-
Assert::greaterThan($type->getTypes(), 2, "Dart does not support union types");
116-
if (!$type->isNullable()) {
117-
return $this->getDartTypeFromPhp(PhpBaseType::mixed(), $dto, $dtoList);
118-
}
119-
$notNullType = $type->getFirstNotNullType();
120-
return sprintf('%s?', $this->getDartTypeFromPhp($notNullType, $dto, $dtoList));
121-
}
122-
123-
if ($type instanceof PhpListType) {
124-
return sprintf('List<%s>', $this->getDartTypeFromPhp($type->getType(), $dto, $dtoList));
125-
}
126-
127-
if ($type instanceof PhpBaseType) {
128-
/** @var PhpBaseType $type */
129-
return match (true) {
130-
$type->equalsTo(PhpBaseType::int()) => 'int',
131-
$type->equalsTo(PhpBaseType::float()) => 'num',
132-
$type->equalsTo(PhpBaseType::string()) => 'String',
133-
$type->equalsTo(PhpBaseType::bool()) => 'bool',
134-
$type->equalsTo(PhpBaseType::mixed()), $type->equalsTo(PhpBaseType::iterable()), $type->equalsTo(PhpBaseType::array()) => 'dynamic',
135-
$type->equalsTo(PhpBaseType::null()) => 'null',
136-
$type->equalsTo(PhpBaseType::self()) => $dto->getName(),
137-
default => throw new Exception(sprintf("Unknown base PHP type: %s", $type->jsonSerialize()))
138-
};
139-
}
140-
141-
/** @var PhpUnknownType $type */
142-
$result = $this->handleUnknownType($type, $dto, $dtoList);
143-
144-
if ($result instanceof PhpTypeInterface) {
145-
return $this->getDartTypeFromPhp($result, $dto, $dtoList);
146-
}
147-
148-
return $result;
149-
}
150-
151-
private function handleUnknownType(PhpUnknownType $type, DtoType|null $dto, DtoList $dtoList): string|PhpTypeInterface
152-
{
153-
if ($type instanceof PhpUnknownType && $dto?->isGeneric() && $dto->hasGeneric($type)) {
154-
return $type->getName();
155-
}
156-
157-
if ($type instanceof PhpUnknownType && $type->hasGenerics() && $dtoList->hasDtoWithType($type->getName())) {
158-
$result = $type->getName();
159-
160-
$generics = array_map(fn (PhpTypeInterface $innerGeneric) => $this->getDartTypeFromPhp(
161-
$innerGeneric,
162-
$dto,
163-
$dtoList,
164-
), $type->getGenerics());
165-
166-
$result .= sprintf("<%s>", join(', ', $generics));
167-
return $result;
168-
}
169-
170-
foreach ($this->unknownTypeResolvers as $unknownTypeResolver) {
171-
if ($unknownTypeResolver->supports($type, $dto, $dtoList)) {
172-
return $unknownTypeResolver->resolve($type, $dto, $dtoList);
173-
}
174-
}
175-
176-
throw UnsupportedTypeException::forType($type, $dto?->getName() ?? '');
177-
}
178-
179113
/** @param DtoEnumProperty[] $properties */
180114
private function convertEnumToTypeScriptProperties(array $properties): string
181115
{
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Riverwaysoft\PhpConverter\OutputGenerator\Dart;
6+
7+
use Riverwaysoft\PhpConverter\Dto\DtoList;
8+
use Riverwaysoft\PhpConverter\Dto\DtoType;
9+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpBaseType;
10+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpListType;
11+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpOptionalType;
12+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeInterface;
13+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnionType;
14+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnknownType;
15+
use Riverwaysoft\PhpConverter\OutputGenerator\UnknownTypeResolver\UnknownTypeResolverInterface;
16+
use Riverwaysoft\PhpConverter\OutputGenerator\UnsupportedTypeException;
17+
use Webmozart\Assert\Assert;
18+
use Exception;
19+
use function sprintf;
20+
21+
class DartTypeResolver
22+
{
23+
/** @param UnknownTypeResolverInterface[] $unknownTypeResolvers */
24+
public function __construct(
25+
private array $unknownTypeResolvers = [],
26+
) {
27+
}
28+
29+
public function getDartTypeFromPhp(PhpTypeInterface $type, DtoType $dto, DtoList $dtoList): string
30+
{
31+
if ($type instanceof PhpUnionType) {
32+
Assert::greaterThan($type->getTypes(), 2, "Dart does not support union types");
33+
if (!$type->isNullable()) {
34+
return $this->getDartTypeFromPhp(PhpBaseType::mixed(), $dto, $dtoList);
35+
}
36+
$notNullType = $type->getFirstNotNullType();
37+
return sprintf('%s?', $this->getDartTypeFromPhp($notNullType, $dto, $dtoList));
38+
}
39+
40+
if ($type instanceof PhpListType) {
41+
return sprintf('List<%s>', $this->getDartTypeFromPhp($type->getType(), $dto, $dtoList));
42+
}
43+
44+
if ($type instanceof PhpOptionalType) {
45+
return $this->getDartTypeFromPhp($type->getType(), $dto, $dtoList);
46+
}
47+
48+
if ($type instanceof PhpBaseType) {
49+
/** @var PhpBaseType $type */
50+
return match (true) {
51+
$type->equalsTo(PhpBaseType::int()) => 'int',
52+
$type->equalsTo(PhpBaseType::float()) => 'num',
53+
$type->equalsTo(PhpBaseType::string()) => 'String',
54+
$type->equalsTo(PhpBaseType::bool()) => 'bool',
55+
$type->equalsTo(PhpBaseType::mixed()), $type->equalsTo(PhpBaseType::iterable()), $type->equalsTo(PhpBaseType::array()) => 'dynamic',
56+
$type->equalsTo(PhpBaseType::null()) => 'null',
57+
$type->equalsTo(PhpBaseType::self()) => $dto->getName(),
58+
default => throw new Exception(sprintf("Unknown base PHP type: %s", $type->jsonSerialize()))
59+
};
60+
}
61+
62+
/** @var PhpUnknownType $type */
63+
$result = $this->handleUnknownType($type, $dto, $dtoList);
64+
65+
if ($result instanceof PhpTypeInterface) {
66+
return $this->getDartTypeFromPhp($result, $dto, $dtoList);
67+
}
68+
69+
return $result;
70+
}
71+
72+
private function handleUnknownType(PhpUnknownType $type, DtoType|null $dto, DtoList $dtoList): string|PhpTypeInterface
73+
{
74+
if ($type instanceof PhpUnknownType && $dto?->isGeneric() && $dto->hasGeneric($type)) {
75+
return $type->getName();
76+
}
77+
78+
if ($type instanceof PhpUnknownType && $type->hasGenerics() && $dtoList->hasDtoWithType($type->getName())) {
79+
$result = $type->getName();
80+
81+
$generics = array_map(fn (PhpTypeInterface $innerGeneric) => $this->getDartTypeFromPhp(
82+
$innerGeneric,
83+
$dto,
84+
$dtoList,
85+
), $type->getGenerics());
86+
87+
$result .= sprintf("<%s>", join(', ', $generics));
88+
return $result;
89+
}
90+
91+
foreach ($this->unknownTypeResolvers as $unknownTypeResolver) {
92+
if ($unknownTypeResolver->supports($type, $dto, $dtoList)) {
93+
return $unknownTypeResolver->resolve($type, $dto, $dtoList);
94+
}
95+
}
96+
97+
throw UnsupportedTypeException::forType($type, $dto?->getName() ?? '');
98+
}
99+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Riverwaysoft\PhpConverter\OutputGenerator;
6+
7+
use Riverwaysoft\PhpConverter\Dto\DtoClassProperty;
8+
use Riverwaysoft\PhpConverter\Dto\DtoType;
9+
10+
interface PropertyNameGeneratorInterface
11+
{
12+
public function supports(DtoType $dto): bool;
13+
14+
public function generate(DtoClassProperty $property): string;
15+
}

0 commit comments

Comments
 (0)