Skip to content
Open
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ The symbol map also stores:
- **Scope boundaries** (`scopes`): function, method, closure, and arrow function body ranges. Used by `find_enclosing_scope` to determine which scope the cursor is in.
- **Template parameter definitions** (`template_defs`): `@template` tag locations so that template parameter names (e.g. `TKey`, `TModel`) that appear in docblock types can be resolved to their declaration site.
- **Candidate render sites** (`view_receiver_sites`): the view names a method call spells when only the receiver's *type* decides whether it renders — a constructor-injected `Factory $views` behind `$this->views->make('page')`, a mailable held in a local. Extraction runs before the file's classes are resolved and cannot type the receiver, so it records the candidates and `blade/typed_receiver.rs` confirms them lazily through the shared type engine, once per file. Consumers of view keys (the call-site diagnostics, call-site inference, `lookup_symbol_map`, find-references) read the confirmed spans alongside the map's own `LaravelStringKey` spans. The reference candidate index takes the *unconfirmed* candidates, since a file has to be findable before it can be asked.
- **Config-backed Laravel resource names**: one declarative table maps direct helpers, facades, contextual attributes, and middleware parameters to their config subtrees. A `SymbolSpan` whose Laravel string kind is `ConfigResource(...)` stores the short source name, while completion, navigation, diagnostics, and references derive the full dot key only at the config boundary. This keeps source ranges and reference identity exact without duplicating one trigger table across LSP features.

