Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
79ae5ff
feat(laravel): add Artisan command signature parser and command index
shuvroroy Jul 23, 2026
660920e
test(laravel): cover signature parser, command scanner, and index
shuvroroy Jul 23, 2026
2adfade
feat(laravel): register commands module and re-export command index API
shuvroroy Jul 23, 2026
e25ac9e
feat(laravel): add laravel_commands index field to Backend
shuvroroy Jul 23, 2026
c37ad41
feat(laravel): build Artisan command index on initialize
shuvroroy Jul 23, 2026
7032c05
feat(laravel): refresh command index on file edits
shuvroroy Jul 23, 2026
6cf7b96
feat(symbols): add Command string-kind and CommandOwnParam span
shuvroroy Jul 23, 2026
09b011d
feat(symbols): recognize Artisan::call/queue, Schedule::command, and …
shuvroroy Jul 23, 2026
7914b86
feat(completion): complete Artisan command names
shuvroroy Jul 23, 2026
aae1cab
feat(definition): resolve command names to their declaring class
shuvroroy Jul 23, 2026
bb787db
feat(completion): complete Artisan command names
shuvroroy Jul 23, 2026
a5c29de
feat(hover): show command signature and own-parameter details
shuvroroy Jul 23, 2026
9302a59
feat(diagnostics): flag unknown command names and own parameters
shuvroroy Jul 23, 2026
a8137e9
feat(completion): complete command own args/options and Artisan::call…
shuvroroy Jul 23, 2026
7de1230
test(completion): cover command-parameter completion detection
shuvroroy Jul 23, 2026
8986890
chore(completion): register command_params module
shuvroroy Jul 23, 2026
6899a3f
feat(completion): wire command-parameter completion into the handler
shuvroroy Jul 23, 2026
21c642c
chore(definition): handle CommandOwnParam in type-definition match
shuvroroy Jul 23, 2026
b7b1a4a
chore(highlight): handle CommandOwnParam span
shuvroroy Jul 23, 2026
1490e9e
chore(references): handle CommandOwnParam span
shuvroroy Jul 23, 2026
9182d77
chore(rename): skip rename on CommandOwnParam span
shuvroroy Jul 23, 2026
176384f
chore(semantic-tokens): skip CommandOwnParam span
shuvroroy Jul 23, 2026
0da9be4
test(laravel): integration tests for command name/param completion, d…
shuvroroy Jul 23, 2026
15c9b74
test: register laravel_commands integration module
shuvroroy Jul 23, 2026
93af897
docs(example): document Artisan command name/signature demo
shuvroroy Jul 23, 2026
77aae64
chore(example): demo Artisan command name and signature features
shuvroroy Jul 23, 2026
2c687b4
chore: remove from laravel todo list
shuvroroy Jul 23, 2026
a28b5c8
chore: last but not least fixed lint issue
shuvroroy Jul 23, 2026
ce97bf4
chore: update CHANGELOG with PR number
shuvroroy Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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.
- **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.
- **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.
- **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).
- **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).
- **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).
- **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).
Expand Down
20 changes: 0 additions & 20 deletions docs/todo/laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -819,26 +819,6 @@ Each family gets the full string-kind treatment for free once wired
as a `LaravelStringKey`: completion, go-to-definition (jump to the
config entry), hover (L16), diagnostics (L14), and references.

#### L33. Artisan command and signature strings

**Impact: Low-Medium · Effort: Medium**

Two related string surfaces, both statically recoverable:

- **Command names.** `Artisan::call('app:sync')`, `Artisan::queue()`,
`$this->call()`/`callSilently()` in commands, and `Schedule::command()`
name a command declared by a `Command` subclass's `$signature` (or
`$name`) property, or an `#[AsCommand]` attribute. Scan project and
vendor command classes for literal signatures; complete, navigate to
the class, and flag unknown names.
- **Own arguments and options.** Inside a command class,
`$this->argument('user')` / `$this->option('queue')` name segments of
that same class's `$signature`. Parse the signature grammar
(`{user}`, `{user?}`, `{--queue=}`, arrays, defaults, descriptions)
once and complete/validate against it; hover shows the segment's
description. The parsed parameter list also enables array-key
completion for `Artisan::call('app:sync', [...])` second arguments.

#### L36. Container binding registrations from service providers

