diff --git a/README.md b/README.md index 9860703..269c444 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ # Laravel Actions IDE Helper -This packages generates IDE helpers for [Laravel Actions v2](https://github.com/lorisleiva/laravel-actions). Currently under development 🚧. Feedback appreciated. Discussion at https://github.com/lorisleiva/laravel-actions/issues/117. +This packages generates IDE helpers for [Laravel Actions v2](https://github.com/lorisleiva/laravel-actions). Feedback appreciated. Discussion at https://github.com/lorisleiva/laravel-actions/issues/117. + +## YOLO-ware + +As I don't use Laravel Actions anymore I decided to go into `YOLO-mode` with this project. That means: + +1. **No guarantees** +2. **Community-powered fixes**: Something doesn't work, you notice it you fix it. +3. **Trust in the community:** I won't test your changes, either they do work or they don't. I just merge them. +4. **Looking for a maintainer:** This situation is less than ideal. Therefore, I am looking for a new maintainer to take over this project. ## Installation @@ -11,4 +20,4 @@ composer require --dev wulfheart/laravel-actions-ide-helper ## Usage ``` php artisan ide-helper:actions -``` \ No newline at end of file +``` diff --git a/composer.json b/composer.json index 7946a00..67164ae 100644 --- a/composer.json +++ b/composer.json @@ -16,18 +16,24 @@ } ], "require": { - "php": "^8.0", - "illuminate/contracts": "^8.37", + "php": "^8.1|^8.2", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "lorisleiva/laravel-actions": "^2.3", + "lorisleiva/lody": "^0.5.0|^0.6.0", + "phpdocumentor/reflection": "^5.1|^6.0", "riimu/kit-pathjoin": "^1.2", - "spatie/laravel-package-tools": "^1.4.3" + "spatie/laravel-package-tools": "^1.14" }, "require-dev": { - "brianium/paratest": "^6.2", - "nunomaduro/collision": "^5.3", - "orchestra/testbench": "^6.15", - "phpunit/phpunit": "^9.3", - "spatie/laravel-ray": "^1.9", - "vimeo/psalm": "^4.4" + "brianium/paratest": "^6.8|^7.4", + "nunomaduro/collision": "^6.1|^8.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "pestphp/pest": "^1.22|^2.34|^3.7", + "phpunit/phpunit": "^9.5.10|^10.5|^11.5.3", + "spatie/invade": "^1.1|^2.0", + "spatie/laravel-ray": "^1.32", + "spatie/pest-plugin-snapshots": "^1.1|^2.1", + "vimeo/psalm": "^5.6|^6.6" }, "autoload": { "psr-4": { @@ -40,10 +46,13 @@ "Wulfheart\\LaravelActionsIdeHelper\\Tests\\": "tests" } }, - "scripts": { - }, + "scripts": [], "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "pestphp/pest-plugin": true + } }, "extra": { "laravel": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9e2f33e..d6cf188 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,39 +1,23 @@ - - - - tests - - - - - ./src - - - - - - - - - - + + + + tests + + + + + + + + + + + + + + + ./src + + diff --git a/src/Commands/LaravelActionsIdeHelperCommand.php b/src/Commands/LaravelActionsIdeHelperCommand.php index 813d9dd..0b72993 100644 --- a/src/Commands/LaravelActionsIdeHelperCommand.php +++ b/src/Commands/LaravelActionsIdeHelperCommand.php @@ -2,17 +2,17 @@ namespace Wulfheart\LaravelActionsIdeHelper\Commands; -use Composer\Autoload\ClassMapGenerator; use Illuminate\Console\Command; -use phpDocumentor\Reflection\Php\Factory\Type; -use phpDocumentor\Reflection\TypeResolver; -use phpDocumentor\Reflection\Types\Nullable; +use Illuminate\Support\Str; +use phpDocumentor\Reflection\File\LocalFile; use PhpParser\BuilderFactory; use PhpParser\PrettyPrinter\Standard; use ReflectionClass; use Riimu\Kit\PathJoin\Path; use Symfony\Component\Finder\Finder; +use Wulfheart\LaravelActionsIdeHelper\ClassMapGenerator; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; +use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfoFactory; use Wulfheart\LaravelActionsIdeHelper\Service\BuildIdeHelper; use Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\AsObjectGenerator; @@ -24,36 +24,17 @@ class LaravelActionsIdeHelperCommand extends Command public function handle() { - $this->traverseFiles(); - $this->comment('IDE Helpers generated for Laravel Actions at ./_ide_helper_actions.php'); - } - protected function traverseFiles() - { - $finder = Finder::create() - ->files() - ->in(app_path()) - ->name('*.php'); - - $map = collect(ClassMapGenerator::createMap($finder->getIterator())); - // dd($map); - $classes = $map->keys(); - - $infos = []; - - foreach ($classes as $class) { - // Fail gracefully if there is any problem with a reflection class - try { - $reflection = new ReflectionClass($class); - $ai = ActionInfo::createFromReflectionClass($reflection); - if (! is_null($ai)) { - $infos[] = $ai; - } - } catch (\Throwable) { - } - } - - $result = BuildIdeHelper::create()->build($infos); - file_put_contents(Path::join([base_path(), '_ide_helper_actions.php']), $result); + $actionsPath = Path::join(app_path() . '/Actions'); + + $outfile = Path::join(base_path(), '/_ide_helper_actions.php'); + + $actionInfos = ActionInfoFactory::create($actionsPath); + + $result = BuildIdeHelper::create()->build($actionInfos); + + file_put_contents($outfile, $result); + + $this->comment('IDE Helpers generated for Laravel Actions at ' . Str::of($outfile)); } } diff --git a/src/Service/ActionInfo.php b/src/Service/ActionInfo.php index 43a0132..b7c80fd 100644 --- a/src/Service/ActionInfo.php +++ b/src/Service/ActionInfo.php @@ -2,13 +2,15 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service; +use Illuminate\Support\Str; use JetBrains\PhpStorm\Pure; -use phpDocumentor\Reflection\Type; -use phpDocumentor\Reflection\TypeResolver; -use PhpParser\BuilderFactory; -use PhpParser\PrettyPrinter\Standard; -use PhpParser\PrettyPrinterAbstract; -use ReflectionClass; +use Lorisleiva\Actions\Concerns\AsCommand; +use Lorisleiva\Actions\Concerns\AsController; +use Lorisleiva\Actions\Concerns\AsFake; +use Lorisleiva\Actions\Concerns\AsJob; +use Lorisleiva\Actions\Concerns\AsListener; +use Lorisleiva\Actions\Concerns\AsObject; +use phpDocumentor\Reflection\Php\Class_; use Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\AsCommandGenerator; use Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\AsControllerGenerator; use Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\AsJobGenerator; @@ -18,75 +20,34 @@ final class ActionInfo { public string $name; + public string $namespace; + public string $fqsen; public bool $asObject; public bool $asController; public bool $asJob; public bool $asListener; public bool $asCommand; - /** @var array $functionInfos */ - public array $functionInfos = []; + public Class_ $classInfo; + const ALL_TRAITS = [ + AsObject::class, + AsController::class, + AsJob::class, + AsListener::class, + AsCommand::class, + AsFake::class, + ]; - const AS_ACTION_NAME = "Lorisleiva\Actions\Concerns\AsAction"; - const AS_OBJECT_NAME = "Lorisleiva\Actions\Concerns\AsObject"; - const AS_CONTROLLER_NAME = "Lorisleiva\Actions\Concerns\AsController"; - const AS_LISTENER_NAME = "Lorisleiva\Actions\Concerns\AsListener"; - const AS_JOB_NAME = "Lorisleiva\Actions\Concerns\AsJob"; - const AS_COMMAND_NAME = "Lorisleiva\Actions\Concerns\AsCommand"; - const AS_FAKE_NAME = "Lorisleiva\Actions\Concerns\AsFake"; - - #[Pure] public static function create(): ActionInfo + public static function create(): ActionInfo { return new ActionInfo(); } - public static function createFromReflectionClass(ReflectionClass $reflection): ?ActionInfo - { - $traits = collect(ActionInfo::getAllTraits($reflection)); - - $intersection = $traits->intersect([ - // Constants that are hard-coded for now - self::AS_OBJECT_NAME, - self::AS_CONTROLLER_NAME, - self::AS_LISTENER_NAME, - self::AS_JOB_NAME, - self::AS_COMMAND_NAME, - ]); - - if ($intersection->count() <= 0) { - return null; - } - - return self::create() - ->setName($reflection->getName()) - ->setAsObject($intersection->contains(self::AS_OBJECT_NAME)) - ->setAsController($intersection->contains(self::AS_CONTROLLER_NAME)) - ->setAsListener($intersection->contains(self::AS_LISTENER_NAME)) - ->setAsJob($intersection->contains(self::AS_JOB_NAME)) - ->setAsCommand($intersection->contains(self::AS_COMMAND_NAME)) - ->setFunctionInfos([ - self::AS_OBJECT_NAME => self::resolveFunctionInfo($reflection), - self::AS_CONTROLLER_NAME => self::resolveFunctionInfo($reflection, 'asController'), - self::AS_LISTENER_NAME => self::resolveFunctionInfo($reflection, 'asListener'), - self::AS_JOB_NAME => self::resolveFunctionInfo($reflection, 'asJob'), - self::AS_COMMAND_NAME => self::resolveFunctionInfo($reflection, 'asCommand'), - ]); - } - - - /** - * @param array $functionInfos - */ - public function setFunctionInfos(array $functionInfos): ActionInfo - { - $this->functionInfos = $functionInfos; - return $this; - } - - public function setName(string $name): ActionInfo { - $this->name = $name; + $this->fqsen = $name; + $this->name = class_basename($name); + $this->namespace = Str::of($name)->beforeLast('\\' . $this->name); return $this; } @@ -126,104 +87,12 @@ public function setAsCommand(bool $asCommand): ActionInfo return $this; } - public function setReturnTypehint(?string $returnTypehint): ActionInfo - { - $this->returnTypehint = $returnTypehint ?? ''; - - return $this; - } - - public function addParameter(ParameterInfo $pi): ActionInfo + public function setClassInfo(Class_ $classInfo): ActionInfo { - $this->parameters[] = $pi; - + $this->classInfo = $classInfo; return $this; } - public function getNamespace(): string - { - $name = explode('\\', $this->name); - array_pop($name); - - return implode('\\', $name); - } - - public function getClass(): string - { - $name = explode('\\', $this->name); - - return $name[array_key_last($name)]; - } - - public function getReturnType(): ?Type - { - return (new TypeResolver())->resolve($this->returnTypehint); - } - - protected static function getAllTraits(ReflectionClass $reflection): array - { - $traitNames = []; - $traits = $reflection->getTraits(); - foreach ($traits as $trait) { - array_push($traitNames, $trait->getName()); - - // Get all child traits - array_push($traitNames, ...ActionInfo::getAllTraits($trait)); - } - - return $traitNames; - } - - protected static function resolveFunctionInfo(ReflectionClass $reflection, string $decorator = null): ?FunctionInfo - { - if ($decorator) { - $namesToTry = [$decorator, 'handle']; - } else { - $namesToTry = ['handle']; - } - foreach ($namesToTry as $name) { - try { - $function = $reflection->getMethod($name); - $fi = FunctionInfo::create(); - $rt = $function->getReturnType()?->getName(); - if (!is_null($rt)) { - $fi->setReturnType($function->getReturnType()?->getName() ?? ""); - } - foreach ($function->getParameters() as $parameter) { - try { - $default = $parameter->getDefaultValue(); - $defaultSet = true; - $factory = new BuilderFactory(); - $node = $factory->param($parameter->getName())->setDefault($default)->getNode(); - $printer = new Standard(); - $name = ltrim($printer->prettyPrint([$node]), '$'); - } catch (\Throwable) { - $name = $parameter->getName(); - } - - $pi = ParameterInfo::create() - ->setName($name) - ->setNullable($parameter->allowsNull()) - ->setPosition($parameter->getPosition()) - ->setVariadic($parameter->isVariadic()); - $temp = $parameter->getName(); - $defaultSet = false; - if ($parameter->hasType()) { - $pi->setTypehint($parameter->getType()?->getName()); - } - if ($parameter->isOptional()) { - $pi->setDefault((string) $parameter->getDefaultValue()); - } - $fi->addParameter($pi); - } - - return $fi; - } catch (\Throwable) { - - } - } - return null; - } /** * @return \Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\DocBlockGeneratorInterface[] @@ -239,8 +108,6 @@ public function getGenerators(): array ); } - public function getFunctionInfosByContext(string $ctx): ?FunctionInfo - { - return $this->functionInfos[$ctx] ?? null; - } + + } diff --git a/src/Service/ActionInfoFactory.php b/src/Service/ActionInfoFactory.php new file mode 100644 index 0000000..c126cb5 --- /dev/null +++ b/src/Service/ActionInfoFactory.php @@ -0,0 +1,85 @@ + */ + public static function create(string $path): array + { + $factory = new self(); + $classes = $factory->loadFromPath($path); + $classMap = $factory->loadPhpDocumentorReflectionClassMap($path); + $ais = []; + foreach ($classes as $class => $traits){ + $tc = collect($traits); + $reflection = new \ReflectionClass($class); + $ais[] = ActionInfo::create() + ->setName($class) + ->setAsObject($tc->contains(AsObject::class)) + ->setAsCommand($tc->contains(AsCommand::class)) + ->setAsController($tc->contains(AsController::class)) + ->setAsJob($tc->contains(AsJob::class)) + ->setAsListener($tc->contains(AsListener::class)) + ->setClassInfo($classMap[$class]); + } + return $ais; + + + } + + /** @return array> */ + protected function loadFromPath(string $path) + { + $res = Lody::classes($path)->isNotAbstract(); + /** @var array> $traits */ + return collect(ActionInfo::ALL_TRAITS) + ->map(fn($trait, $key) => [$trait => $res->hasTrait($trait)->all()]) + ->collapse() + ->map(function ($item, $key) { + return collect($item) + ->map(fn($i) => [ + 'item' => $i, + 'group' => $key, + ]) + ->toArray(); + }) + ->values() + ->collapse() + ->groupBy('item') + ->map(fn($item) => $item->pluck('group')->toArray()) + ->toArray(); + } + + /** @return array<\phpDocumentor\Reflection\Php\Class_> + * @throws \phpDocumentor\Reflection\Exception + */ + protected function loadPhpDocumentorReflectionClassMap(string $path): array{ + $finder = Finder::create()->files()->in($path)->name('*.php'); + $files = collect($finder)->map(fn(SplFileInfo $file) => new LocalFile($file->getRealPath()))->toArray(); + + /** @var \phpDocumentor\Reflection\Php\Project $project */ + $project = ProjectFactory::createInstance()->create('Laravel Actions IDE Helper', $files); + return collect($project->getFiles()) + ->map(fn(File $f) => $f->getClasses()) + ->collapse() + ->mapWithKeys(fn($item, string $key) => [Str::of($key)->ltrim("\\")->toString() => $item]) + ->toArray(); + + } + +} \ No newline at end of file diff --git a/src/Service/BuildIdeHelper.php b/src/Service/BuildIdeHelper.php index 1c4aaf9..36cecef 100644 --- a/src/Service/BuildIdeHelper.php +++ b/src/Service/BuildIdeHelper.php @@ -3,41 +3,30 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service; -use JetBrains\PhpStorm\Pure; -use Lorisleiva\Actions\Concerns\AsController; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Serializer; use phpDocumentor\Reflection\DocBlock\Tag; -use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\TypeResolver; -use PhpParser\Builder\Method; use PhpParser\Builder\Trait_; use PhpParser\BuilderFactory; use PhpParser\Comment\Doc; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Expression; use PhpParser\PrettyPrinter\Standard; class BuildIdeHelper { - #[Pure] - public static function create(): BuildIdeHelper - { - return new BuildIdeHelper(); - } + public static function create(): BuildIdeHelper + { + return new BuildIdeHelper(); + } /** * @param \Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo[] $actionInfos */ - - public function build(array $actionInfos): string { - - $groups = collect($actionInfos)->groupBy(function (ActionInfo $item) { - return $item->getNamespace(); + return $item->namespace; })->toArray(); $nodes = []; @@ -48,10 +37,10 @@ public function build(array $actionInfos): string $factory = new BuilderFactory(); foreach ($groups as $namespace => $items) { $ns = $factory->namespace($namespace); - foreach ($items as $item){ - $ns->addStmt($factory->class($item->getClass())->setDocComment(new Doc($this->generateDocBlocks($item)))); + foreach ($items as $item) { + $ns->addStmt($factory->class($item->classInfo->getName())->setDocComment(new Doc($this->generateDocBlocks($item)))); } - $nodes[] = $ns->getNode(); + $nodes[] = $ns->getNode(); } $nodes[] = $this->getTraitIdeHelpers($factory); $printer = new Standard(); @@ -70,7 +59,8 @@ protected function generateDocBlocks(ActionInfo $info): string return $this->serializeDocBlocks(...$tags); } - protected function serializeDocBlocks(Tag ...$tags): string { + protected function serializeDocBlocks(Tag ...$tags): string + { $db = new DocBlock('', null, $tags); $serializer = new Serializer(); @@ -87,7 +77,8 @@ protected function resolveAsUnionType(string ...$types): Type return (new TypeResolver())->resolve(implode('|', $types)); } - protected function getTraitIdeHelpers(BuilderFactory $factory): \PhpParser\Node{ + protected function getTraitIdeHelpers(BuilderFactory $factory): \PhpParser\Node + { return $factory->namespace("Lorisleiva\Actions\Concerns") ->addStmt( (new Trait_("AsController"))->setDocComment( @@ -112,8 +103,8 @@ protected function getTraitIdeHelpers(BuilderFactory $factory): \PhpParser\Node{ ->addStmt( (new Trait_("AsCommand"))->setDocComment( $this->serializeDocBlocks( - new DocBlock\Tags\Method('asCommand',arguments: [ - ['name' => 'command', 'type' => $this->resolveType("\Illuminate\Console\Command")] + new DocBlock\Tags\Method('asCommand', arguments: [ + ['name' => 'command', 'type' => $this->resolveType("\Illuminate\Console\Command")], ], returnType: $this->resolveType('void')) ) ) diff --git a/src/Service/FunctionInfo.php b/src/Service/FunctionInfo.php deleted file mode 100644 index 4f2faf4..0000000 --- a/src/Service/FunctionInfo.php +++ /dev/null @@ -1,47 +0,0 @@ -returnType = null; - } - - public function setReturnType(string $returnType): FunctionInfo - { - $this->returnType = (new TypeResolver())->resolve($returnType); - return $this; - } - - public function setParameterInfos(array $parameterInfos): FunctionInfo - { - $this->parameterInfos = $parameterInfos; - return $this; - } - - public function addParameter(ParameterInfo $param): FunctionInfo - { - array_push($this->parameterInfos, $param); - return $this; - } - - - - -} \ No newline at end of file diff --git a/src/Service/Generator/DocBlock/AsCommandGenerator.php b/src/Service/Generator/DocBlock/AsCommandGenerator.php index 6591ed9..cd3d482 100644 --- a/src/Service/Generator/DocBlock/AsCommandGenerator.php +++ b/src/Service/Generator/DocBlock/AsCommandGenerator.php @@ -3,11 +3,12 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock; +use Lorisleiva\Actions\Concerns\AsCommand; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; class AsCommandGenerator extends DocBlockGeneratorBase implements DocBlockGeneratorInterface { - protected string $context = ActionInfo::AS_COMMAND_NAME; + protected string $context = AsCommand::class; /** * @inheritDoc */ diff --git a/src/Service/Generator/DocBlock/AsControllerGenerator.php b/src/Service/Generator/DocBlock/AsControllerGenerator.php index 3d37338..2c78bbc 100644 --- a/src/Service/Generator/DocBlock/AsControllerGenerator.php +++ b/src/Service/Generator/DocBlock/AsControllerGenerator.php @@ -3,11 +3,12 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock; +use Lorisleiva\Actions\Concerns\AsController; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; class AsControllerGenerator extends DocBlockGeneratorBase implements DocBlockGeneratorInterface { - protected string $context = ActionInfo::AS_CONTROLLER_NAME; + protected string $context = AsController::class; /** * @inheritDoc */ diff --git a/src/Service/Generator/DocBlock/AsJobGenerator.php b/src/Service/Generator/DocBlock/AsJobGenerator.php index bc15ef1..e4b8c57 100644 --- a/src/Service/Generator/DocBlock/AsJobGenerator.php +++ b/src/Service/Generator/DocBlock/AsJobGenerator.php @@ -3,33 +3,51 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock; -use phpDocumentor\Reflection\DocBlock\Tags\Method; -use phpDocumentor\Reflection\DocBlock\Tags\Param; +use Illuminate\Foundation\Bus\PendingDispatch; +use Illuminate\Support\Fluent; +use Lorisleiva\Actions\Concerns\AsJob; +use Lorisleiva\Actions\Decorators\JobDecorator; +use Lorisleiva\Actions\Decorators\UniqueJobDecorator; +use phpDocumentor\Reflection\Php\Argument; +use phpDocumentor\Reflection\Types\Boolean; +use Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\Custom\Method; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; -use Wulfheart\LaravelActionsIdeHelper\Service\ParameterInfo; class AsJobGenerator extends DocBlockGeneratorBase implements DocBlockGeneratorInterface { - protected string $context = ActionInfo::AS_JOB_NAME; + protected string $context = AsJob::class; + /** * @inheritDoc */ public function generate(ActionInfo $info): array { - $params = array_map(function (ParameterInfo $parameterInfo) { - return $parameterInfo->getArgumentArray(); - }, $info->getFunctionInfosByContext($this->context)?->parameterInfos ?? []); + + $method = $this->findMethod($info, 'asJob', 'handle'); + + if ($method == null) { + return []; + } + + $args = $method->getArguments(); return [ - new Method('makeJob', $params, $this->resolveAsUnionType('\Lorisleiva\Actions\Decorators\JobDecorator', '\Lorisleiva\Actions\Decorators\UniqueJobDecorator'), true), - new Method('makeUniqueJob', $params, $this->resolveType('\Lorisleiva\Actions\Decorators\UniqueJobDecorator'), true), - new Method('dispatch', $params, $this->resolveType('\Illuminate\Foundation\Bus\PendingDispatch'), true), - new Method('dispatchIf', collect($params)->prepend(ParameterInfo::create()->setName('boolean')->setTypehint('bool')->getArgumentArray())->toArray(), $this->resolveAsUnionType('\Illuminate\Foundation\Bus\PendingDispatch', '\Illuminate\Support\Fluent'), true), - new Method('dispatchUnless', collect($params)->prepend(ParameterInfo::create()->setName('boolean')->setTypehint('bool')->getArgumentArray())->toArray(), $this->resolveAsUnionType('\Illuminate\Foundation\Bus\PendingDispatch', '\Illuminate\Support\Fluent'), true), - new Method('dispatchSync', $params, null, true), - new Method('dispatchNow', $params, null, true), - new Method('dispatchAfterResponse', $params, null, true), + new Method('makeJob', $args, $this->resolveAsUnionType(JobDecorator::class, UniqueJobDecorator::class), + true), + new Method('makeUniqueJob', $args, $this->resolveType(UniqueJobDecorator::class), true), + new Method('dispatch', $args, $this->resolveType(PendingDispatch::class), true), + new Method('dispatchIf', + collect($args)->prepend(new Argument('boolean', new Boolean()))->toArray(), + $this->resolveAsUnionType(PendingDispatch::class, Fluent::class), + true), + new Method('dispatchUnless', + collect($args)->prepend(new Argument('boolean', new Boolean()))->toArray(), + $this->resolveAsUnionType(PendingDispatch::class, Fluent::class), + true), + new Method('dispatchSync', $args, null, true), + new Method('dispatchNow', $args, null, true), + new Method('dispatchAfterResponse', $args, null, true), ]; } diff --git a/src/Service/Generator/DocBlock/AsListenerGenerator.php b/src/Service/Generator/DocBlock/AsListenerGenerator.php index 3ea068b..a98f4af 100644 --- a/src/Service/Generator/DocBlock/AsListenerGenerator.php +++ b/src/Service/Generator/DocBlock/AsListenerGenerator.php @@ -3,11 +3,12 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock; +use Lorisleiva\Actions\Concerns\AsListener; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; class AsListenerGenerator extends DocBlockGeneratorBase implements DocBlockGeneratorInterface { - protected string $context = ActionInfo::AS_LISTENER_NAME; + protected string $context = AsListener::class; /** * @inheritDoc */ diff --git a/src/Service/Generator/DocBlock/AsObjectGenerator.php b/src/Service/Generator/DocBlock/AsObjectGenerator.php index 86b0146..d78c0bf 100644 --- a/src/Service/Generator/DocBlock/AsObjectGenerator.php +++ b/src/Service/Generator/DocBlock/AsObjectGenerator.php @@ -3,15 +3,16 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock; +use Lorisleiva\Actions\Concerns\AsObject; use phpDocumentor\Reflection\DocBlock\Tags\Method; use phpDocumentor\Reflection\DocBlock\Tags\Param; +use phpDocumentor\Reflection\Php\Argument; use phpDocumentor\Reflection\TypeResolver; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; -use Wulfheart\LaravelActionsIdeHelper\Service\ParameterInfo; class AsObjectGenerator extends DocBlockGeneratorBase implements DocBlockGeneratorInterface { - protected string $context = ActionInfo::AS_OBJECT_NAME; + protected string $context = AsObject::class; /** * @param \Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo $info @@ -19,11 +20,8 @@ class AsObjectGenerator extends DocBlockGeneratorBase implements DocBlockGenerat */ public function generate(ActionInfo $info): array { - $functionInfo = $info->getFunctionInfosByContext($this->context); - $params = array_map(function (ParameterInfo $parameterInfo) { - return $parameterInfo->getArgumentArray(); - }, $functionInfo?->parameterInfos ?? []); - - return [new Method('run', $params, $functionInfo?->returnType, true)]; + /** @var Method $method */ + $method = $this->findMethod($info, 'handle'); + return $method == null ? [] : [new \Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\Custom\Method('run', $method->getArguments(), $method->getReturnType(), true)]; } } diff --git a/src/Service/Generator/DocBlock/Custom/Method.php b/src/Service/Generator/DocBlock/Custom/Method.php new file mode 100644 index 0000000..8d50e98 --- /dev/null +++ b/src/Service/Generator/DocBlock/Custom/Method.php @@ -0,0 +1,82 @@ + $arguments + * @param \phpDocumentor\Reflection\Type|null $returnType + * @param bool $static + * @param \phpDocumentor\Reflection\DocBlock\Description|null $description + */ + public function __construct( + protected string $methodName, + protected array $arguments = [], + protected ?Type $returnType = null, + protected bool $static = false, + protected ?Description $description = null + ) { + + } + + public static function create(string $body) + { + // TODO: Implement create() method. + } + + public function __toString(): string + { + $s = ''; + if($this->static){ + $s .= 'static '; + } + + if($this->returnType){ + $s .= (string) $this->returnType . ' '; + } + + + $s .= $this->methodName . '('; + + $s .= collect($this->arguments)->map(fn(Argument $arg) => $this->stringifyArgument($arg))->implode(', '); + + $s .= ')'; + + return $s; + } + + protected function stringifyArgument(Argument $argument): string + { + $s = ""; + $type = $argument->getType(); + if ($type) { + $s .= (string) $type." "; + } + + if ($argument->isVariadic()) { + $s .= "..."; + } + + if ($argument->isByReference()) { + $s .= "&"; + } + + $s .= '$'.$argument->getName(); + + $default = $argument->getDefault(); + if ($default) { + $s .= ' = ' . $default; + } + + return $s; + } +} \ No newline at end of file diff --git a/src/Service/Generator/DocBlock/DocBlockGeneratorBase.php b/src/Service/Generator/DocBlock/DocBlockGeneratorBase.php index d11563c..f0e24d1 100644 --- a/src/Service/Generator/DocBlock/DocBlockGeneratorBase.php +++ b/src/Service/Generator/DocBlock/DocBlockGeneratorBase.php @@ -3,14 +3,14 @@ namespace Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock; -use JetBrains\PhpStorm\Pure; +use phpDocumentor\Reflection\Php\Argument; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\TypeResolver; use Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo; class DocBlockGeneratorBase implements DocBlockGeneratorInterface { - #[Pure] public static function create(): static + public static function create(): static { return new static(); } @@ -29,4 +29,27 @@ public function generate(ActionInfo $info): array { return []; } + + /** + * Needed because otherwise a docblock method is not able to get parsed + * @param array $arguments + * @phpstan-return array> + */ + protected function convertArguments(array $arguments): array { + return collect($arguments) + ->transform(fn(Argument $arg) => ['name' => $arg->getName(),'type' => $arg->getType()]) + ->toArray(); + } + + protected function findMethod(ActionInfo $info, string ...$methods): ?\phpDocumentor\Reflection\Php\Method { + foreach ($methods as $method){ + $m = collect($info->classInfo->getMethods()) + ->filter(fn(\phpDocumentor\Reflection\Php\Method $m) => $m->getName() == $method) + ->first(); + if(!empty($m)){ + return $m; + } + } + return null; + } } \ No newline at end of file diff --git a/src/Service/ParameterInfo.php b/src/Service/ParameterInfo.php deleted file mode 100644 index 5bbc3c8..0000000 --- a/src/Service/ParameterInfo.php +++ /dev/null @@ -1,93 +0,0 @@ -name = $name; - - return $this; - } - - public function setTypehint(string $typehint): ParameterInfo - { - $this->typehint = $typehint; - - return $this; - } - - public function setNullable(bool $nullable): ParameterInfo - { - $this->nullable = $nullable; - - return $this; - } - - public function setDefault(string $default): ParameterInfo - { - $this->default = $default; - - return $this; - } - - public function setVariadic(bool $variadic): ParameterInfo - { - $this->variadic = $variadic; - - return $this; - } - - public function setPosition(int $position): ParameterInfo - { - $this->position = $position; - - return $this; - } - - public function isOptional(): bool - { - return isset($this->default) && $this->default !== ''; - } - - public function getParameter(): Param - { - $type = (new TypeResolver())->resolve($this->typehint); - // TODO: Support default parameters - // For now I decided to not include them. It should (!) work to include them - // in the name like "name = 'default'" - return new Param($this->name, $type, $this->variadic); - } - - public function getArgumentArray(): array { - $type = null; - if(!is_null($this->typehint)){ - - - $type = (new TypeResolver())->resolve($this->typehint); - - } - // TODO: Support default parameters - // For now I decided to not include them. It should (!) work to include them - // in the name like "name = 'default'" - return['name' => $this->name, 'type' => $type]; - } -} diff --git a/tests/ActionInfoFactoryTest.php b/tests/ActionInfoFactoryTest.php new file mode 100644 index 0000000..ad708ed --- /dev/null +++ b/tests/ActionInfoFactoryTest.php @@ -0,0 +1,40 @@ +loadFromPath(__DIR__ . '/stubs'); + + expect($result)->toBeArray()->toMatchArray([ + BaseAction::class => [AsObject::class], + NewAction::class => [AsObject::class, AsJob::class], + TestAction::class => ActionInfo::ALL_TRAITS, + ]); + + expect(collect($result)->keys()->toArray())->not()->toContain(NotAnAction::class); +}); + +it('creates correct ActionInfos', function (){ + $ai = getActionInfo(BaseAction::class); + + expect($ai->asObject)->toBeTrue(); + expect($ai->asCommand)->toBeFalse(); + + expect($ai->classInfo instanceof Class_)->toBeTrue(); + expect($ai->classInfo)->not()->toBeNull(); +}); + +it('parses the classes correctly', function() { + $result = invade(new ActionInfoFactory())->loadPhpDocumentorReflectionClassMap(__DIR__ . '/stubs'); + + $keys = collect($result)->keys()->toArray(); + expect($keys)->toContain(NotAnAction::class, BaseAction::class, NotAnAction::class, TestAction::class); +}); \ No newline at end of file diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 750dd52..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,12 +0,0 @@ -assertTrue(true); - } -} diff --git a/tests/Generators/BaseGeneratorTest.php b/tests/Generators/BaseGeneratorTest.php new file mode 100644 index 0000000..a333470 --- /dev/null +++ b/tests/Generators/BaseGeneratorTest.php @@ -0,0 +1,28 @@ +findMethod($ai, 'handle'); + expect($method)->toBeNull(); + + +}); + +it('can find a method in the correct precedence', function () { + $ai = $ai = getActionInfo(WithDecoratorAction::class); + /** @var \phpDocumentor\Reflection\Php\Method $method */ + $method = invade(new DocBlockGeneratorBase())->findMethod($ai, 'asJob', 'handle'); + expect($method->getName())->toBe('asJob'); +}); + +it('can find a method in the correct precedence even when one is not present', function () { + $ai = $ai = getActionInfo(WithoutDecoratorAction::class); + /** @var \phpDocumentor\Reflection\Php\Method $method */ + $method = invade(new DocBlockGeneratorBase())->findMethod($ai, 'asJob', 'handle'); + expect($method->getName())->toBe('handle'); +}); \ No newline at end of file diff --git a/tests/Generators/JobGeneratorTest.php b/tests/Generators/JobGeneratorTest.php new file mode 100644 index 0000000..a397dbb --- /dev/null +++ b/tests/Generators/JobGeneratorTest.php @@ -0,0 +1,34 @@ +generate($ai)); + + expect($docblock->count())->toEqual(8); + + $all = $docblock->map(fn($item) => $item->render())->implode(PHP_EOL); + foreach ($expectations as $expectation) { + expect($all)->toContain($expectation); + } +})->with([ + // Just one Method as an example + 'with decorator' => [ + WithDecoratorAction::class, [ + '@method static \\'.JobDecorator::class.'|\\'.UniqueJobDecorator::class.' makeJob(int $i)', + ], + ], + 'without decorator' => [ + WithoutDecoratorAction::class, [ + '@method static \\'.JobDecorator::class.'|\\'.UniqueJobDecorator::class.' makeJob()', + ], + ], + +]); \ No newline at end of file diff --git a/tests/Generators/ObjectGeneratorTest.php b/tests/Generators/ObjectGeneratorTest.php new file mode 100644 index 0000000..8996446 --- /dev/null +++ b/tests/Generators/ObjectGeneratorTest.php @@ -0,0 +1,37 @@ +generate($ai))->first(); + expect($docblock)->toBeInstanceOf(\Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\Custom\Method::class); + + expect($docblock->render())->toEqual($docblockExpectation); + +})->with([ + "BaseAction" => [BaseAction::class, '@method static string run()'], + "UnionTypeAction" => [UnionTypeAction::class, '@method static int|string run(string $string, float|int $number)'], + "VoidAction" => [VoidAction::class, '@method static void run(int $i)'], + "VoidActionWithNoReturnType" => [VoidActionWithNoReturnType::class, '@method static mixed run()'], +]); + +it('can render the run method with default parameter values', function(){ + $ai = getActionInfo(DefaultParameterValuesAction::class); + + /** @var \phpDocumentor\Reflection\DocBlock\Tag $docblock */ + $docblock = collect((new AsObjectGenerator())->generate($ai))->first(); + expect($docblock)->toBeInstanceOf(\Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\Custom\Method::class); + + $docblockExpectation = '@method static int run(string $s, bool $var = false)'; + expect($docblock->render())->toEqual($docblockExpectation); + +}); \ No newline at end of file diff --git a/tests/IdeHelperOutputTest.php b/tests/IdeHelperOutputTest.php new file mode 100644 index 0000000..7dc5596 --- /dev/null +++ b/tests/IdeHelperOutputTest.php @@ -0,0 +1,21 @@ +build($actionInfos); + + assertMatchesSnapshot($result); +}); + +it('can render intersection types ', function() { + $actionInfos = ActionInfoFactory::create(__DIR__ . '/stubs/IntersectionTypes'); + + $result = BuildIdeHelper::create()->build($actionInfos); + + assertMatchesSnapshot($result); +}); \ No newline at end of file diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..d4f11bb --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,12 @@ +in(__DIR__); + +/** @param class-string $class */ +function getActionInfo(string $class): ActionInfo { + $actionInfos = collect(ActionInfoFactory::create(__DIR__ . '/stubs')); + return $actionInfos->filter(fn(ActionInfo $ai) => $ai->fqsen == $class)->firstOrFail(); +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index eaaeaa5..4c3e578 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,21 +2,11 @@ namespace Wulfheart\LaravelActionsIdeHelper\Tests; -use Illuminate\Database\Eloquent\Factories\Factory; use Orchestra\Testbench\TestCase as Orchestra; use Wulfheart\LaravelActionsIdeHelper\LaravelActionsIdeHelperServiceProvider; class TestCase extends Orchestra { - public function setUp(): void - { - parent::setUp(); - - Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'Wulfheart\\LaravelActionsIdeHelper\\Database\\Factories\\'.class_basename($modelName).'Factory' - ); - } - protected function getPackageProviders($app) { return [ @@ -24,13 +14,4 @@ protected function getPackageProviders($app) ]; } - public function getEnvironmentSetUp($app) - { - config()->set('database.default', 'testing'); - - /* - include_once __DIR__.'/../database/migrations/create_laravel-actions-ide-helper_table.php.stub'; - (new \CreatePackageTable())->up(); - */ - } } diff --git a/tests/__snapshots__/IdeHelperOutputTest__it_can_render_correctly_an_action_with_AsObject__1.txt b/tests/__snapshots__/IdeHelperOutputTest__it_can_render_correctly_an_action_with_AsObject__1.txt new file mode 100644 index 0000000..08c0233 --- /dev/null +++ b/tests/__snapshots__/IdeHelperOutputTest__it_can_render_correctly_an_action_with_AsObject__1.txt @@ -0,0 +1,146 @@ +