### Tier 2: Stored Byte Offsets (cross-file jumps)

Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **More configured Laravel service names are editor-aware.** Beyond storage disks, names passed through framework facades, contextual attributes, and route authentication middleware now complete from the matching config subtree; hover identifies the resource family, Ctrl+Click opens the exact declaration, references connect direct config access, and misspellings are reported with family-specific diagnostics. Channel arrays passed to `Log::stack()` are understood value by value. Contributed by @shuvroroy.
- **Storage disk names are navigable wherever Laravel accepts one.** `Storage::disk()`, `fake()`, `persistentFake()`, `forgetDisk()`, and the `#[Storage]` container attribute now complete from `config/filesystems.php`; hover shows the config key, Ctrl+Click opens its declaration, and find-references links every use. Calls that require a configured disk report misspellings, while test fakes and disk eviction keep accepting the ad-hoc names Laravel permits at runtime. Contributed by @shuvroroy.
- **Class and namespace moves from the command line.** `phpantom_lsp move FROM TO` moves one class or a whole namespace and updates declarations, imports, references, and PSR-4 paths across the project. Both sides can be fully-qualified names or Composer PSR-4 file/directory paths, and `--dry-run --format json` provides a validation-only form for scripts and coding agents. A destination that would overwrite an existing class or file is refused before any changes are made. A move into a namespace no PSR-4 mapping covers is called out rather than reported as a plain success, since the files cannot follow the declarations there and the autoloader stops finding them. A class installed by Composer is refused outright, the same way renaming one in the editor is. Contributed by @calebdw.
- **Qualified names can be converted to imports in one action.** Invoke the refactoring on an absolute or relative qualified class, function, or constant to add the matching `use`, `use function`, or `use const` declaration and shorten every equivalent usage in the file. When the natural short name is already imported from elsewhere, the new import receives a namespace-derived alias instead. A companion action on the same cursor position does the whole namespace at once, importing every qualified class, function, and constant it contains and aliasing the ones whose short names collide. Contributed by @calebdw.
Expand Down
5 changes: 4 additions & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,11 @@ unlikely to move the needle for most users.
| L24 | [Translation depth: JSON lang files, locales, placeholders](todo/laravel.md#l24-translation-depth-json-lang-files-locales-placeholders) | Medium-High | Medium-High |
| L46 | [`->can()` on a user model the receiver does not name](todo/laravel.md#l46-can-on-a-user-model-the-receiver-does-not-name) | Medium-High | Medium-High |
| L30 | [Eloquent attribute-array key completion](todo/laravel.md#l30-eloquent-attribute-array-key-completion) | Medium | Medium |
| L56 | [Typed Laravel connection names](todo/laravel.md#l56-typed-laravel-connection-names) | Medium | Medium |
| L58 | [Laravel rate limiter names](todo/laravel.md#l58-laravel-rate-limiter-names) | Medium | Medium |
| L57 | [Laravel queue names](todo/laravel.md#l57-laravel-queue-names) | Low-Medium | Medium |
| L55 | [Typed controller middleware names](todo/laravel.md#l55-typed-controller-middleware-names) | Low-Medium | Medium |
| L53 | [Collection key types from the column for `keyBy` / `groupBy` / `pluck`](todo/laravel.md#l53-collection-key-types-from-the-column-for-keyby-groupby-pluck) | Medium | Medium |
| L32 | [Config-backed named-resource strings](todo/laravel.md#l32-config-backed-named-resource-strings) (log channels, cache stores, guards, connections, rate limiters) | Medium | Medium |
| L49 | [Unguarded Eloquent mass assignment diagnostic](todo/laravel.md#l49-unguarded-eloquent-mass-assignment-diagnostic) | Medium | Medium |
| L17 | [Additional string contexts without booting](todo/laravel.md#l17-additional-string-contexts-without-booting) (middleware, assets, validation, Inertia) | Medium | Medium-High |
| L54 | [Audit custom-builder and relation-closure inference against the PHPStan extensions](todo/laravel.md#l54-audit-custom-builder-and-relation-closure-inference-against-the-phpstan-extensions) | Medium | Medium-High |
Expand Down
76 changes: 41 additions & 35 deletions docs/todo/laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ accessors, `@property` tags) — no database needed.

**Impact: Low-Medium · Complexity: Medium**

References and go-to-definition already work for the four indexed
References and go-to-definition already work for indexed Laravel
string kinds, but the rename, document-highlight, and semantic-token
arms are explicit no-ops. Wiring them up exceeds the Laravel LSP (which
has none of the three): renaming a translation key updates the lang
Expand All @@ -672,43 +672,49 @@ updates the `->name()` declaration and all usages; highlight and
semantic tokens reuse the existing spans. Renaming a view name implies
moving the Blade file — defer that one until the rest is in place.

#### L32. Config-backed named-resource strings
#### L56. Typed Laravel connection names

**Impact: Medium · Complexity: Medium**

Storage disks are one instance of a general pattern: a method argument
names an entry under a known config subtree, and the config scanner
already parses those files. Auth guards (`auth('...')`,
`Auth::guard()`, `->middleware('auth:web')`), cache stores
(`Cache::store()`), log channels (`Log::channel()`), and storage disks
(`Storage::disk()`, test fakes, disk eviction, and `#[Storage]`) already
complete against their config subtree — but all of them route through
the generic `LaravelStringKind::Config` kind rather than a dedicated
one, so they get completion plus the shared config
diagnostics/go-to-definition and nothing family-specific (a "cache
store" hovers with the same generic wording as any other config key).
`Log::stack()` (array values) isn't recognized at all. Generalize into
a declarative table of `(trigger context, config path)` pairs so each
new family is one table row, and cover the rest of the family in one
pass:

- **Database connections** — `DB::connection()`, `->connection()` /
`$connection` on models and jobs → `database.connections.*`.
- **Queue connections and queues** — `Queue::connection()`,
`->onConnection()` → `queue.connections.*`; `->onQueue()` names are
free-form (completion from literals seen elsewhere, no diagnostic).
- **Mailers** — `Mail::mailer()` → `mail.mailers.*`.
- **Broadcast connections** — `Broadcast::connection()` →
`broadcasting.connections.*`.
- **Rate limiter names** — not config-backed: registered via
`RateLimiter::for('name', …)` in providers. Scan literal
registrations (same shape as the macro scanner) and validate
`throttle:name` middleware parameters and `new RateLimited('name')`
against the set.

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, diagnostics, and references.
The method name `->connection()` does not identify one config subtree: the
receiver may select `database.connections.*`, `queue.connections.*`, or
`broadcasting.connections.*`. Resolve it through the shared type engine, and
treat `->onConnection()` as a queue connection. A model's `$connection` is a
database connection, while the same property on a queueable job selects a
queue connection. Each confirmed literal should receive the same completion,
hover, navigation, diagnostics, and references as the direct facade spelling.

#### L55. Typed controller middleware names

**Impact: Low-Medium · Complexity: Medium**

`$this->middleware('auth:admin')` names middleware only when `$this` is a
Laravel controller; an unrelated class may define the same method for a
different purpose. Confirm the enclosing class through the shared type engine
before completing or validating embedded authentication guards. Static and
fluent `Route::middleware()` calls remain syntactically unambiguous.

#### L57. Laravel queue names

**Impact: Low-Medium · Complexity: Medium**

`->onQueue()` names are free-form rather than config-backed. Complete from
literals seen elsewhere in the project and connect those occurrences for
navigation and references, but do not diagnose a name merely because the
static index has not seen it.

#### L58. Laravel rate limiter names

**Impact: Medium · Complexity: Medium**

Rate limiter names are registered through `RateLimiter::for('name', …)` in
service providers. Scan literal registrations using the same provider-aware
shape as the macro scanner, then complete, navigate, and validate
`throttle:name` middleware parameters and `new RateLimited('name')` against
the discovered set. Numeric inline limits such as `throttle:60,1` remain
values rather than named registrations. Keep the world open when no
registration source can be read so a partial index does not create false
diagnostics.

#### L39. Unused view and translation key detection

Expand Down
64 changes: 57 additions & 7 deletions examples/laravel/app/Demo.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
use Database\Factories\AnnotatedPostFactory;
use Database\Factories\BlogAuthorFactory;
use Database\Factories\EditorialFactory;
use Illuminate\Container\Attributes\Auth as InjectAuth;
use Illuminate\Container\Attributes\Authenticated as InjectAuthenticated;
use Illuminate\Container\Attributes\Cache as InjectCache;
use Illuminate\Container\Attributes\Database as InjectDatabase;
use Illuminate\Container\Attributes\Log as InjectLog;
use Illuminate\Container\Attributes\Storage as InjectStorage;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Client\Factory as HttpFactory;
Expand All @@ -37,11 +43,16 @@
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Response;
Expand Down Expand Up @@ -887,6 +898,46 @@ public function laravelConfig(): void
}


// ── Config-backed Laravel resource names ───────────────────────────

public function injectedNamedResources(
#[InjectAuth(guard: 'admin')] mixed $guard,
#[InjectAuthenticated(guard: 'admin')] mixed $user,
#[InjectCache(store: 'memory')] mixed $cache,
#[InjectLog(channel: 'daily')] mixed $logger,
#[InjectStorage(disk: 'pantry')] mixed $disk,
#[InjectDatabase(connection: 'mysql')] mixed $database,
): void
{
// Contextual-attribute arguments complete and navigate against the
// same family-specific config entries as their facade counterparts.
}

public function namedLaravelResources(): void
{
// Hover identifies each resource family, Ctrl+Click opens its config
// entry, and references include direct config() access to that entry.
auth('admin');
Auth::guard('admin');
Cache::store('memory');
Log::channel('daily');
Log::stack(['daily', 'stderr']);
Storage::disk('pantry');
DB::connection('mysql');
DB::connection('mysql::read');
Queue::connection('redis');
Mail::mailer('transactional');
Broadcast::connection('internal');
Route::middleware(['auth:admin']);
config('cache.stores.memory');

// Laravel supplies these null drivers at runtime even though no
// matching child needs to exist in cache.php or queue.php.
Cache::store('null');
Queue::connection('null');
}


// ── Cache::remember() — closure return type binding ─────────────────

public function cacheRemember(): void
Expand Down Expand Up @@ -1157,21 +1208,20 @@ public function mixinModel(): void
// ── Storage::fake() resolves to the concrete adapter ────────────────

public function storageFake(
#[\Illuminate\Container\Attributes\Storage('avatars')] Filesystem $avatars,
#[\Illuminate\Container\Attributes\Storage(disk: 'avatars')] Filesystem $avatars,
): void
{
// fake() declares the Filesystem contract but always builds a
// FilesystemAdapter, so the adapter-only assertion helpers resolve.
// Disk names complete from config/filesystems.php, hover as their full
// config keys, and navigate back to their declarations — in the
// #[Storage] attribute above as much as in the calls below.
Storage::fake('avatars')->assertExists('me.png');
// Disk names complete from config/filesystems.php, hover with their
// resource family, and navigate back to their declarations.
Storage::fake(disk: 'avatars')->assertExists('me.png');
Storage::persistentFake(disk: 'logs')->assertMissing('old.log');

// forgetDisk() takes one name or a list of them, and tolerates a disk
// that was never configured, so an unknown name here is not flagged.
Storage::forgetDisk('avatars');
Storage::forgetDisk(['avatars', 'logs']);
Storage::forgetDisk(disk: ['avatars', 'logs']);
}


Expand All @@ -1183,7 +1233,7 @@ public function storageDisk(): void
// disk config/filesystems.php configures ('local', 's3', ...) builds
// a FilesystemAdapter, so adapter-only methods like download()
// resolve on every configured disk, not just a faked one.
Storage::disk('s3')->download('report.pdf');
Storage::disk(name: 's3')->download('report.pdf');
Storage::disk(name: 'local')->exists('notes.txt');
Storage::cloud()->assertExists('logo.png');

Expand Down
Loading
Loading