Skip to content

Commit

Permalink
Use phpstan aliases instead of NamedType (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
allestuetsmerweh authored Dec 30, 2024
1 parent deaa5ff commit a29e865
Show file tree
Hide file tree
Showing 16 changed files with 175 additions and 255 deletions.
12 changes: 11 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,20 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache other caches
uses: actions/cache@v3
with:
path: |
*.cache
key: ${{ runner.os }}-other-${{ hashFiles('*.cache') }}
restore-keys: |
${{ runner.os }}-other-
- name: Install JavaScript dependencies
run: npm install
- name: Code style check
- name: PHP Code style check
run: composer fixdiff
- name: PHP Static Analysis
run: composer check
- name: Lint JavaScript & TypeScript
run: npm run lint
- name: Compile TypeScript
Expand Down
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/.github/
/.php-cs-fixer.cache
/.phpunit.cache/
/client/src/
/client/tests/
/coverage/
Expand All @@ -16,5 +18,7 @@
/composer.json
/composer.lock
/jest.config.js
/phpstan.neon
/phpunit.xml
/phpunit.xml.bak
/tsconfig.json
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "allestuetsmerweh/php-typescript-api",
"version": "2.6.0",
"version": "2.6.1",
"type": "library",
"description": "Build a typed Web API using PHP and TypeScript",
"keywords": ["PHP","TypeScript","API"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
<?php

use PhpTypeScriptApi\PhpStan\NamedType;
use PhpTypeScriptApi\TypedEndpoint;

/** @extends NamedType<array{type: string, x: string, y: string}> */
class SptCoordinate extends NamedType {
}

/** @extends NamedType<array{id: string, name: string, coordinate: SptCoordinate}> */
class SptLocation extends NamedType {
}

/** @extends NamedType<array{stationId: string, arrival: ?string, departure: ?string, delay: ?int, platform: ?string}> */
class SptStop extends NamedType {
}

/** @extends NamedType<array{departure: SptStop, arrival: SptStop, passList: array<SptStop>}> */
class SptSection extends NamedType {
}

/** @extends NamedType<array{sections: array<SptSection>}> */
class SptConnection extends NamedType {
}

/**
* Search for a swiss public transport connection.
*
* for further information on the backend used, see
* https://transport.opendata.ch/docs.html#connections
*
* @phpstan-type SptCoordinate array{type: string, x: string, y: string}
* @phpstan-type SptLocation array{id: string, name: string, coordinate: SptCoordinate}
* @phpstan-type SptStop array{stationId: string, arrival: ?string, departure: ?string, delay: ?int, platform: ?string}
* @phpstan-type SptSection array{departure: SptStop, arrival: SptStop, passList: array<SptStop>}
* @phpstan-type SptConnection array{sections: array<SptSection>}
*
* @extends TypedEndpoint<
* array{'from': string, 'to': string, 'via': ?array<string>, 'date': string, 'time': string, 'isArrivalTime': ?bool},
* array{stationById: array<string, SptLocation>, connections: array<SptConnection>},
Expand Down Expand Up @@ -84,22 +69,30 @@ protected function getStationByIds(array $backend_result): array {
return $station_by_id;
}

/** @param array<string, mixed> $backend_station */
protected function convertStation(array $backend_station): SptLocation {
return new SptLocation([
/**
* @param array<string, mixed> $backend_station
*
* @return SptLocation
*/
protected function convertStation(array $backend_station): array {
return [
'id' => $backend_station['id'],
'name' => $backend_station['name'],
'coordinate' => $this->convertCoordinate($backend_station['coordinate']),
]);
];
}

/** @param array<string, mixed> $args */
protected function convertCoordinate(array $args): SptCoordinate {
return new SptCoordinate([
/**
* @param array<string, mixed> $args
*
* @return SptCoordinate
*/
protected function convertCoordinate(array $args): array {
return [
'type' => $args['type'],
'x' => $args['x'],
'y' => $args['y'],
]);
];
}

/**
Expand All @@ -112,30 +105,34 @@ protected function getConnections(array $backend_result): array {
foreach ($backend_result['connections'] as $connection) {
$new_sections = [];
foreach ($connection['sections'] as $section) {
$new_section = new SptSection([
$new_section = [
'departure' => $this->convertStop($section['departure']),
'arrival' => $this->convertStop($section['arrival']),
'passList' => array_map(
fn ($stop) => $this->convertStop($stop),
$section['journey']['passList'],
),
]);
];
$new_sections[] = $new_section;
}
$new_connections[] = new SptConnection(['sections' => $new_sections]);
$new_connections[] = ['sections' => $new_sections];
}
return $new_connections;
}

/** @param array<string, mixed> $backend_stop */
protected function convertStop(array $backend_stop): SptStop {
return new SptStop([
/**
* @param array<string, mixed> $backend_stop
*
* @return SptStop
*/
protected function convertStop(array $backend_stop): array {
return [
'stationId' => $backend_stop['station']['id'],
'arrival' => $this->getTime($backend_stop['arrival'] ?? null),
'departure' => $this->getTime($backend_stop['departure'] ?? null),
'delay' => $backend_stop['delay'],
'platform' => $backend_stop['platform'],
]);
];
}

protected function getTime(?string $backend_value): ?string {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "php-typescript-api",
"version": "2.6.0",
"version": "2.6.1",
"description": "Build a typed Web API using PHP and TypeScript",
"main": "client/lib/index.js",
"types": "client/lib/index.d.ts",
Expand Down
13 changes: 0 additions & 13 deletions server/lib/PhpStan/NamedType.php

This file was deleted.

34 changes: 11 additions & 23 deletions server/lib/PhpStan/PhpStanUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,19 @@

class PhpStanUtils {
/** @param \ReflectionClass<object> $class_info */
public function getNamedTypeNode(\ReflectionClass $class_info): TypeNode {
if (!$this->extendsNamedType($class_info)) {
throw new \Exception('Only classes directly extending NamedType may be used.');
public function getAliasTypeNode(string $name, \ReflectionClass $class_info): TypeNode {
$php_doc_node = $this->parseDocComment($class_info->getDocComment());
// TODO: Use array_find (PHP 8.4)
$alias_node = null;
foreach ($php_doc_node->getTypeAliasTagValues() as $node) {
if ($node->alias === $name) {
$alias_node = $node;
}
}
$phpDocNode = $this->parseDocComment($class_info->getDocComment());
$php_type_node = $phpDocNode->getExtendsTagValues()[0]->type;
if (!preg_match('/(^|\\\)NamedType$/', "{$php_type_node->type}")) {
// @codeCoverageIgnoreStart
// Reason: phpstan does not allow testing this!
throw new \Exception('Only classes directly extending NamedType (in doc comment) may be used.');
// @codeCoverageIgnoreEnd
if ($alias_node === null) {
throw new \Exception("Type alias not found: {$name}");
}
if (count($php_type_node->genericTypes) !== 1) {
// @codeCoverageIgnoreStart
// Reason: phpstan does not allow testing this!
throw new \Exception('NamedType has exactly one generic parameter.');
// @codeCoverageIgnoreEnd
}
return $php_type_node->genericTypes[0];
}

/** @param \ReflectionClass<object> $class_info */
public function extendsNamedType(\ReflectionClass $class_info): bool {
return $class_info->getParentClass()
&& $class_info->getParentClass()->getName() === NamedType::class;
return $alias_node->type;
}

public function parseDocComment(string|false|null $doc_comment): PhpDocNode {
Expand Down
17 changes: 9 additions & 8 deletions server/lib/PhpStan/TypeScriptVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;

final class TypeScriptVisitor extends AbstractNodeVisitor {
private PhpStanUtils $phpStanUtils;

/** @var array<string, \ReflectionClass<object>> */
/** @var array<string, TypeNode> */
public array $exported_classes = [];

/** @param \ReflectionClass<object> $endpointClass */
public function __construct(
protected string $namespaceName,
protected \ReflectionClass $endpointClass,
) {
$this->phpStanUtils = new PhpStanUtils();
}
Expand Down Expand Up @@ -91,12 +93,11 @@ public function leaveNode(Node $originalNode): Node {
];
$new_name = $mapping[$node->name] ?? null;
if ($new_name === null && preg_match('/^[A-Z]/', $node->name)) {
// @phpstan-ignore argument.type, phpstanApi.runtimeReflection
$class_info = new \ReflectionClass("{$this->namespaceName}\\{$node->name}");
if (!$this->phpStanUtils->extendsNamedType($class_info)) {
throw new \Exception('Only classes extending NamedType may be used.');
}
$this->exported_classes[$node->name] = $class_info;
$resolved_node = $this->phpStanUtils->getAliasTypeNode(
$node->name,
$this->endpointClass
);
$this->exported_classes[$node->name] = $resolved_node;
$new_name = $node->name;
}
if ($new_name === null) {
Expand Down
17 changes: 7 additions & 10 deletions server/lib/PhpStan/ValidateVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@
final class ValidateVisitor extends AbstractNodeVisitor {
private PhpStanUtils $phpStanUtils;

/** @param \ReflectionClass<object> $endpointClass */
public function __construct(
protected string $namespaceName,
protected \ReflectionClass $endpointClass,
protected mixed $value,
) {
$this->phpStanUtils = new PhpStanUtils();
}

public static function validate(string $namespace, mixed $value, Node $type): ValidationResultNode {
$validator = new ValidateVisitor($namespace, $value);
/** @param \ReflectionClass<object> $endpointClass */
public static function validate(\ReflectionClass $endpointClass, mixed $value, Node $type): ValidationResultNode {
$validator = new ValidateVisitor($endpointClass, $value);
return $validator->subValidate($value, $type);
}

Expand Down Expand Up @@ -76,12 +78,7 @@ public function enterNode(Node $node): ValidationResultNode {
$fn = $mapping[$node->name] ?? null;
if ($fn === null) {
if (preg_match('/^[A-Z]/', $node->name)) {
// @phpstan-ignore argument.type, phpstanApi.runtimeReflection
$class_info = new \ReflectionClass("{$this->namespaceName}\\{$node->name}");
if (!$this->phpStanUtils->extendsNamedType($class_info)) {
throw new \Exception('Only classes extending NamedType may be used.');
}
$named_class_type = $this->phpStanUtils->getNamedTypeNode($class_info);
$named_class_type = $this->phpStanUtils->getAliasTypeNode($node->name, $this->endpointClass);
return $this->subValidate($this->value, $named_class_type);
}
throw new \Exception("Unknown IdentifierTypeNode name: {$node->name}");
Expand Down Expand Up @@ -238,7 +235,7 @@ public function enterNode(Node $node): ValidationResultNode {
}

public function subValidate(mixed $value, Node $type): ValidationResultNode {
$visitor = new ValidateVisitor($this->namespaceName, $value);
$visitor = new ValidateVisitor($this->endpointClass, $value);
$traverser = new NodeTraverser([$visitor]);
[$result_node] = $traverser->traverse([$type]);
if (!($result_node instanceof ValidationResultNode)) {
Expand Down
Loading

0 comments on commit a29e865

Please sign in to comment.