diff --git a/php-templates/models.php b/php-templates/models.php index 1571ce07..3524d816 100644 --- a/php-templates/models.php +++ b/php-templates/models.php @@ -159,8 +159,11 @@ protected function getInfo($className) ->toArray(); $data['scopes'] = collect($reflection->getMethods()) - ->filter(fn($method) =>!$method->isStatic() && ($method->getAttributes(\Illuminate\Database\Eloquent\Attributes\Scope::class) || ($method->isPublic() && str_starts_with($method->name, 'scope')))) - ->map(fn($method) => str($method->name)->replace('scope', '')->lcfirst()->toString()) + ->filter(fn(\ReflectionMethod $method) => !$method->isStatic() && ($method->getAttributes(\Illuminate\Database\Eloquent\Attributes\Scope::class) || ($method->isPublic() && str_starts_with($method->name, 'scope')))) + ->map(fn(\ReflectionMethod $method) => [ + "name" => str($method->name)->replace('scope', '')->lcfirst()->toString(), + "parameters" => collect($method->getParameters())->map($this->getScopeParameterInfo(...)), + ]) ->values() ->toArray(); @@ -170,6 +173,84 @@ protected function getInfo($className) $className => $data, ]; } + + protected function getScopeParameterInfo(\ReflectionParameter $parameter): array + { + $result = [ + "name" => $parameter->getName(), + "type" => $this->typeToString($parameter->getType()), + "hasDefault" => $parameter->isDefaultValueAvailable(), + "isVariadic" => $parameter->isVariadic(), + "isPassedByReference" => $parameter->isPassedByReference(), + ]; + + if ($parameter->isDefaultValueAvailable()) { + $result['default'] = $this->defaultValueToString($parameter); + } + + return $result; + } + + protected function typeToString(?\ReflectionType $type): string + { + return match (true) { + $type instanceof \ReflectionNamedType => $this->namedTypeToString($type), + $type instanceof \ReflectionUnionType => $this->unionTypeToString($type), + $type instanceof \ReflectionIntersectionType => $this->intersectionTypeToString($type), + default => 'mixed', + }; + } + + protected function namedTypeToString(\ReflectionNamedType $type): string + { + $name = $type->getName(); + + if (! $type->isBuiltin() && ! in_array($name, ['self', 'parent', 'static'])) { + $name = '\\'.$name; + } + + if ($type->allowsNull() && ! in_array($name, ['null', 'mixed', 'void'])) { + $name = '?'.$name; + } + + return $name; + } + + protected function unionTypeToString(\ReflectionUnionType $type): string + { + return implode('|', array_map(function (\ReflectionType $type) { + $result = $this->typeToString($type); + + if ($type instanceof \ReflectionIntersectionType) { + return "({$result})"; + } + + return $result; + }, $type->getTypes())); + } + + protected function intersectionTypeToString(\ReflectionIntersectionType $type): string + { + return implode('&', array_map($this->typeToString(...), $type->getTypes())); + } + + protected function defaultValueToString(\ReflectionParameter $param): string + { + if ($param->isDefaultValueConstant()) { + return '\\'.$param->getDefaultValueConstantName(); + } + + $value = $param->getDefaultValue(); + + return match (true) { + is_null($value) => 'null', + is_numeric($value) => $value, + is_bool($value) => $value ? 'true' : 'false', + is_array($value) => '[]', + is_object($value) => 'new \\'.get_class($value), + default => "'{$value}'", + }; + } }; $builder = new class($docblocks) { diff --git a/src/index.d.ts b/src/index.d.ts index dbc537e8..2477a897 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -81,7 +81,7 @@ declare namespace Eloquent { relations: Relation[]; events: Event[]; observers: Observer[]; - scopes: string[]; + scopes: Scope[]; extends: string | null; } @@ -115,4 +115,18 @@ declare namespace Eloquent { event: string; observer: string[]; } + + interface Scope { + name: string; + parameters: ScopeParameter[]; + } + + interface ScopeParameter { + name: string; + type: string; + default?: string | null; + isOptional: boolean; + isVariadic: boolean; + isPassedByReference: boolean; + } } diff --git a/src/support/docblocks.ts b/src/support/docblocks.ts index 45c73681..d59f4b63 100644 --- a/src/support/docblocks.ts +++ b/src/support/docblocks.ts @@ -114,14 +114,13 @@ const getBlocks = ( return model.attributes .map((attr) => getAttributeBlocks(attr, className)) .concat( - [...model.scopes, "newModelQuery", "newQuery", "query"].map( - (method) => { - return `@method static ${modelBuilderType( - className, - )} ${method}()`; - }, - ), + ["newModelQuery", "newQuery", "query"].map((method) => { + return `@method static ${modelBuilderType( + className, + )} ${method}()`; + }), ) + .concat(model.scopes.map((scope) => getScopeBlock(className, scope))) .concat(model.relations.map((relation) => getRelationBlocks(relation))) .flat() .map((block) => ` * ${block}`) @@ -175,6 +174,25 @@ const getRelationBlocks = (relation: Eloquent.Relation): string[] => { return [`@property-read \\${relation.related} $${relation.name}`]; }; +const getScopeBlock = (className: string, scope: Eloquent.Scope): string => { + const parameters = scope.parameters + .slice(1) + .map((param) => { + return [ + param.type, + param.isVariadic ? " ..." : " ", + param.isPassedByReference ? "&" : "", + `$${param.name}`, + param.default ? ` = ${param.default}` : "", + ].join(""); + }) + .join(", "); + + return `@method static ${modelBuilderType( + className, + )} ${scope.name}(${parameters})`; +}; + const classToDocBlock = (block: ClassBlock, namespace: string) => { return [ `/**`, diff --git a/src/templates/models.ts b/src/templates/models.ts index 282e3864..bbca37bb 100644 --- a/src/templates/models.ts +++ b/src/templates/models.ts @@ -159,8 +159,11 @@ $models = new class($factory) { ->toArray(); $data['scopes'] = collect($reflection->getMethods()) - ->filter(fn($method) =>!$method->isStatic() && ($method->getAttributes(\\Illuminate\\Database\\Eloquent\\Attributes\\Scope::class) || ($method->isPublic() && str_starts_with($method->name, 'scope')))) - ->map(fn($method) => str($method->name)->replace('scope', '')->lcfirst()->toString()) + ->filter(fn(\\ReflectionMethod $method) => !$method->isStatic() && ($method->getAttributes(\\Illuminate\\Database\\Eloquent\\Attributes\\Scope::class) || ($method->isPublic() && str_starts_with($method->name, 'scope')))) + ->map(fn(\\ReflectionMethod $method) => [ + "name" => str($method->name)->replace('scope', '')->lcfirst()->toString(), + "parameters" => collect($method->getParameters())->map($this->getScopeParameterInfo(...)), + ]) ->values() ->toArray(); @@ -170,6 +173,84 @@ $models = new class($factory) { $className => $data, ]; } + + protected function getScopeParameterInfo(\\ReflectionParameter $parameter): array + { + $result = [ + "name" => $parameter->getName(), + "type" => $this->typeToString($parameter->getType()), + "hasDefault" => $parameter->isDefaultValueAvailable(), + "isVariadic" => $parameter->isVariadic(), + "isPassedByReference" => $parameter->isPassedByReference(), + ]; + + if ($parameter->isDefaultValueAvailable()) { + $result['default'] = $this->defaultValueToString($parameter); + } + + return $result; + } + + protected function typeToString(?\\ReflectionType $type): string + { + return match (true) { + $type instanceof \\ReflectionNamedType => $this->namedTypeToString($type), + $type instanceof \\ReflectionUnionType => $this->unionTypeToString($type), + $type instanceof \\ReflectionIntersectionType => $this->intersectionTypeToString($type), + default => 'mixed', + }; + } + + protected function namedTypeToString(\\ReflectionNamedType $type): string + { + $name = $type->getName(); + + if (! $type->isBuiltin() && ! in_array($name, ['self', 'parent', 'static'])) { + $name = '\\\\'.$name; + } + + if ($type->allowsNull() && ! in_array($name, ['null', 'mixed', 'void'])) { + $name = '?'.$name; + } + + return $name; + } + + protected function unionTypeToString(\\ReflectionUnionType $type): string + { + return implode('|', array_map(function (\\ReflectionType $type) { + $result = $this->typeToString($type); + + if ($type instanceof \\ReflectionIntersectionType) { + return "({$result})"; + } + + return $result; + }, $type->getTypes())); + } + + protected function intersectionTypeToString(\\ReflectionIntersectionType $type): string + { + return implode('&', array_map($this->typeToString(...), $type->getTypes())); + } + + protected function defaultValueToString(\\ReflectionParameter $param): string + { + if ($param->isDefaultValueConstant()) { + return '\\\\'.$param->getDefaultValueConstantName(); + } + + $value = $param->getDefaultValue(); + + return match (true) { + is_null($value) => 'null', + is_numeric($value) => $value, + is_bool($value) => $value ? 'true' : 'false', + is_array($value) => '[]', + is_object($value) => 'new \\\\'.get_class($value), + default => "'{$value}'", + }; + } }; $builder = new class($docblocks) {