├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── actions-ide-helper.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_actions-ide-helper_table.php.stub ├── resources └── views │ └── .gitkeep ├── src ├── Commands │ └── LaravelActionsIdeHelperCommand.php ├── LaravelActionsIdeHelperServiceProvider.php └── Service │ ├── ActionInfo.php │ ├── ActionInfoFactory.php │ ├── BuildIdeHelper.php │ └── Generator │ └── DocBlock │ ├── AsCommandGenerator.php │ ├── AsControllerGenerator.php │ ├── AsJobGenerator.php │ ├── AsListenerGenerator.php │ ├── AsObjectGenerator.php │ ├── Custom │ └── Method.php │ ├── DocBlockGeneratorBase.php │ └── DocBlockGeneratorInterface.php └── vendor └── Expectation.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-actions-ide-helper` will be documented in this file. 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Wulfheart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Actions IDE Helper 2 | 3 | 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. 4 | 5 | ## YOLO-ware 6 | 7 | As I don't use Laravel Actions anymore I decided to go into `YOLO-mode` with this project. That means: 8 | 9 | 1. **No guarantees** 10 | 2. **Community-powered fixes**: Something doesn't work, you notice it you fix it. 11 | 3. **Trust in the community:** I won't test your changes, either they do work or they don't. I just merge them. 12 | 4. **Looking for a maintainer:** This situation is less than ideal. Therefore, I am looking for a new maintainer to take over this project. 13 | 14 | ## Installation 15 | 16 | ``` 17 | composer require --dev wulfheart/laravel-actions-ide-helper 18 | ``` 19 | 20 | ## Usage 21 | ``` 22 | php artisan ide-helper:actions 23 | ``` 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wulfheart/laravel-actions-ide-helper", 3 | "description": "Generate a new IDE Helper file for Laravel Actions.", 4 | "keywords": [ 5 | "Wulfheart", 6 | "laravel", 7 | "laravel-actions-ide-helper" 8 | ], 9 | "homepage": "https://github.com/wulfheart/laravel-actions-ide-helper", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Alexander Wulf", 14 | "email": "dev@alexfwulf.de", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1|^8.2", 20 | "illuminate/contracts": "^10.0|^11.0|^12.0", 21 | "lorisleiva/laravel-actions": "^2.3", 22 | "lorisleiva/lody": "^0.5.0|^0.6.0", 23 | "phpdocumentor/reflection": "^5.1|^6.0", 24 | "riimu/kit-pathjoin": "^1.2", 25 | "spatie/laravel-package-tools": "^1.14" 26 | }, 27 | "require-dev": { 28 | "brianium/paratest": "^6.8|^7.4", 29 | "nunomaduro/collision": "^6.1|^8.0", 30 | "orchestra/testbench": "^8.0|^9.0|^10.0", 31 | "pestphp/pest": "^1.22|^2.34|^3.7", 32 | "phpunit/phpunit": "^9.5.10|^10.5|^11.5.3", 33 | "spatie/invade": "^1.1|^2.0", 34 | "spatie/laravel-ray": "^1.32", 35 | "spatie/pest-plugin-snapshots": "^1.1|^2.1", 36 | "vimeo/psalm": "^5.6|^6.6" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Wulfheart\\LaravelActionsIdeHelper\\": "src", 41 | "Wulfheart\\LaravelActionsIdeHelper\\Database\\Factories\\": "database/factories" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Wulfheart\\LaravelActionsIdeHelper\\Tests\\": "tests" 47 | } 48 | }, 49 | "scripts": [], 50 | "config": { 51 | "sort-packages": true, 52 | "allow-plugins": { 53 | "composer/package-versions-deprecated": true, 54 | "pestphp/pest-plugin": true 55 | } 56 | }, 57 | "extra": { 58 | "laravel": { 59 | "providers": [ 60 | "Wulfheart\\LaravelActionsIdeHelper\\LaravelActionsIdeHelperServiceProvider" 61 | ], 62 | "aliases": { 63 | "LaravelActionsIdeHelper": "Wulfheart\\LaravelActionsIdeHelper\\LaravelActionsIdeHelperFacade" 64 | } 65 | } 66 | }, 67 | "minimum-stability": "dev", 68 | "prefer-stable": true 69 | } 70 | -------------------------------------------------------------------------------- /config/actions-ide-helper.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wulfheart/laravel-actions-ide-helper/fe8965069225e6c9674c1763dd897189098526f2/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Commands/LaravelActionsIdeHelperCommand.php: -------------------------------------------------------------------------------- 1 | build($actionInfos); 35 | 36 | file_put_contents($outfile, $result); 37 | 38 | $this->comment('IDE Helpers generated for Laravel Actions at ' . Str::of($outfile)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/LaravelActionsIdeHelperServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-actions-ide-helper') 20 | // ->hasConfigFile() 21 | // ->hasViews() 22 | // ->hasMigration('create_laravel-actions-ide-helper_table') 23 | ->hasCommand(LaravelActionsIdeHelperCommand::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Service/ActionInfo.php: -------------------------------------------------------------------------------- 1 | fqsen = $name; 49 | $this->name = class_basename($name); 50 | $this->namespace = Str::of($name)->beforeLast('\\' . $this->name); 51 | 52 | return $this; 53 | } 54 | 55 | public function setAsObject(bool $asObject): ActionInfo 56 | { 57 | $this->asObject = $asObject; 58 | 59 | return $this; 60 | } 61 | 62 | public function setAsController(bool $asController): ActionInfo 63 | { 64 | $this->asController = $asController; 65 | 66 | return $this; 67 | } 68 | 69 | public function setAsJob(bool $asJob): ActionInfo 70 | { 71 | $this->asJob = $asJob; 72 | 73 | return $this; 74 | } 75 | 76 | public function setAsListener(bool $asListener): ActionInfo 77 | { 78 | $this->asListener = $asListener; 79 | 80 | return $this; 81 | } 82 | 83 | public function setAsCommand(bool $asCommand): ActionInfo 84 | { 85 | $this->asCommand = $asCommand; 86 | 87 | return $this; 88 | } 89 | 90 | public function setClassInfo(Class_ $classInfo): ActionInfo 91 | { 92 | $this->classInfo = $classInfo; 93 | return $this; 94 | } 95 | 96 | 97 | /** 98 | * @return \Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\DocBlockGeneratorInterface[] 99 | */ 100 | public function getGenerators(): array 101 | { 102 | return array_merge( 103 | ($this->asCommand ? [AsCommandGenerator::class] : []), 104 | ($this->asController ? [AsControllerGenerator::class] : []), 105 | ($this->asJob ? [AsJobGenerator::class] : []), 106 | ($this->asListener ? [AsListenerGenerator::class] : []), 107 | ($this->asObject ? [AsObjectGenerator::class] : []), 108 | ); 109 | } 110 | 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/Service/ActionInfoFactory.php: -------------------------------------------------------------------------------- 1 | */ 22 | public static function create(string $path): array 23 | { 24 | $factory = new self(); 25 | $classes = $factory->loadFromPath($path); 26 | $classMap = $factory->loadPhpDocumentorReflectionClassMap($path); 27 | $ais = []; 28 | foreach ($classes as $class => $traits){ 29 | $tc = collect($traits); 30 | $reflection = new \ReflectionClass($class); 31 | $ais[] = ActionInfo::create() 32 | ->setName($class) 33 | ->setAsObject($tc->contains(AsObject::class)) 34 | ->setAsCommand($tc->contains(AsCommand::class)) 35 | ->setAsController($tc->contains(AsController::class)) 36 | ->setAsJob($tc->contains(AsJob::class)) 37 | ->setAsListener($tc->contains(AsListener::class)) 38 | ->setClassInfo($classMap[$class]); 39 | } 40 | return $ais; 41 | 42 | 43 | } 44 | 45 | /** @return array> */ 46 | protected function loadFromPath(string $path) 47 | { 48 | $res = Lody::classes($path)->isNotAbstract(); 49 | /** @var array> $traits */ 50 | return collect(ActionInfo::ALL_TRAITS) 51 | ->map(fn($trait, $key) => [$trait => $res->hasTrait($trait)->all()]) 52 | ->collapse() 53 | ->map(function ($item, $key) { 54 | return collect($item) 55 | ->map(fn($i) => [ 56 | 'item' => $i, 57 | 'group' => $key, 58 | ]) 59 | ->toArray(); 60 | }) 61 | ->values() 62 | ->collapse() 63 | ->groupBy('item') 64 | ->map(fn($item) => $item->pluck('group')->toArray()) 65 | ->toArray(); 66 | } 67 | 68 | /** @return array<\phpDocumentor\Reflection\Php\Class_> 69 | * @throws \phpDocumentor\Reflection\Exception 70 | */ 71 | protected function loadPhpDocumentorReflectionClassMap(string $path): array{ 72 | $finder = Finder::create()->files()->in($path)->name('*.php'); 73 | $files = collect($finder)->map(fn(SplFileInfo $file) => new LocalFile($file->getRealPath()))->toArray(); 74 | 75 | /** @var \phpDocumentor\Reflection\Php\Project $project */ 76 | $project = ProjectFactory::createInstance()->create('Laravel Actions IDE Helper', $files); 77 | return collect($project->getFiles()) 78 | ->map(fn(File $f) => $f->getClasses()) 79 | ->collapse() 80 | ->mapWithKeys(fn($item, string $key) => [Str::of($key)->ltrim("\\")->toString() => $item]) 81 | ->toArray(); 82 | 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /src/Service/BuildIdeHelper.php: -------------------------------------------------------------------------------- 1 | groupBy(function (ActionInfo $item) { 29 | return $item->namespace; 30 | })->toArray(); 31 | 32 | $nodes = []; 33 | /** 34 | * @var string $namespace 35 | * @var \Wulfheart\LaravelActionsIdeHelper\Service\ActionInfo[] $items 36 | */ 37 | $factory = new BuilderFactory(); 38 | foreach ($groups as $namespace => $items) { 39 | $ns = $factory->namespace($namespace); 40 | foreach ($items as $item) { 41 | $ns->addStmt($factory->class($item->classInfo->getName())->setDocComment(new Doc($this->generateDocBlocks($item)))); 42 | } 43 | $nodes[] = $ns->getNode(); 44 | } 45 | $nodes[] = $this->getTraitIdeHelpers($factory); 46 | $printer = new Standard(); 47 | $data = $printer->prettyPrintFile($nodes); 48 | return $data; 49 | } 50 | 51 | protected function generateDocBlocks(ActionInfo $info): string 52 | { 53 | $tags = []; 54 | foreach ($info->getGenerators() as $generator) { 55 | $tags = array_merge($tags, $generator::create()->generate($info)); 56 | } 57 | 58 | 59 | return $this->serializeDocBlocks(...$tags); 60 | } 61 | 62 | protected function serializeDocBlocks(Tag ...$tags): string 63 | { 64 | $db = new DocBlock('', null, $tags); 65 | $serializer = new Serializer(); 66 | 67 | return $serializer->getDocComment($db); 68 | } 69 | 70 | protected function resolveType(string $type): Type 71 | { 72 | return (new TypeResolver())->resolve($type); 73 | } 74 | 75 | protected function resolveAsUnionType(string ...$types): Type 76 | { 77 | return (new TypeResolver())->resolve(implode('|', $types)); 78 | } 79 | 80 | protected function getTraitIdeHelpers(BuilderFactory $factory): \PhpParser\Node 81 | { 82 | return $factory->namespace("Lorisleiva\Actions\Concerns") 83 | ->addStmt( 84 | (new Trait_("AsController"))->setDocComment( 85 | $this->serializeDocBlocks( 86 | new DocBlock\Tags\Method('asController', [], $this->resolveType('void')) 87 | ) 88 | ) 89 | )->addStmt( 90 | (new Trait_("AsListener"))->setDocComment( 91 | $this->serializeDocBlocks( 92 | new DocBlock\Tags\Method('asListener', [], $this->resolveType('void')) 93 | ) 94 | ) 95 | )->addStmt( 96 | (new Trait_("AsJob"))->setDocComment( 97 | $this->serializeDocBlocks( 98 | new DocBlock\Tags\Method('asJob', [], $this->resolveType('void')) 99 | ) 100 | ) 101 | 102 | ) 103 | ->addStmt( 104 | (new Trait_("AsCommand"))->setDocComment( 105 | $this->serializeDocBlocks( 106 | new DocBlock\Tags\Method('asCommand', arguments: [ 107 | ['name' => 'command', 'type' => $this->resolveType("\Illuminate\Console\Command")], 108 | ], returnType: $this->resolveType('void')) 109 | ) 110 | ) 111 | ) 112 | ->getNode(); 113 | 114 | 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/Service/Generator/DocBlock/AsCommandGenerator.php: -------------------------------------------------------------------------------- 1 | findMethod($info, 'asJob', 'handle'); 27 | 28 | if ($method == null) { 29 | return []; 30 | } 31 | 32 | $args = $method->getArguments(); 33 | 34 | 35 | return [ 36 | new Method('makeJob', $args, $this->resolveAsUnionType(JobDecorator::class, UniqueJobDecorator::class), 37 | true), 38 | new Method('makeUniqueJob', $args, $this->resolveType(UniqueJobDecorator::class), true), 39 | new Method('dispatch', $args, $this->resolveType(PendingDispatch::class), true), 40 | new Method('dispatchIf', 41 | collect($args)->prepend(new Argument('boolean', new Boolean()))->toArray(), 42 | $this->resolveAsUnionType(PendingDispatch::class, Fluent::class), 43 | true), 44 | new Method('dispatchUnless', 45 | collect($args)->prepend(new Argument('boolean', new Boolean()))->toArray(), 46 | $this->resolveAsUnionType(PendingDispatch::class, Fluent::class), 47 | true), 48 | new Method('dispatchSync', $args, null, true), 49 | new Method('dispatchNow', $args, null, true), 50 | new Method('dispatchAfterResponse', $args, null, true), 51 | ]; 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Service/Generator/DocBlock/AsListenerGenerator.php: -------------------------------------------------------------------------------- 1 | findMethod($info, 'handle'); 25 | return $method == null ? [] : [new \Wulfheart\LaravelActionsIdeHelper\Service\Generator\DocBlock\Custom\Method('run', $method->getArguments(), $method->getReturnType(), true)]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Service/Generator/DocBlock/Custom/Method.php: -------------------------------------------------------------------------------- 1 | $arguments 17 | * @param \phpDocumentor\Reflection\Type|null $returnType 18 | * @param bool $static 19 | * @param \phpDocumentor\Reflection\DocBlock\Description|null $description 20 | */ 21 | public function __construct( 22 | protected string $methodName, 23 | protected array $arguments = [], 24 | protected ?Type $returnType = null, 25 | protected bool $static = false, 26 | protected ?Description $description = null 27 | ) { 28 | 29 | } 30 | 31 | public static function create(string $body) 32 | { 33 | // TODO: Implement create() method. 34 | } 35 | 36 | public function __toString(): string 37 | { 38 | $s = ''; 39 | if($this->static){ 40 | $s .= 'static '; 41 | } 42 | 43 | if($this->returnType){ 44 | $s .= (string) $this->returnType . ' '; 45 | } 46 | 47 | 48 | $s .= $this->methodName . '('; 49 | 50 | $s .= collect($this->arguments)->map(fn(Argument $arg) => $this->stringifyArgument($arg))->implode(', '); 51 | 52 | $s .= ')'; 53 | 54 | return $s; 55 | } 56 | 57 | protected function stringifyArgument(Argument $argument): string 58 | { 59 | $s = ""; 60 | $type = $argument->getType(); 61 | if ($type) { 62 | $s .= (string) $type." "; 63 | } 64 | 65 | if ($argument->isVariadic()) { 66 | $s .= "..."; 67 | } 68 | 69 | if ($argument->isByReference()) { 70 | $s .= "&"; 71 | } 72 | 73 | $s .= '$'.$argument->getName(); 74 | 75 | $default = $argument->getDefault(); 76 | if ($default) { 77 | $s .= ' = ' . $default; 78 | } 79 | 80 | return $s; 81 | } 82 | } -------------------------------------------------------------------------------- /src/Service/Generator/DocBlock/DocBlockGeneratorBase.php: -------------------------------------------------------------------------------- 1 | resolve($type); 21 | } 22 | 23 | protected function resolveAsUnionType(string ...$types): Type 24 | { 25 | return (new TypeResolver())->resolve(implode('|', $types)); 26 | } 27 | 28 | public function generate(ActionInfo $info): array 29 | { 30 | return []; 31 | } 32 | 33 | /** 34 | * Needed because otherwise a docblock method is not able to get parsed 35 | * @param array $arguments 36 | * @phpstan-return array> 37 | */ 38 | protected function convertArguments(array $arguments): array { 39 | return collect($arguments) 40 | ->transform(fn(Argument $arg) => ['name' => $arg->getName(),'type' => $arg->getType()]) 41 | ->toArray(); 42 | } 43 | 44 | protected function findMethod(ActionInfo $info, string ...$methods): ?\phpDocumentor\Reflection\Php\Method { 45 | foreach ($methods as $method){ 46 | $m = collect($info->classInfo->getMethods()) 47 | ->filter(fn(\phpDocumentor\Reflection\Php\Method $m) => $m->getName() == $method) 48 | ->first(); 49 | if(!empty($m)){ 50 | return $m; 51 | } 52 | } 53 | return null; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Service/Generator/DocBlock/DocBlockGeneratorInterface.php: -------------------------------------------------------------------------------- 1 |