Skip to content

Commit adf3751

Browse files
authored
L33. Artisan command and signature strings (#274)
1 parent 5826d7f commit adf3751

33 files changed

Lines changed: 2321 additions & 27 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- **Laravel string key completion (route, config, view, trans).** Typing inside the first string argument of `route()`, `to_route()`, `config()`, `Config::get()`, `view()`, `View::make()`, `__()`, `trans()`, `Lang::get()`, and related helpers now offers autocompletion from the project's actual route names, config keys, view templates, and translation keys. Route names are collected from `routes/*.php` (including group prefixes and `Route::group([], __DIR__ . '/sub.php')` file includes), `Route::resource()` and `Route::apiResource()` generate conventional named routes (`index`, `create`, `store`, `show`, `edit`, `update`, `destroy`) respecting `->only()` and `->except()` modifiers, config keys from `config/*.php` array declarations, view names from `resources/views/` file paths, and translation keys from `lang/` files. Go-to-definition on route names also follows file includes and resolves resource routes. Laravel container attributes (`#[Config('key')]`, `#[Database('conn')]`, `#[Cache('store')]`, `#[Log('channel')]`, `#[Storage('disk')]`, `#[Auth('guard')]`) offer completion from the relevant config sub-keys (e.g. `#[Database('')]` shows database connection names from `config/database.php`). Facade methods like `Auth::guard()`, `DB::connection()`, `Cache::store()`, `Log::channel()`, `Storage::disk()`, and the `auth()` helper also complete from their respective config sub-keys. Contributed by @calebdw.
2424
- **Hover for Laravel string keys.** Hovering over a route name, config key, view name, or translation key string now shows the key kind, the key value, and where it's defined (e.g. `routes/web/ems.php`, `config/app.php`). Contributed by @calebdw.
2525
- **Diagnostics for invalid Laravel string keys.** A typo in `route('dashbaord')`, `config('app.naem')`, `view('layouts.ap')`, or `__('auth.failedd')` now produces a warning: `Unknown route: 'dashbaord'`. Only plain string literals are flagged; dynamic keys with variables are ignored. Contributed by @calebdw.
26+
- **Artisan command names and signature strings.** Command names declared by a command class's `$signature`, `$name`, or `#[AsCommand]` attribute are now recovered from project and vendor command classes, so referencing one as a string completes, navigates to the declaring class, hovers with its arguments and options, and flags unknown names. This covers `Artisan::call()`, `Artisan::queue()`, `Schedule::command()`, and `$this->call()` / `$this->callSilently()` inside a command. The `$signature` grammar (`{user}`, `{user?}`, `{--queue=}`, arrays, defaults, shortcuts, and ` : ` descriptions) is parsed so that, inside a command, `$this->argument('user')` and `$this->option('queue')` complete and hover against that command's own signature and unknown parameter names are flagged, and the parameter array of `Artisan::call('cmd', [...])` completes the target command's argument and `--option` keys. Contributed by @shuvroroy (#274).
2627
- **Laravel model factory relationship methods.** Eloquent factories now offer the dynamic `has{Relationship}()` and `for{Relationship}()` methods that Laravel resolves through `Factory::__call()`, one per relationship on the associated model, plus `trashed()` when the model uses `SoftDeletes`. They complete, hover, and resolve, and because each returns the factory the fluent chain continues, so `Post::factory()->hasComments(3)->create()` still resolves to the model. The model is derived from the factory naming convention, so no `@extends Factory<Model>` generic is required. Contributed by @shuvroroy (#260).
2728
- **Eloquent `$pivot` attribute on many-to-many related models.** Models that are the target of a `belongsToMany`/`morphToMany` relationship now expose a `$pivot` property, so accessing the intermediate row (e.g. `$user->roles->first()->pivot`) completes, hovers, and resolves. The pivot type is taken from the relationship's `TPivotModel` generic (`BelongsToMany<Permission, $this, PermissionRole>`), falling back to a `->using(CustomPivot::class)` call in the relationship body and then to the base `\Illuminate\Database\Eloquent\Relations\Pivot`; a declared `pivot` property still takes precedence. Only models actually reached through such a relationship gain the attribute, and the `->withPivot('col', …)` columns and custom pivot class are also shown when hovering the relationship. Contributed by @shuvroroy (#266).
2829
- **Laravel `Macroable::mixin()` registrations are recognized.** A `Str::mixin(new StrMixin())` or `Collection::mixin(CollectionMixin::class)` call in a service provider now contributes one macro per public/protected method of the mixin class, taking the signature of the closure that method returns, so those methods autocomplete, hover, resolve, and type-check on the target class just like a `Target::macro(...)` registration. Mixins registered through a facade also attach to the facade's concrete container-bound class, and go-to-definition on a mixed-in method jumps to the mixin method's own declaration. Editing the mixin class (for example adding a helper method) refreshes the recognized macros. Contributed by @shuvroroy (#256).

docs/todo/laravel.md

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -819,26 +819,6 @@ Each family gets the full string-kind treatment for free once wired
819819
as a `LaravelStringKey`: completion, go-to-definition (jump to the
820820
config entry), hover (L16), diagnostics (L14), and references.
821821

822-
#### L33. Artisan command and signature strings
823-
824-
**Impact: Low-Medium · Effort: Medium**
825-
826-
Two related string surfaces, both statically recoverable:
827-
828-
- **Command names.** `Artisan::call('app:sync')`, `Artisan::queue()`,
829-
`$this->call()`/`callSilently()` in commands, and `Schedule::command()`
830-
name a command declared by a `Command` subclass's `$signature` (or
831-
`$name`) property, or an `#[AsCommand]` attribute. Scan project and
832-
vendor command classes for literal signatures; complete, navigate to
833-
the class, and flag unknown names.
834-
- **Own arguments and options.** Inside a command class,
835-
`$this->argument('user')` / `$this->option('queue')` name segments of
836-
that same class's `$signature`. Parse the signature grammar
837-
(`{user}`, `{user?}`, `{--queue=}`, arrays, defaults, descriptions)
838-
once and complete/validate against it; hover shows the segment's
839-
description. The parsed parameter list also enables array-key
840-
completion for `Artisan::call('app:sync', [...])` second arguments.
841-
842822
#### L36. Container binding registrations from service providers
843823

844824
**Impact: Medium · Effort: Medium**

examples/laravel/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ features against a real Laravel installation.
1212
- **Route navigation.** Go-to-definition for `route('home')` (resolves to `->name('home')` in route files).
1313
- **Controller action navigation.** Go-to-definition, hover, rename, references, and completion for route action strings in `[Controller::class, 'method']` callables and `Route::controller(...)->group(...)` routes.
1414
- **Translation navigation.** Go-to-definition for `__('messages.welcome')`, `trans('auth.failed')`, and `trans_choice(...)` (resolves to `lang/` PHP files).
15+
- **Artisan command names & signatures.** Completion, go-to-definition, hover, and unknown-name diagnostics for command names in `Artisan::call(...)`, `Artisan::queue(...)`, `Schedule::command(...)`, and `$this->call(...)` (resolves to the command class declared by `$signature` / `$name` / `#[AsCommand]`). Inside a command, `$this->argument(...)` / `$this->option(...)` complete, hover, and validate against that command's own signature, and the parameter array of `Artisan::call('cmd', [...])` completes its argument and `--option` keys. See `Demo::artisanCommands()` and `app/Console/Commands/`.
1516
- **Blade template intelligence.** Variable completion and hover in `{{ }}` expressions (shown as `e()` calls), go-to-definition on `@include`/`@extends` view references, `@forelse`/`@empty` directives, implicit `$loop` variable in `@foreach`/`@forelse`, implicit `$message` in `@error`, implicit `$value` in `@session`, `@verbatim` block handling, and standalone `@var` docblocks for type narrowing.
1617

1718
## Getting started
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Symfony\Component\Console\Attribute\AsCommand;
7+
8+
/**
9+
* Artisan command whose name comes from an `#[AsCommand]` attribute.
10+
*
11+
* PHPantom recovers the command name (`reports:generate`) from the attribute
12+
* — the third supported declaration surface alongside `$signature` and
13+
* `$name`. The `$signature` here still contributes the `--format` option for
14+
* own-parameter and array-key completion.
15+
*/
16+
#[AsCommand(name: 'reports:generate')]
17+
class GenerateReportCommand extends Command
18+
{
19+
protected $signature = 'reports:generate {--format=json : Output format (json|csv)}';
20+
21+
protected $description = 'Generate a bakery report';
22+
23+
public function handle(): int
24+
{
25+
$format = $this->option('format');
26+
27+
$this->info("Generating report as {$format}");
28+
29+
return self::SUCCESS;
30+
}
31+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use Illuminate\Console\Command;
6+
7+
/**
8+
* Artisan command declared through a `$signature` string.
9+
*
10+
* PHPantom parses the signature grammar so that:
11+
* - the command name (`bakery:sync`) completes / navigates / validates
12+
* wherever it is referenced as a string (see `Demo::artisanCommands()`);
13+
* - `$this->argument(...)` / `$this->option(...)` below complete and hover
14+
* against this same signature, and unknown names are flagged.
15+
*/
16+
class SyncBakeryCommand extends Command
17+
{
18+
/**
19+
* The signature encodes one argument and two options, each with an
20+
* inline `:` description that PHPantom surfaces on hover.
21+
*/
22+
protected $signature = 'bakery:sync
23+
{bakery : The bakery ID to synchronise}
24+
{--fresh : Only synchronise freshly baked loaves}
25+
{--since= : Only loaves baked since this date}';
26+
27+
protected $description = 'Synchronise a bakery and its loaves';
28+
29+
public function handle(): int
30+
{
31+
// Own-parameter completion + hover: trigger completion inside the
32+
// string and only `bakery` is offered; hover shows its description.
33+
$bakery = $this->argument('bakery');
34+
35+
// Options complete to `fresh` and `since`; hover shows "takes a
36+
// value" for `--since`.
37+
$onlyFresh = $this->option('fresh');
38+
$since = $this->option('since');
39+
40+
// Referencing a name that is NOT in the signature above is flagged
41+
// as `invalid_command_parameter` (uncomment to see the diagnostic):
42+
// $this->argument('kitchen');
43+
44+
// `$this->call('...')` inside a command runs another Artisan command,
45+
// so the command name completes / navigates / validates too.
46+
$this->call('reports:generate', ['--format' => 'json']);
47+
48+
$this->info("Synced bakery {$bakery} (fresh={$onlyFresh}, since={$since})");
49+
50+
return self::SUCCESS;
51+
}
52+
}

examples/laravel/app/Demo.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
use Illuminate\Http\Request;
1616
use Carbon\CarbonImmutable;
1717
use Illuminate\Support\Collection;
18+
use Illuminate\Support\Facades\Artisan;
1819
use Illuminate\Support\Facades\Auth;
1920
use Illuminate\Support\Facades\Cache;
2021
use Illuminate\Support\Facades\Config;
2122
use Illuminate\Support\Facades\Lang;
2223
use Illuminate\Support\Facades\Redis;
24+
use Illuminate\Support\Facades\Schedule;
2325
use Illuminate\Support\Facades\Storage;
2426
use Illuminate\Support\Facades\View;
2527

@@ -291,6 +293,44 @@ public function laravelNavigation(): void
291293
}
292294

293295

296+
// ── Artisan Command Names & Signatures ─────────────────────────────────
297+
298+
/**
299+
* Command names declared by a command class's `$signature` / `$name` /
300+
* `#[AsCommand]` are recoverable statically, so referencing them as a
301+
* string completes, navigates, and validates.
302+
*
303+
* Try:
304+
* 1. Trigger completion inside `Artisan::call('|')` — offers `bakery:sync`
305+
* and `reports:generate` from app/Console/Commands.
306+
* 2. Ctrl+Click "bakery:sync" to jump to SyncBakeryCommand.
307+
* 3. Hover "bakery:sync" to see its arguments and options.
308+
* 4. "does:not-exist" below is flagged as an unknown command.
309+
* 5. Trigger completion inside the parameter array of the last call —
310+
* offers `bakery` and `--fresh` / `--since` from the target signature.
311+
*/
312+
public function artisanCommands(): void
313+
{
314+
// Command-name string completion / navigation / hover.
315+
Artisan::call('bakery:sync');
316+
Artisan::queue('reports:generate');
317+
318+
// Scheduled commands name the same declarations.
319+
Schedule::command('bakery:sync')->daily();
320+
321+
// Unknown command name → `invalid_laravel_command` diagnostic.
322+
Artisan::call('does:not-exist');
323+
324+
// Second-argument array-key completion resolves against the target
325+
// command's parsed signature: arguments by name, options as `--name`.
326+
Artisan::call('bakery:sync', [
327+
'bakery' => 1,
328+
'--fresh' => true,
329+
'--since' => '2024-01-01',
330+
]);
331+
}
332+
333+
294334
// ── PHPDoc Virtual Member References & Rename ───────────────────────────
295335
// Try: right-click "displayName" or "bio" below and use
296336
// • Find All References — includes the @property/@method declaration

0 commit comments

Comments
 (0)