**Impact: Medium · Effort: Medium**
Expand Down
1 change: 1 addition & 0 deletions examples/laravel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ features against a real Laravel installation.
- **Route navigation.** Go-to-definition for `route('home')` (resolves to `->name('home')` in route files).
- **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.
- **Translation navigation.** Go-to-definition for `__('messages.welcome')`, `trans('auth.failed')`, and `trans_choice(...)` (resolves to `lang/` PHP files).
- **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/`.
- **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.

## Getting started
Expand Down
31 changes: 31 additions & 0 deletions examples/laravel/app/Console/Commands/GenerateReportCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;

/**
* Artisan command whose name comes from an `#[AsCommand]` attribute.
*
* PHPantom recovers the command name (`reports:generate`) from the attribute
* — the third supported declaration surface alongside `$signature` and
* `$name`. The `$signature` here still contributes the `--format` option for
* own-parameter and array-key completion.
*/
#[AsCommand(name: 'reports:generate')]
class GenerateReportCommand extends Command
{
protected $signature = 'reports:generate {--format=json : Output format (json|csv)}';

protected $description = 'Generate a bakery report';

public function handle(): int
{
$format = $this->option('format');

$this->info("Generating report as {$format}");

return self::SUCCESS;
}
}
52 changes: 52 additions & 0 deletions examples/laravel/app/Console/Commands/SyncBakeryCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

/**
* Artisan command declared through a `$signature` string.
*
* PHPantom parses the signature grammar so that:
* - the command name (`bakery:sync`) completes / navigates / validates
* wherever it is referenced as a string (see `Demo::artisanCommands()`);
* - `$this->argument(...)` / `$this->option(...)` below complete and hover
* against this same signature, and unknown names are flagged.
*/
class SyncBakeryCommand extends Command
{
/**
* The signature encodes one argument and two options, each with an
* inline `:` description that PHPantom surfaces on hover.
*/
protected $signature = 'bakery:sync
{bakery : The bakery ID to synchronise}
{--fresh : Only synchronise freshly baked loaves}
{--since= : Only loaves baked since this date}';

protected $description = 'Synchronise a bakery and its loaves';

public function handle(): int
{
// Own-parameter completion + hover: trigger completion inside the
// string and only `bakery` is offered; hover shows its description.
$bakery = $this->argument('bakery');

// Options complete to `fresh` and `since`; hover shows "takes a
// value" for `--since`.
$onlyFresh = $this->option('fresh');
$since = $this->option('since');

// Referencing a name that is NOT in the signature above is flagged
// as `invalid_command_parameter` (uncomment to see the diagnostic):
// $this->argument('kitchen');

// `$this->call('...')` inside a command runs another Artisan command,
// so the command name completes / navigates / validates too.
$this->call('reports:generate', ['--format' => 'json']);

$this->info("Synced bakery {$bakery} (fresh={$onlyFresh}, since={$since})");

return self::SUCCESS;
}
}
40 changes: 40 additions & 0 deletions examples/laravel/app/Demo.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
use Illuminate\Http\Request;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;

Expand Down Expand Up @@ -291,6 +293,44 @@ public function laravelNavigation(): void
}


// ── Artisan Command Names & Signatures ─────────────────────────────────

/**
* Command names declared by a command class's `$signature` / `$name` /
* `#[AsCommand]` are recoverable statically, so referencing them as a
* string completes, navigates, and validates.
*
* Try:
* 1. Trigger completion inside `Artisan::call('|')` — offers `bakery:sync`
* and `reports:generate` from app/Console/Commands.
* 2. Ctrl+Click "bakery:sync" to jump to SyncBakeryCommand.
* 3. Hover "bakery:sync" to see its arguments and options.
* 4. "does:not-exist" below is flagged as an unknown command.
* 5. Trigger completion inside the parameter array of the last call —
* offers `bakery` and `--fresh` / `--since` from the target signature.
*/
public function artisanCommands(): void
{
// Command-name string completion / navigation / hover.
Artisan::call('bakery:sync');
Artisan::queue('reports:generate');

// Scheduled commands name the same declarations.
Schedule::command('bakery:sync')->daily();

// Unknown command name → `invalid_laravel_command` diagnostic.
Artisan::call('does:not-exist');

// Second-argument array-key completion resolves against the target
// command's parsed signature: arguments by name, options as `--name`.
Artisan::call('bakery:sync', [
'bakery' => 1,
'--fresh' => true,
'--since' => '2024-01-01',
]);
}


// ── PHPDoc Virtual Member References & Rename ───────────────────────────
// Try: right-click "displayName" or "bio" below and use
// • Find All References — includes the @property/@method declaration
Expand Down
Loading
Loading