mirror of
https://github.com/archtechx/airwire.git
synced 2025-12-12 10:44:03 +00:00
initial
This commit is contained in:
commit
d26fa93f1e
35 changed files with 2388 additions and 0 deletions
241
src/TypehintConverter.php
Normal file
241
src/TypehintConverter.php
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace Airwire;
|
||||
|
||||
use Airwire\Attributes\Wired;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionObject;
|
||||
use ReflectionProperty;
|
||||
use ReflectionUnionType;
|
||||
|
||||
class TypehintConverter
|
||||
{
|
||||
public array $namedTypes = [];
|
||||
|
||||
public function convertBuiltinType(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'int' => 'number',
|
||||
'float' => 'number',
|
||||
'string' => 'string',
|
||||
'array' => 'any', // Arrays can be associative, so they're essentially objects
|
||||
'object' => 'any',
|
||||
'null' => 'null',
|
||||
default => 'any',
|
||||
};
|
||||
}
|
||||
|
||||
public function convertType(string $php): string
|
||||
{
|
||||
if (class_exists($php)) {
|
||||
if (is_subclass_of($php, Model::class) && ($model = $php::first())) {
|
||||
return $this->convertModel($model);
|
||||
}
|
||||
|
||||
if (is_subclass_of($php, Collection::class) && ($model = $php::first())) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
return 'any';
|
||||
}
|
||||
|
||||
return $this->convertBuiltinType($php);
|
||||
}
|
||||
|
||||
public function typeFromValue(mixed $value): string
|
||||
{
|
||||
return match (true) {
|
||||
$value instanceof Model => $this->convertModel($value),
|
||||
$value instanceof Collection => 'array',
|
||||
default => $this->convertBuiltinType(gettype($value)),
|
||||
};
|
||||
}
|
||||
|
||||
public function convertModel(Model $model): string
|
||||
{
|
||||
$alias = $this->getClassName($model);
|
||||
|
||||
if (! isset($this->namedTypes[$alias])) {
|
||||
$this->namedTypes[$alias] = 'pending'; // We do this to avoid infinite loops when recursively generating model type definitions
|
||||
|
||||
$values = $model->toArray()
|
||||
?: $model->first()->toArray() // If this model is empty, attempt finding the first one in the DB
|
||||
?: collect(Schema::getColumnListing($model->getTable()))->mapWithKeys(fn (string $column) => [$column => []])->toArray(); // [] for any
|
||||
|
||||
$this->namedTypes[$alias] = '{ ' .
|
||||
collect($values)
|
||||
->map(fn (mixed $value) => $this->typeFromValue($value))
|
||||
->map(function (string $type, string $property) use ($model) {
|
||||
if ($model->getKeyName() !== $property) {
|
||||
// Don't do anything
|
||||
return $type;
|
||||
}
|
||||
|
||||
if ($type === 'any' && $model->getIncrementing()) {
|
||||
$type = 'number';
|
||||
}
|
||||
|
||||
return $type;
|
||||
})
|
||||
->merge($this->getModelRelations($model))
|
||||
->map(fn (string $type, string $property) => "{$property}: {$type}")->join(', ')
|
||||
. ' }';
|
||||
}
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
public function getModelRelations(Model $model): array
|
||||
{
|
||||
$loaded = collect($model->getRelations())
|
||||
->map(fn ($value) => $value instanceof Enumerable ? $value->first() : $value) // todo plural relations are incorrectly typed - should be e.g. Report[]
|
||||
->filter(fn ($value) => $value instanceof Model);
|
||||
|
||||
/** @var Collection<string, Model> */
|
||||
$reflected = collect((new ReflectionObject($model))->getMethods())
|
||||
->keyBy(fn (ReflectionMethod $method) => $method->getName())
|
||||
->filter(fn (ReflectionMethod $method) => $method->getReturnType() && is_subclass_of($method->getReturnType()->getName(), Relation::class)) // todo support this even without typehints
|
||||
->map(fn (ReflectionMethod $method, string $name) => $model->$name()->getRelated())
|
||||
->filter(fn ($value, $relation) => ! $loaded->has($relation)); // Ignore relations that we could find using getRelations()
|
||||
|
||||
$relations = $loaded->merge($reflected);
|
||||
|
||||
return $relations->map(fn (Model $model) => $this->convertModel($model))->toArray();
|
||||
}
|
||||
|
||||
public function getClassName(object|string $class): string
|
||||
{
|
||||
if (is_object($class)) {
|
||||
$class = $class::class;
|
||||
}
|
||||
|
||||
return last(explode('\\', $class));
|
||||
}
|
||||
|
||||
public function convertComponent(Component $component): string
|
||||
{
|
||||
$properties = $component->getSharedProperties();
|
||||
$methods = $component->getSharedMethods();
|
||||
|
||||
$tsProperties = [];
|
||||
$tsMethods = [];
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$tsProperties[$property] = $this->convertProperty($component, $property);
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$tsMethods[] = $this->convertMethod($component, $method);
|
||||
}
|
||||
|
||||
$definition = '';
|
||||
|
||||
$class = $this->getClassName($component);
|
||||
$definition .= "interface {$class} {\n";
|
||||
|
||||
foreach ($tsProperties as $property => $type) {
|
||||
$definition .= " {$property}: {$type};\n";
|
||||
}
|
||||
|
||||
foreach ($tsMethods as $signature) {
|
||||
$definition .= " {$signature}\n";
|
||||
}
|
||||
|
||||
$definition .= <<<TS
|
||||
errors: {
|
||||
[key in keyof WiredProperties<{$class}>]: string[];
|
||||
}
|
||||
|
||||
loading: boolean;
|
||||
|
||||
watch(responses: (response: ComponentResponse<{$class}>) => void, errors?: (error: AirwireException) => void): void;
|
||||
defer(callback: CallableFunction): void;
|
||||
refresh(): ComponentResponse<{$class}>;
|
||||
remount(...args: any): ComponentResponse<{$class}>;
|
||||
|
||||
readonly: {$class};
|
||||
|
||||
deferred: {$class};
|
||||
\$component: {$class};
|
||||
}
|
||||
TS;
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
public function convertProperty(object $object, string $property): string
|
||||
{
|
||||
$reflection = new ReflectionProperty($object, $property);
|
||||
|
||||
if ($wired = optional($reflection->getAttributes(Wired::class))[0]) {
|
||||
if ($type = $wired->newInstance()->type) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
$type = $reflection->getType();
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
$types = $type->getTypes();
|
||||
} else {
|
||||
$types = [$type];
|
||||
}
|
||||
|
||||
if ($type->allowsNull()) {
|
||||
$types[] = 'null';
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
// If we're working with a union type, some types are only accessible
|
||||
// from the typehint, but for one type we'll also have the value.
|
||||
if (isset($object->$property) && gettype($object->$property) === $type->getName()) {
|
||||
$results[] = $this->typeFromValue($object->$property);
|
||||
} else {
|
||||
$results[] = $this->convertType($type->getName());
|
||||
}
|
||||
}
|
||||
|
||||
return join(' | ', $results);
|
||||
}
|
||||
|
||||
public function convertMethod(object $object, string $method): string
|
||||
{
|
||||
$reflection = new ReflectionMethod($object, $method);
|
||||
|
||||
$parameters = [];
|
||||
|
||||
foreach ($reflection->getParameters() as $parameter) {
|
||||
$type = $parameter->getType();
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
$types = $type->getTypes();
|
||||
} else {
|
||||
$types = [$type];
|
||||
}
|
||||
|
||||
if ($type->allowsNull()) {
|
||||
$types[] = 'null';
|
||||
}
|
||||
|
||||
$parameters[$parameter->getName()] = join(' | ', array_map(fn (ReflectionNamedType $type) => $this->convertType($type->getName()), $types));
|
||||
}
|
||||
|
||||
$parameters = collect($parameters)->map(fn (string $type, string $name) => "{$name}: {$type}")->join(', ');
|
||||
|
||||
$return = match ($type = $reflection->getReturnType()) {
|
||||
null => 'any',
|
||||
default => $this->convertType($type),
|
||||
};
|
||||
|
||||
return "{$method}(" . $parameters . "): AirwirePromise<{$return}>;";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue