Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **View names and component tags resolve through one project-wide index.** Laravel addresses a template by a dotted view name and a component by a tag name, and both are transforms of a file path rather than anything written in the code. PHPantom now builds that index once, over the view roots `config/view.php` configures, the view directories packages register, and the namespaces class-based and Livewire components live in, including the ones a service provider registers with `Blade::componentNamespace(…)` and a custom `livewire.class_namespace`. View name completion and go-to-definition read the index instead of walking the project per request, and it is rebuilt as you add or move files. One thing it finds that name-guessing could not: an index component, addressed by its directory alone (`<x-card>` backed by `App\View\Components\Card\Card`, `<livewire:posts>` by `App\Livewire\Posts\Index`), now supplies its template's variables like any other component class.
- **A custom `Storage::extend()` driver no longer costs the rest of the project its disk type.** `Storage::disk()` and friends declare only the `Filesystem` contract, and PHPantom resolves them to what the disks in `config/filesystems.php` are really built from. Until now a single disk on a driver the framework does not ship was enough to give that up for every disk in the project, because the driver's type was unknowable. PHPantom now reads the `Storage::extend('name', function (…) { … })` registration in your service providers, so the disk it backs resolves to whatever that closure builds. The documented registration shape returns a `FilesystemAdapter`, in which case the custom disk resolves to the same concrete adapter as the built-in ones; a driver that builds something else widens the disk type to include it instead of dropping the correction. A registration named after a built-in driver replaces it, the way the container does.
- **A component's own class supplies the variables its view reads.** Blade merges a class component's public properties and its public argument-less methods into the data the view renders with, and Livewire hands its view the component instance, so a component template that reads one of those had no way to know what it was and reported it undefined. PHPantom now resolves the class behind a component view the way Laravel does, over the class-component namespaces a service provider registers (`Blade::componentNamespace(…)`), the `App\View\Components` convention, and the configured Livewire class namespace, and puts its members in the template's scope. They sit below the template's own `@bladestan-signature`, `@props`, and `@aware` declarations and above the types inferred from call sites, so a template that documents a name keeps its own type for it. Members the framework base class declares stay out, as does any method that requires an argument, matching what Blade actually exposes.
- **String container bindings resolve to their bound class.** A service provider's `$this->app->singleton('sentry', fn () => new HubAdapter())`, `bind('key', Concrete::class)`, `instance('key', new Concrete())`, and `alias(Concrete::class, 'key')` are now indexed, so `app()->make('sentry')`, `app('sentry')`, and `resolve('sentry')` resolve to the bound class the same way a `::class` argument does. The key does not have to be written as a literal: a package that keeps it in a class constant or a static property on the base provider its subclass extends (`$this->app->alias(HubInterface::class, static::$abstract)`) is read the same way. `extend()` calls are skipped, since they decorate whatever the key already holds rather than replacing it. When more than one provider binds the same key, the key resolves to the class the container would end up holding: an application's registration replaces a framework or package default, and a provider that subclasses another replaces the binding it inherited, so swapping an implementation out (rebinding `'translator'` from your own `TranslationServiceProvider`, say) resolves to the replacement rather than the class it replaced.
- **String container bindings resolve to their bound class.** A service provider's `$this->app->singleton('sentry', fn () => new HubAdapter())`, `bind('key', Concrete::class)`, `instance('key', new Concrete())`, and `alias(Concrete::class, 'key')` are now indexed, so `app()->make('sentry')`, `app('sentry')`, and `resolve('sentry')` resolve to the bound class the same way a `::class` argument does. The key does not have to be written as a literal: a package that keeps it in a class constant or a static property on the base provider its subclass extends (`$this->app->alias(HubInterface::class, static::$abstract)`) is read the same way. `extend()` calls are skipped, since they decorate whatever the key already holds rather than replacing it. When more than one provider binds the same key, the key resolves to the class the container would end up holding: an application's registration replaces a framework or package default, and a provider that subclasses another replaces the binding it inherited, so swapping an implementation out (rebinding `'translator'` from your own `TranslationServiceProvider`, say) resolves to the replacement rather than the class it replaced. A provider that lists its registrations in the `$bindings` / `$singletons` arrays Laravel reads off it, instead of writing them out in `register()`, is read the same way, and applied where the framework applies them, so an array entry beats what `register()` bound the same key to. A factory that declares a return type is taken at its word, so `singleton('gateway', fn (): Gateway => new StripeGateway())` resolves the key to the contract its author named rather than the implementation behind it; only an undeclared factory is read from what its body builds, which also lets a factory resolve when its body builds something PHPantom cannot follow. The key itself is now a place you can navigate from: hover one and PHPantom names the class it resolves to and the provider that registered it, Ctrl+Click jumps to the registration, and find-references collects every other call that asks for it. An entry keyed by a contract still leaves `app(Gateway::class)` as the contract, which is what the application declared it wants, and no unknown-key diagnostic fires, since anything at all can be bound at runtime. Contributed by @shuvroroy (#335).
- **A Blade template's variables come from one declared priority chain.** What a template gets in scope is now resolved the way Bladestan (the PHPStan extension for Blade) resolves it, so one set of annotations drives both the editor and CI. A `@bladestan-signature` docblock is the template's contract; without the marker the first docblock before any template code serves as one. Below that, `@props` and `@aware` supply the names the contract leaves out, each typed from its default value, and a component view also receives the variables Blade injects into it, now including `$componentName` alongside `$attributes` and `$slot`. Types inferred from `view()` call sites remain the last resort. Each source only fills in what the ones above it did not declare. A directive Blade itself ignores no longer declares anything either: a `@props` written inside a comment, a `@verbatim` block, or a PHP string literal is inert, so it neither declares props nor marks the template as a component.
- **Blade templates infer their variables from `view()` call sites.** A template with no `@var` declarations of its own now gets its variable types from the places that render it: `view('name', ['user' => $user])` array literals, `compact()` arguments, and `->with('key', $value)` chains, including `View::make()`. Variables passed at several call sites union their types, and completion, hover, go-to-definition, and undefined-variable diagnostics inside the template all see the inferred set. Declared `@var` annotations still take precedence: a template that documents its own contract is left untouched. Closes #296.
- **Analyze verbosity flags.** `phpantom_lsp analyze` now supports PHPStan-style `--debug` and `-v`/`-vv`/`-vvv` flags. `--debug` prints each file as it is analyzed and disables the progress bar, so a hang or slowdown is immediately attributable to a specific file; warnings about unusually slow files also moved under this flag. `-v` adds per-file durations and a phase timing summary, `-vv` adds worker ids and parse-phase tracing, and `-vvv` adds memory usage.
Expand Down
3 changes: 2 additions & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ unlikely to move the needle for most users.
| T5 | Fiber type resolution | Low | Medium |
| T10 | [Ternary expression as RHS of list destructuring](todo/type-inference.md#t10-ternary-expression-as-rhs-of-list-destructuring) | Low | Medium |
| T11 | [Nested list destructuring](todo/type-inference.md#t11-nested-list-destructuring) | Low | Medium |
| | **[Bugs](todo/bugs.md)** | | |
| B1 | [Editing a service provider does not re-scan what it registers](todo/bugs.md#b1-editing-a-service-provider-does-not-re-scan-what-it-registers) | Medium | Medium |
| | **[Diagnostics](todo/diagnostics.md)** | | |
| D6 | [Unreachable code diagnostic](todo/diagnostics.md#d6-unreachable-code-diagnostic) | Low-Medium | Medium |
| D16 | [`unreachable_match_arm` ignores literal subject types](todo/diagnostics.md#d16-unreachable_match_arm-ignores-literal-subject-types) | Low-Medium | Medium |
Expand Down Expand Up @@ -174,7 +176,6 @@ unlikely to move the needle for most users.
| L8 | `withSum`/`withAvg`/`withMin`/`withMax` aggregate properties | Low-Medium | High |
| L45 | [`*_count` properties are offered on every relationship](todo/laravel.md#l45-_count-properties-are-offered-on-every-relationship) | Low-Medium | High |
| L29 | [Livewire and Volt component names](todo/laravel.md#l29-livewire-and-volt-component-names) (Livewire projects only) | Low | Low |
| L36 | [Container binding registrations from service providers](todo/laravel.md#l36-container-binding-registrations-from-service-providers) | Low | Low |
| L27 | [Legacy `Controller@method` action strings](todo/laravel.md#l27-legacy-controllermethod-action-strings) | Low | Low |
| L10 | `View::withX()` / `RedirectResponse::withX()` dynamic methods | Low | Medium |
| L39 | [Unused view and translation key detection](todo/laravel.md#l39-unused-view-and-translation-key-detection) | Low | Medium |
Expand Down
27 changes: 26 additions & 1 deletion docs/todo/bugs.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,29 @@ No outstanding items.

## Miscellaneous

No outstanding items.
#### B1. Editing a service provider does not re-scan what it registers

**Impact: Medium · Complexity: Medium**

`build_provider_resources` runs once, at `initialized`, and in the
`analyze` CLI. Nothing re-runs it when a provider file changes, so
everything the scan recovers goes stale for the rest of the session:
a container binding written now needs a restart before `app('key')`
resolves, hovers, or navigates, and the same applies to the view
directories, translation directories, route files, config files, and
component namespaces a provider registers.

Every other provider-derived table already has its per-file
counterpart (`refresh_laravel_gates`, `refresh_laravel_morph_map`,
`refresh_laravel_command_index`, `refresh_laravel_macros`,
`refresh_laravel_storage_drivers`); provider resources are the one
table without one. A `refresh_laravel_provider_resources(uri,
content)` on the same didChange/didSave path closes it.

Re-scanning a single file is not enough on its own: the binding table
is merged across every provider and its precedence depends on which
provider outranks which, so the refresh has to rebuild the merged set
rather than patch one file's entries into it. It must also reset
`laravel_aliases` and clear the class-not-found cache, the way
`build_provider_resources` already does, or the keys it just learned
stay unresolvable.
33 changes: 1 addition & 32 deletions docs/todo/laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ within the same impact tier.

| Item | Reason |
|------|--------|
| Container bindings registered dynamically or conditionally | Binding names or targets computed at runtime (variables, environment switches, loops) cannot be recovered statically. Literal `bind('name', Target::class)`-style registrations in app and package service providers **are** in scope — see L36. Names in the framework's own `registerCoreContainerAliases()` and the app's alias config already resolve via parsing. |
| Container bindings registered dynamically or conditionally | Binding names or targets computed at runtime (variables, environment switches, loops) cannot be recovered statically. Literal `bind('name', Target::class)`-style registrations in app and package service providers are recovered, as are the `$bindings` / `$singletons` arrays. Names in the framework's own `registerCoreContainerAliases()` and the app's alias config already resolve via parsing. |
| Facade `getFacadeAccessor()` with string aliases | Requires booting the application. `@method static` tags provide a workable fallback. |
| Blade templates | Separate project. See `blade.md` for the implementation plan. |
| Model column types from a live database connection | Requires a reachable, migrated database plus credentials, and answers "true for that database" rather than "true for this code". Committed schema artifacts (migration files, `database/schema/*.sql` dumps) are now parsed statically (schema dump + migration scanning). |
Expand Down Expand Up @@ -553,11 +553,6 @@ name), `Lang::has()`, and the typed config accessors (`Config::string()`,
`LaravelStringKey` spans instead of the current definition-only
ad-hoc fallback, so references (and completion/diagnostics)
work uniformly.
- **Container alias strings** — `app('cache')` already resolves to the
concrete class for member completion via the alias tables; wire
go-to-definition and hover on the string itself to that resolved
class. No new data needed. Provider-registered binding names stay out
of scope (see the table at the top).

#### L24. Translation depth: JSON lang files, locales, placeholders

Expand Down Expand Up @@ -692,32 +687,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, diagnostics, and references.

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

**Impact: Low · Complexity: Low**

`$this->app->bind('payments', StripeGateway::class)`, `bindIf()`,
`singleton()`, `singletonIf()`, `scoped()`, `scopedIf()`, `instance()`,
and `alias()` calls in `register()`/`boot()` of app and package service
providers are already scanned for literal `(string name, target)` pairs
and merged into the alias table, so `app('payments')->charge()`
resolves members, and the string gets go-to-definition (the
registration site) and hover. Binding precedence follows what the
container would actually end up holding: an application's registration
replaces a framework default, and a subclass provider's replaces its
parent's.

The remaining gap is the declarative form: a provider's `$bindings` /
`$singletons` array properties (`protected array $bindings =
['payments' => StripeGateway::class];`) are not read at all, so a
package that registers that way instead of calling
`bind()`/`singleton()` in `register()` stays invisible.

Interface targets follow the declared-types philosophy unchanged: a
binding `bind(Gateway::class, StripeGateway::class)` does **not**
retype `app(Gateway::class)` to the concrete — the contract is the
interface.

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

**Impact: Low · Complexity: Medium**
Expand Down
13 changes: 13 additions & 0 deletions examples/laravel/app/Demo.php
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,19 @@ public function containerAliases(): void
// replacement rather than the default it swapped out.
app('pastry.oven')->bake('croissant'); // → BakeryService
app('pastry.oven.supplier')->supply(12); // → CroissantSupplier

// A provider may list its registrations in the `$bindings` /
// `$singletons` arrays Laravel reads off it instead of writing them
// out in register(), and a factory whose body builds something
// PHPantom cannot follow still declares what it hands back. All
// three are in DemoServiceProvider.
app('pastry.counter')->counted('croissant'); // → int
app('pastry.plain-oven')->bake('rye'); // → string
app('pastry.tally')->tally()->counted('bun'); // → int

// Hover any of these keys to see the class it resolves to and the
// provider that registered it, and go-to-definition to jump to the
// registration itself.
}


Expand Down
26 changes: 26 additions & 0 deletions examples/laravel/app/Providers/DemoServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
use App\Support\CarbonMixin;
use App\Support\CollectionMixin;
use App\Support\CroissantSupplier;
use App\Support\PastryCounter;
use App\Support\PlainOven;
use App\View\Composers\SidebarComposer;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Collection;
Expand All @@ -25,6 +28,21 @@

class DemoServiceProvider extends BaseDemoServiceProvider
{
/**
* Laravel reads these two arrays off the provider itself and applies them
* once register() has run, so a key listed here binds exactly as a
* `bind()` / `singleton()` call would. Hover either key where it is
* resolved and PHPantom reports the class; go-to-definition jumps back to
* the entry below.
*/
public array $bindings = [
'pastry.counter' => PastryCounter::class,
];

public array $singletons = [
'pastry.plain-oven' => PlainOven::class,
];

public function register(): void
{
// A container key is not always written as a literal. This one lives
Expand All @@ -42,6 +60,14 @@ public function register(): void
// `alias()` takes its arguments the other way round from `bind()`:
// the second one is the new name, the first is what it stands for.
$this->app->alias(CroissantSupplier::class, static::$abstract . '.supplier');

// A factory that hands back whatever something else builds says
// nothing PHPantom can follow, but it does declare what comes out.
// The declared return type is the author's own statement of what the
// key holds, so that is what it resolves to.
$this->app->singleton('pastry.tally', function (Application $app): PastryCounter {
return $app->make('pastry.counter')->tally();
});
}

public function boot(): void
Expand Down
16 changes: 16 additions & 0 deletions examples/laravel/app/Support/PastryCounter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Support;

class PastryCounter
{
public function counted(string $item): int
{
return strlen($item);
}

public function tally(): static
{
return $this;
}
}
Loading
Loading