Skip to content

Commit

Permalink
API Objects (e.g. IsoDate, IsoDateTime, IsoTime) for feature parity
Browse files Browse the repository at this point in the history
  • Loading branch information
allestuetsmerweh committed Jan 5, 2025
1 parent 9495623 commit 081beed
Show file tree
Hide file tree
Showing 32 changed files with 1,010 additions and 185 deletions.
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.5",
"version": "2.6.6",
"type": "library",
"description": "Build a typed Web API using PHP and TypeScript",
"keywords": ["PHP","TypeScript","API"],
Expand Down
34 changes: 34 additions & 0 deletions example/api/Endpoints/CombineDateTimeTypedEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use PhpTypeScriptApi\PhpStan\IsoDate;
use PhpTypeScriptApi\PhpStan\IsoTime;
use PhpTypeScriptApi\PhpStan\IsoDateTime;
use PhpTypeScriptApi\TypedEndpoint;

/**
* @extends TypedEndpoint<
* array{date: IsoDate, time: \PhpTypeScriptApi\PhpStan\IsoTime},
* array{dateTime: IsoDateTime},
* >
*/
class CombineDateTimeTypedEndpoint extends TypedEndpoint {
public static function getApiObjectClasses(): array {
return [IsoDate::class, IsoTime::class, IsoDateTime::class];
}

public function runtimeSetup(): void {
// no runtime setup required
}

public static function getIdent(): string {
return 'CombineDateTimeTypedEndpoint';
}

protected function handle(mixed $input): mixed {
$date = $input['date']->format('Y-m-d');
$time = $input['time']->format('H:i:s');
return [
'dateTime' => new IsoDateTime("{$date} {$time}"),
];
}
}
4 changes: 4 additions & 0 deletions example/api/Endpoints/DivideNumbersTypedEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* >
*/
class DivideNumbersTypedEndpoint extends TypedEndpoint {
public static function getApiObjectClasses(): array {
return [];
}

public function runtimeSetup(): void {
// no runtime setup required.
}
Expand Down
4 changes: 4 additions & 0 deletions example/api/Endpoints/EmptyTypedEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* >
*/
class EmptyTypedEndpoint extends TypedEndpoint {
public static function getApiObjectClasses(): array {
return [];
}

public function runtimeSetup(): void {
// no runtime setup required.
}
Expand Down
4 changes: 4 additions & 0 deletions example/api/Endpoints/SquareRootTypedEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* >
*/
class SquareRootTypedEndpoint extends TypedEndpoint {
public static function getApiObjectClasses(): array {
return [];
}

public function runtimeSetup(): void {
// no runtime setup required.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
* @phpstan-type SptConnection array{sections: array<SptSection>}
*
* @extends TypedEndpoint<
* array{'from': string, 'to': string, 'via': ?array<string>, 'date': string, 'time': string, 'isArrivalTime': ?bool},
* array{'from': string, 'to': string, 'via': ?array<string>, 'date': \PhpTypeScriptApi\PhpStan\IsoDate, 'time': string, 'isArrivalTime': ?bool},
* array{stationById: array<string, SptLocation>, connections: array<SptConnection>},
* >
*/
class SwissPublicTransportConnectionsTypedEndpoint extends TypedEndpoint {
public static function getApiObjectClasses(): array {
return [];
}

public function runtimeSetup(): void {
// no runtime setup required.
}
Expand All @@ -34,7 +38,7 @@ protected function handle(mixed $input): mixed {
'from' => $input['from'],
'to' => $input['to'],
'via' => $input['via'],
'date' => $input['date'],
'date' => $input['date']->data(),
'time' => $input['time'],
'isArrivalTime' => $input['isArrivalTime'],
]);
Expand Down
5 changes: 5 additions & 0 deletions example/api/example_api.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
return new SquareRootTypedEndpoint();
});

$example_api->registerEndpoint('combineDateTimeTyped', function () {
require_once __DIR__.'/Endpoints/CombineDateTimeTypedEndpoint.php';
return new CombineDateTimeTypedEndpoint();
});

$example_api->registerEndpoint('searchSwissPublicTransportConnectionTyped', function () {
require_once __DIR__.'/Endpoints/SwissPublicTransportConnectionsTypedEndpoint.php';
return new SwissPublicTransportConnectionsTypedEndpoint();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace PhpTypeScriptApi\BackendTests\Endpoints;

use PhpTypeScriptApi\BackendTests\Common\ExampleBackendTestCase;

/**
* @internal
*
* @coversNothing
*/
final class CombineDateTimeTypedEndpointTest extends ExampleBackendTestCase {
public function testCombineDateTimeTyped(): void {
$result = $this->callBackend('combineDateTimeTyped', [
'date' => '2025-01-01',
'time' => '13:27:35',
]);
$this->assertSame('', $result['error']);
$this->assertSame(200, $result['http_code']);
$this->assertSame(['dateTime' => '2025-01-01 13:27:35'], $result['result']);
}
}
13 changes: 12 additions & 1 deletion example/web/ExampleApiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export type SPTransportStop = {
'platform': string|null,
};

export type IsoDate = string;

export type _PhpTypeScriptApi_PhpStan_IsoTime = string;

export type IsoDateTime = string;

export type _PhpTypeScriptApi_PhpStan_IsoDate = string;

export type SptLocation = {'id': string, 'name': string, 'coordinate': SptCoordinate};

export type SptConnection = {'sections': Array<SptSection>};
Expand All @@ -48,6 +56,7 @@ export type ExampleApiEndpoint =
'empty'|
'divideNumbersTyped'|
'squareRootTyped'|
'combineDateTimeTyped'|
'searchSwissPublicTransportConnectionTyped'|
'emptyTyped';

Expand All @@ -70,7 +79,8 @@ export interface ExampleApiRequests extends ExampleApiEndpointMapping {
empty: Record<string, never>,
divideNumbersTyped: {'dividend': number, 'divisor': number},
squareRootTyped: (number | number),
searchSwissPublicTransportConnectionTyped: {'from': string, 'to': string, 'via': (Array<string> | null), 'date': string, 'time': string, 'isArrivalTime': (boolean | null)},
combineDateTimeTyped: {'date': IsoDate, 'time': _PhpTypeScriptApi_PhpStan_IsoTime},
searchSwissPublicTransportConnectionTyped: {'from': string, 'to': string, 'via': (Array<string> | null), 'date': _PhpTypeScriptApi_PhpStan_IsoDate, 'time': string, 'isArrivalTime': (boolean | null)},
emptyTyped: Record<string, never>,
}

Expand All @@ -84,6 +94,7 @@ export interface ExampleApiResponses extends ExampleApiEndpointMapping {
empty: Record<string, never>,
divideNumbersTyped: number,
squareRootTyped: number,
combineDateTimeTyped: {'dateTime': IsoDateTime},
searchSwissPublicTransportConnectionTyped: {'stationById': {[key: string]: SptLocation}, 'connections': Array<SptConnection>},
emptyTyped: Record<string, never>,
}
Expand Down
39 changes: 39 additions & 0 deletions example/web/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@
<div><input type='submit' value='Calculate' /></div>
<input type='text' name='result' readonly />
</form>
<h2>Divide (typed)</h2>
<form onsubmit='return example.submitDivideFormTyped(this)'>
<input type='text' name='dividend' />
<input type='text' name='divisor' />
<input type='submit' value='Calculate' />
<input type='text' name='result' readonly />
</form>
<hr />
<h2>Square root (typed)</h2>
<form onsubmit='return example.submitSqrtFormTyped(this)'>
<input type='text' name='input' />
<input type='submit' value='Calculate' />
<input type='text' name='result' readonly />
</form>
<hr />
<h2>Combine Date and Time (typed)</h2>
<form onsubmit='return example.submitCombineDateTimeFormTyped(this)'>
<input type='text' name='date' />
<input type='text' name='time' />
<input type='submit' value='Calculate' />
<input type='text' name='result' readonly />
</form>
<hr />
<h2>Swiss public transport connection (typed)</h2>
<form onsubmit='return example.submitSPTransportConnectionFormTyped(this)'>
<div>From: <input type='text' name='from' /></div>
<div>To: <input type='text' name='to' /></div>
<div>Via: <input type='text' name='via' /> (comma-separated)</div>
<div>Date: <input type='text' name='date' /></div>
<div>Time: <input type='text' name='time' /></div>
<div>
<input type='radio' name='isArrivalTime' value='0' /> Departure
&nbsp;
<input type='radio' name='isArrivalTime' value='1' /> Arrival
</div>
<div><input type='submit' value='Calculate' /></div>
<input type='text' name='result' readonly />
</form>
</body>
</html>
ZZZZZZZZZZ;
64 changes: 64 additions & 0 deletions example/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,67 @@ export function submitSPTransportConnectionForm(form: HTMLFormElement): boolean
return false;
}

export function submitDivideFormTyped(form: HTMLFormElement): boolean {
const dividendField = form.elements.namedItem('dividend');
const divisorField = form.elements.namedItem('divisor');
const resultField = form.elements.namedItem('result');
exampleApi.call('divideNumbersTyped', {
dividend: Number(dividendField && 'value' in dividendField ? dividendField.value : null),
divisor: Number(divisorField && 'value' in divisorField ? divisorField.value : null),
}).then((result) => {
if (resultField && 'value' in resultField) {
resultField.value = `The result is ${result}`;
}
});
return false;
}

export function submitSqrtFormTyped(form: HTMLFormElement): boolean {
const inputField = form.elements.namedItem('input');
const resultField = form.elements.namedItem('result');
exampleApi.call('squareRootTyped', Number(inputField && 'value' in inputField ? inputField.value : null)).then((result) => {
if (resultField && 'value' in resultField) {
resultField.value = `The result is ${result}`;
}
});
return false;
}

export function submitCombineDateTimeFormTyped(form: HTMLFormElement): boolean {
const dateField = form.elements.namedItem('date');
const timeField = form.elements.namedItem('time');
const resultField = form.elements.namedItem('result');
exampleApi.call('combineDateTimeTyped', {
date: dateField && 'value' in dateField ? dateField.value : '',
time: timeField && 'value' in timeField ? timeField.value : '',
}).then((result) => {
if (resultField && 'value' in resultField) {
resultField.value = `The result is ${result.dateTime}`;
}
});
return false;
}

export function submitSPTransportConnectionFormTyped(form: HTMLFormElement): boolean {
const fromField = form.elements.namedItem('from');
const toField = form.elements.namedItem('to');
const viaField = form.elements.namedItem('via');
const dateField = form.elements.namedItem('date');
const timeField = form.elements.namedItem('time');
const isArrivalTimeField = form.elements.namedItem('isArrivalTime');
const resultField = form.elements.namedItem('result');
exampleApi.call('searchSwissPublicTransportConnectionTyped', {
from: fromField && 'value' in fromField ? fromField.value : '',
to: toField && 'value' in toField ? toField.value : '',
via: viaField && 'value' in viaField && viaField.value ? [viaField.value] : [],
date: dateField && 'value' in dateField ? dateField.value : '',
time: timeField && 'value' in timeField ? timeField.value : '',
isArrivalTime: isArrivalTimeField && 'value' in isArrivalTimeField ? isArrivalTimeField.value === '1' : false,
}).then((result) => {
if (resultField && 'value' in resultField) {
resultField.value = `The result is ${result}`;
}
});
return false;
}

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.5",
"version": "2.6.6",
"description": "Build a typed Web API using PHP and TypeScript",
"main": "client/lib/index.js",
"types": "client/lib/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions resources/lang/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"must_be_allowed_value": "Wert muss unter den erlaubten Werten sein.",
"must_be_array": "Wert muss eine Liste sein.",
"must_be_boolean": "Wert muss Ja oder Nein sein.",
"must_be_dict": "Wert muss ein Mapping sein.",
"must_be_integer": "Wert muss eine Ganzzahl sein.",
"must_be_number": "Wert muss eine Zahl sein.",
"must_be_object": "Wert muss ein Objekt sein.",
Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"must_be_allowed_value": "Value must be among the allowed values.",
"must_be_array": "Value must be a list.",
"must_be_boolean": "Value must be yes or no.",
"must_be_dict": "Value must be a mapping.",
"must_be_integer": "Value must be an integer.",
"must_be_number": "Value must be a number.",
"must_be_object": "Value must be an object.",
Expand Down
16 changes: 16 additions & 0 deletions server/lib/PhpStan/ApiObjectInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace PhpTypeScriptApi\PhpStan;

/**
* @template Data
*/
interface ApiObjectInterface {
/** @return Data */
public function data(): mixed;

/**
* @return ApiObjectInterface<Data>
*/
public static function fromData(mixed $data): ApiObjectInterface;
}
26 changes: 26 additions & 0 deletions server/lib/PhpStan/IsoDate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace PhpTypeScriptApi\PhpStan;

/**
* @implements ApiObjectInterface<non-empty-string>
*/
class IsoDate extends \DateTime implements ApiObjectInterface {
public function data(): mixed {
return $this->format('Y-m-d');
}

public static function fromData(mixed $data): IsoDate {
if (!is_string($data)) {
throw new \InvalidArgumentException("IsoDate must be string");
}
if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $data)) {
throw new \InvalidArgumentException("IsoDate must be Y-m-d");
}
return new IsoDate($data);
}

public function __toString(): string {
return "{$this->data()}";
}
}
26 changes: 26 additions & 0 deletions server/lib/PhpStan/IsoDateTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace PhpTypeScriptApi\PhpStan;

/**
* @implements ApiObjectInterface<non-empty-string>
*/
class IsoDateTime extends \DateTime implements ApiObjectInterface {
public function data(): mixed {
return $this->format('Y-m-d H:i:s');
}

public static function fromData(mixed $data): IsoDateTime {
if (!is_string($data)) {
throw new \InvalidArgumentException("IsoDateTime must be string");
}
if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/', $data)) {
throw new \InvalidArgumentException("IsoDateTime must be Y-m-d H:i:s");
}
return new IsoDateTime($data);
}

public function __toString(): string {
return "{$this->data()}";
}
}
Loading

0 comments on commit 081beed

Please sign in to comment.