/ :: / $, * Ctrl+Click for go-to-definition, or use "Go to Type Definition" to * jump to the class declaration of a variable's resolved type. * * Layout: * 1. DEMOS — open any demo() method and try completion inside it * 2. SCAFFOLDING — supporting definitions (scroll past these) * * Priority: * Basic everyday features → Trivial (works everywhere) → Advanced */ namespace Demo; use Attribute; use Closure; use Demo\ValidationException; use Demo\NotFoundException; use Exception; use Demo\UserProfile as Profile; // ═══════════════════════════════════════════════════════════════════════════ // DEMOS — open any demo() method and trigger completion inside // ═══════════════════════════════════════════════════════════════════════════ // ── Auto-Import (completion) ──────────────────────────────────────────────── // Try: type `new DateT` and accept `DateTime`. The `use DateTime;` statement // is inserted between `use Exception;` and `use Stringable;` above to // maintain alphabetical order. // // The `use Exception;` import above occupies the short name "Exception". // Try: type `throw new pq\Exception()` and accept — the auto-import inserts // `\pq\Exception` at the usage site instead of a conflicting `use` statement. // ── Namespace Segment Completion ──────────────────────────────────────────── // Try: erase the class name after `use Demo\` and trigger completion to see // namespace segments (module/folder icon) alongside class names. // ── Namespaced Function Completion ────────────────────────────────────────── // Try: type `use function parse_file` and accept to get // `use function ast\parse_file;` // ── Instance Completion ───────────────────────────────────────────────────── class InstanceCompletionDemo { public function demo(): void { $zoo = new Zoo(); $zoo->aardvark(); // own method $zoo->baboon; // own property $zoo->buffalo; // constructor-promoted property $zoo->cheetah; // readonly promoted (from base) $zoo->dingo(); // trait method $zoo->elephant('Hi'); // trait method $zoo->falcon(); // inherited from parent $zoo->gorilla; // @property (own class) $zoo->hyena('x'); // @method (own class) $zoo->iguana; // @property-read (interface) $zoo->jaguar(); // @method (interface) // MUST NOT appear: $keeper (protected), $ceo (private), nocturnal() (private) } } // ── Mixed Accessor Chaining ───────────────────────────────────────────────── class MixedAccessorDemo { public function demo(): void { $foobar = new StaticPropHolder(); $foobar->holder::$shared; // $obj->prop::$static chain // Inline (new Foo)->method() chaining (new Pen())->write(); // resolves Pen then write() } } // ── Method & Property Chaining ────────────────────────────────────────────── class ChainingDemo { public function demo(): void { $studio = new ScaffoldingChainingDemo(); // Fluent method chains — MUST NOT appear: calibrate() (protected) $studio->brush->setSize('large')->setStyle('pointed')->stroke(); // Return type chains $studio->brush->getCanvas()->title(); // Variable → method chain $canvas = $studio->brush->getCanvas(); $canvas->getBrush()->stroke(); // Deep property chain $studio->canvas->easel->material; $studio->canvas->easel->height(); // Null-safe chaining $maybe = Brush::find(1); $maybe?->getCanvas()?->title(); // Multi-line method chains $studio->brush->setSize('large') ->setStyle('pointed') ->stroke(); // Variable assigned from chain $directBrush = $studio->brush->getCanvas()->getBrush(); $directBrush->stroke(); // (new Class())->method() $fromNew = (new Canvas())->getBrush(); $fromNew->stroke(); // Intermediate variable from property access $easel = (new Canvas())->easel; $easel->material; } } // ── @var Docblock Override ────────────────────────────────────────────────── class VarDocblockDemo { public function demo(): void { /** @var Pencil $inlineHinted */ $inlineHinted = getUnknownValue(); $inlineHinted->sketch(); /** @var Pen */ $hinted = getUnknownValue(); $hinted->write(); // without variable name (PHPStorm fails this) } } // ── Return Type Resolution ────────────────────────────────────────────────── class ReturnTypeDemo { public function demo(): void { $made = Pen::make(); // static return type → Pen $made->write(); $marker = Marker::make(); // static on subclass → Marker $marker->highlight(); // resolves to Marker, not Pen $fluent = $marker->rename('Bold'); // rename returns static → Marker $fluent->highlight(); // chained static stays on the subclass $created = makePen(); $created->write(); // function return type // MUST NOT appear: refill() (private) $found = pickPenOrPencil(); // Pen|Pencil union $found->label(); // available on both types } } // ── Type Narrowing ────────────────────────────────────────────────────────── class TypeNarrowingDemo { public function demo(): void { $specimen = pickRockOrBanana(); // Rock|Banana if ($specimen instanceof Rock) { $specimen->crush(); // narrowed to Rock // MUST NOT appear: peel() (Banana only) } else { $specimen->peel(); // narrowed to Banana (else branch) // MUST NOT appear: crush() (Rock only) } if (!$specimen instanceof Rock) { $specimen->peel(); // negated instanceof } $unknown = getUnknownValue(); if (is_a($unknown, Rock::class)) { $unknown->crush(); // is_a() narrowing } $target = getUnknownValue(); assert($target instanceof Banana); $target->peel(); // assert() narrowing // Inline && narrowing — RHS of && sees the narrowed type from LHS $sample = pickRockOrBanana(); if ($sample instanceof Rock && $sample->crush()) { // $sample is Rock here too } } } // ── Type Guard Narrowing (is_array, is_object, …) ────────────────────────── class TypeGuardNarrowingDemo { /** * @param null|list|Pen $input */ public function demo(null|array|Pen $input): void { // is_array() narrows the union to the array-like PHPDoc member, // preserving the generic element type for foreach iteration. if (is_array($input)) { foreach ($input as $pen) { $pen->write(); // list → Pen } } // Else branch: non-array members survive if (is_array($input)) { // array branch } else { // $input is null|Pen here } // Guard clause: is_array() + early return if (is_array($input)) { return; } // $input is null|Pen after the guard // is_object() narrows to class members only $mixed = pickRockOrBanana(); // Rock|Banana if (is_object($mixed)) { $mixed->weigh(); // both Rock and Banana have weigh() } // is_object() narrows mixed → object, suppressing diagnostics // on dynamic property access (stdClass / object permit any property). $decoded = json_decode('{}'); // mixed if (is_object($decoded)) { echo $decoded->anything; // no diagnostic — object allows any property } // Compound && condition: is_object() narrowing propagates // through the entire condition and into the if-body. $payload = json_decode('{}'); // mixed if (is_object($payload) && property_exists($payload, 'name')) { echo $payload->name; // no diagnostic } } } // ── instanceof self/static/parent Narrowing ──────────────────────────────── class InstanceofSelfDemo extends ScaffoldingSedan { public function sport(): void {} public function demo(ScaffoldingMotor $m): void { // instanceof self — narrows to InstanceofSelfDemo assert($m instanceof self); $m->cruise(); // inherited from ScaffoldingSedan $m->sport(); // own method via self narrowing // instanceof static — narrows to InstanceofSelfDemo $x = getUnknownValue(); if ($x instanceof static) { $x->sport(); // narrowed to static (this class) } // instanceof parent — narrows to ScaffoldingSedan $y = getUnknownValue(); if ($y instanceof parent) { $y->cruise(); // narrowed to parent (ScaffoldingSedan) } } } // ── Custom Assert Narrowing ───────────────────────────────────────────────── class AssertNarrowingDemo { public function demo(): void { $unknown = getUnknownValue(); assertRock($unknown); // @phpstan-assert Rock $value $unknown->crush(); $sample = pickRockOrBanana(); if (isRock($sample)) { // @phpstan-assert-if-true Rock $sample->crush(); } else { $sample->peel(); } $maybe = pickRockOrBanana(); if (isNotRock($maybe)) { // @phpstan-assert-if-false Rock $maybe->peel(); } else { $maybe->crush(); } } } // ── Static Method Assert Narrowing ───────────────────────────────────────── class StaticAssertNarrowingDemo { public function demo(): void { // @phpstan-assert on static method — unconditional narrowing $unknown = getUnknownValue(); StaticAssert::assertRock($unknown); $unknown->crush(); // narrowed to Rock // @phpstan-assert-if-true on static method — narrows in then-branch $sample = pickRockOrBanana(); if (StaticAssert::isRock($sample)) { $sample->crush(); // narrowed to Rock } // @phpstan-assert-if-false on static method — narrows in else-branch $maybe = pickRockOrBanana(); if (StaticAssert::isNotRock($maybe)) { $maybe->peel(); // narrowed to Banana } else { $maybe->crush(); // narrowed to Rock } } } // ── Guard Clause Narrowing (Early Return / Throw) ────────────────────────── class GuardClauseDemo { public function demo(): void { $subject = pickRockOrBanana(); // Rock|Banana if (!$subject instanceof Banana) { return; // early return — guard clause } $subject->peel(); // narrowed to Banana after guard $candidate = pickRockOrBanana(); // Rock|Banana if ($candidate instanceof Rock) { throw new Exception('no rocks'); // early throw — guard clause } $candidate->peel(); // narrowed to Banana (Rock excluded) $unknown = getUnknownValue(); if (!$unknown instanceof Rock) return; // single-statement guard (no braces) $unknown->crush(); // narrowed to Rock } /** Positive instanceof + early return on a mixed parameter. */ public function mixedGuard(mixed $value): void { if ($value instanceof Banana) { return; // $value is Banana → exit } // After the guard, $value is NOT Banana. if ($value instanceof Rock) { $value->crush(); // narrowed to Rock (not Banana) } } } // ── in_array Strict-Mode Narrowing ───────────────────────────────────────── class InArrayNarrowingDemo { /** * @param Rock|Banana $item * @param list $rocks */ public function demo($item, array $rocks): void { if (in_array($item, $rocks, true)) { $item->crush(); // narrowed to Rock // MUST NOT appear: peel() (Banana only) } else { $item->peel(); // excluded Rock → Banana // MUST NOT appear: crush() (Rock only) } // Guard clause with in_array $specimen = pickRockOrBanana(); // Rock|Banana if (!in_array($specimen, $rocks, true)) { return; } $specimen->crush(); // narrowed to Rock after guard } } // ── Generics (@template / @extends) ──────────────────────────────────────── class GenericsDemo { public function demo(): void { $repo = new PenRepository(); $repo->find(1)->write(); // Repository::find() → Pen $repo->findOrNull(1)?->write(); // ?Pen $pens = new PenCollection(); // TypedCollection $pens->first()->write(); // MUST NOT appear: refill() (private on Pen) $pens->thickOnly(); // own method on subclass $cachingRepo = new CachingPenRepository(); $cachingRepo->find(1)->write(); // grandparent generics $responses = new ResponseCollection(); // @phpstan-extends variant $responses->first()->getStatusCode(); } } // ── @implements Generic Resolution ───────────────────────────────────────── class ImplementsGenericDemo { public function demo(): void { $repo = new PenStorage(); $repo->find(1)->write(); // @implements Storage → Pen $penCatalog = new PenCatalog(); $penCatalog->find(1)->write(); // @template-implements alias $items = new ItemIterableCollection(); foreach ($items as $item) { $item->write(); // @implements IteratorAggregate } } } // ── Built-in Generic Collections (ArrayIterator, SplFixedArray, etc.) ─────── class BuiltinGenericCollectionDemo { /** @return \ArrayIterator */ public function getPens(): \ArrayIterator { return new \ArrayIterator([new Pen()]); } public function demo(): void { $pen = $this->getPens()->current(); $pen->write(); // ArrayIterator → current() returns Pen // Direct chain also works: $this->getPens()->current()->write(); // same resolution through the chain } } // ── Inherited Docblock Types ──────────────────────────────────────────────── class InheritedDocblockDemo { public function demo(): void { // Interface declares @return list, implementor has only `: array`. // The richer type propagates automatically. $holder = new ScaffoldingConcreteHolder(); $holder->getPens()[0]->write(); // list inherited from interface // Parent class declares @return list, child overrides with `: array`. $child = new ScaffoldingChildHolder(); $child->getPens()[0]->write(); // list inherited from parent // When the child writes its own @return, it wins over the parent. $cat = new ScaffoldingCatStore(); $cat->getAnimals()[0]->label(); // list from child's own docblock // Parameter types propagate by position (child may rename params). $box = new ScaffoldingPenBox(); $box->accept([new Pen()]); // @param list inherited from interface // Grandparent @return flows through the entire chain. $deep = new ScaffoldingDeepChild(); $deep->getPens()[0]->write(); // list from grandparent } } // ── Conditional Return Types ──────────────────────────────────────────────── class ConditionalReturnDemo { public function demo(): void { $container = new Container(); $resolved = $container->make(Pen::class); $resolved->write(); // class-string → T $appPen = app(Pen::class); // conditional on standalone function $appPen->write(); // Literal string conditional return type $mapper = new TreeMapperImpl(); $result = $mapper->map('foo', 'bar'); $result->write(); // "foo" → Pen (literal string match) } } // ── Method-Level @template ────────────────────────────────────────────────── class MethodTemplateDemo { public function demo(): void { $locator = new ServiceLocator(); $locator->get(Pen::class)->write(); // class-string → T Factory::create(Pen::class)->write(); // static @template resolve(Marker::class)->highlight(); // function @template $mapper = new ObjectMapper(); $mapped = $mapper->wrap(new Pen()); $mapped->first(); // → Pen (T resolved from argument) $mapper->wrap(new Product())->first()->getPrice(); // new expression arg → Product // Chained instantiation preserves constructor-inferred generics (new ObjectMapper())->wrap(new Pen())->first()->write(); // (new ...)->method() chain with generics // Variadic class-string → union return type $locator2 = new ServiceLocator(); $union = $locator2->getAny(Pen::class, Marker::class); $union->write(); // A|B from variadic class-string $union->highlight(); // Nested generic return: @return Box with class-string param $boxed = $locator->wrap(Pen::class); $boxed->unwrap()->write(); // Box::unwrap() → Pen } } // ── Closure Return Type Template Binding ──────────────────────────────────── class ClosureReturnTemplateDemo { public function demo(): void { // Method-level @template bound from closure return type annotation. // reduce()'s TReduceReturnType is inferred from the closure's `: Pen` return type. /** @var ScaffoldingReducible $pencils */ $pencils = new ScaffoldingReducible(); $merged = $pencils->reduce( fn(Pen $carry, Pencil $item): Pen => $carry, new Pen('starter') ); $merged->write(); // TReduceReturnType = Pen // Same with function() keyword closure $merged2 = $pencils->reduce( function(Pen $carry, Pencil $item): Pen { return $carry; }, new Pen('starter') ); $merged2->color(); // TReduceReturnType = Pen // Chained call: reduce() result used directly without intermediate variable. // The template inference must survive the symbol-map subject text serialization. $pencils->reduce(fn(Pen $carry, Pencil $item): Pen => $carry, new Pen('starter'))->write(); } } // ── Closure Param → Template Inference (Contravariant) ───────────────────── class ClosureParamTemplateDemo { public function demo(): void { // When a method declares @param Closure(T): void $cb, the template // param T is inferred from the closure's *parameter* type annotation // (contravariant position), not the return type. $bus = new ScaffoldingEventBus(); // Arrow function: T inferred as Pen from fn(Pen $p) $result = $bus->listen(function(Pen $p): void { $p->write(); }); $result->write(); // T = Pen $result->color(); // completions for Pen // Full closure: T inferred as User from function(User $u) $user = $bus->listen(function(User $u): void { $u->getEmail(); }); $user->getName(); // T = User // Second param position: @param Closure(int, T): void $proc = new ScaffoldingBatchProcessor(); $item = $proc->process(function(int $i, Pencil $p): void { $p->sketch(); }); $item->sketch(); // T = Pencil (from position 1) $item->sharpen(); } } // ── Trait Generic Substitution ────────────────────────────────────────────── class TraitGenericDemo { public function demo(): void { Product::factory()->create(); // @use HasFactory → UserFactory Product::factory()->count(5)->make(); // count() returns static, make() returns Product $idx = new PenIndex(); // @use Indexable $idx->get()->write(); // TValue → Pen } } // ── Null-Init + Conditional Reassignment ──────────────────────────────────── class NullInitReassignDemo { /** @param list $pens */ public function demo(array $pens): void { // Pattern 1: null-init + foreach reassignment + truthiness guard $found = null; foreach ($pens as $pen) { if ($pen->color() === 'blue') { $found = $pen; } } if ($found) { $found->write(); // Pen from foreach reassignment } // Pattern 2: null-coalesce + guard inside foreach /** @var array $lookup */ $lookup = getUnknownValue(); $keys = ['a', 'b']; foreach ($keys as $key) { $item = $lookup[$key] ?? null; if (!$item) { continue; } $item->write(); // Pen from array access via coalesce } } } // ── Loop-Carried Assignment ───────────────────────────────────────────────── // When a variable is initialized as null and reassigned inside a loop body, // the assignment from a previous iteration is visible at the top of the loop. class LoopCarriedAssignmentDemo { /** @param list $pens */ public function demo(array $pens): void { // Pattern: null-init + reassignment after the usage point in the loop. // On the second iteration, $prev holds the Pen from the prior iteration. $prev = null; foreach ($pens as $pen) { if ($prev !== null) { $prev->write(); // Pen from previous iteration } $prev = $pen; } // Same pattern with a while loop $lastOrder = null; while ($row = rand(0, 1)) { if ($lastOrder !== null) { $lastOrder->getStatusCode(); // Response from previous iteration } $lastOrder = new Response(200, 'ok'); } } } // ── Null Coalesce (`??`) Refinement ───────────────────────────────────────── class NullCoalesceDemo { /** @return ?Pen */ public function maybePen(): ?Pen { return rand(0, 1) ? new Pen() : null; } public function demo(): void { // Non-nullable LHS: `new Foo()` can never be null, so the RHS // is dead code and the result resolves to Pen only. $a = new Pen() ?? new Marker(); $a->write(); // Pen (RHS ignored) // Nullable LHS: `?Pen` return strips null, unions with RHS. $b = $this->maybePen() ?? new Marker(); $b->write(); // Pen|Marker // Clone is non-nullable — RHS is dead code. $pen = new Pen(); $c = clone $pen ?? new Marker(); $c->write(); // Pen (RHS ignored) } } // ── Foreach & Array Access ────────────────────────────────────────────────── class ForeachArrayAccessDemo { public function demo(): void { /** @var list $members */ $members = getUnknownValue(); foreach ($members as $member) { $member->write(); // element type from list } $members[0]->color(); // array access element type /** @var array */ $annotated = []; // @var without variable name $annotated[0]->write(); // type from next-line annotation $inferred = [new Pen(), new Marker()]; $inferred[0]->write(); // element type inferred from literal } } // ── Foreach By-Reference ──────────────────────────────────────────────────── class ForeachByReferenceDemo { public function demo(): void { /** @var list $pens */ $pens = getUnknownValue(); // By-reference foreach: $pen resolves to element type (Pen) // and is not flagged as undefined or unused. foreach ($pens as &$pen) { $pen->write(); // Pen from list $pen = new Pen(); // reassignment through reference } unset($pen); // Key-value with by-reference value /** @var array $named */ $named = getUnknownValue(); foreach ($named as $key => &$tool) { $tool->color(); // Pen from array } unset($tool); } } // ── Property Array Access (generic annotations) ──────────────────────────── class PropertyArrayAccessDemo { /** @var array */ private array $cache = []; /** @var array */ public array $items = []; public function demo(): void { // Property typed as array — variable key $this->cache[$this->getKey()]->write(); // element type from generic annotation // Property typed as array — string-literal key $this->cache['brushes']->color(); // element type from generic annotation // Property typed as array — numeric index $this->items[0]->write(); // element type from generic annotation // Method chain after bracket access $this->cache['tools']->rename('Fine')->write(); // chain through element type } private function getKey(): string { return 'k'; } } // ── Array Destructuring ──────────────────────────────────────────────────── class ArrayDestructuringDemo { public function demo(): void { /** @var list */ [$first, $second] = getUnknownValue(); $first->write(); // destructured element type } } // ── Array Shapes ──────────────────────────────────────────────────────────── class ArrayShapeDemo { public function demo(): void { // Literal array shape — key completion and value types $config = ['host' => 'localhost', 'port' => 3306, 'tool' => new Pen()]; $config['']; // Try: key completion: host, port, tool $config['tool']->write(); // value type → Pen // Annotated shape /** @var array{first: Pen, second: Pencil} $pair */ $pair = getUnknownValue(); $pair['first']->write(); $pair['second']->sketch(); // Shape from function return type $cfg = getAppConfig(); $cfg['logger']->write(); } } // ── Object Shapes ─────────────────────────────────────────────────────────── class ObjectShapeDemo { public function demo(): void { /** @var object{title: string, score: float} $item */ $item = getUnknownValue(); $item->title; // Ctrl+Click → jumps to `title:` in docblock above $item->score; // Ctrl+Click → jumps to `score:` in docblock above } } // ── Spread Operator Type Tracking ─────────────────────────────────────────── class SpreadOperatorDemo { public function demo(): void { /** @var list */ $penList = []; /** @var list */ $pencilList = []; $allPens = [...$penList]; $allPens[0]->write(); // resolves Pen from spread $everything = [...$penList, ...$pencilList]; $everything[0]->label(); // union: Pen|Pencil from multiple spreads } } // ── Clone Expression ──────────────────────────────────────────────────────── class CloneDemo { public function demo(): void { $pen = new Pen('blue'); $copy = clone $pen; $copy->write(); // preserves Pen type } } // ── Class-String Variable Static Access ───────────────────────────────────── class ClassStringStaticDemo { public function demo(): void { $cls = Pen::class; $cls::make(); // static method from Pen } } // ── Class-String Parameter Static Dispatch ────────────────────────────────── class ClassStringParamDispatchDemo { /** * @param class-string<\BackedEnum> $enumClass */ public function demo(string $enumClass): void { // Static method dispatch through class-string parameter. // $enumClass::from() returns static, resolved to BackedEnum. $result = $enumClass::from('foo'); $result->name; // property from UnitEnum via BackedEnum // Foreach over $enumClass::cases() resolves items to BackedEnum. foreach ($enumClass::cases() as $item) { $item->value; // property from BackedEnum $item->name; // property from UnitEnum } } } // ── Ambiguous Variables ───────────────────────────────────────────────────── class AmbiguousVariableDemo { public function demo(): void { if (rand(0, 1)) { $ambiguous = new Lamp(); } else { $ambiguous = new Faucet(); } $ambiguous->turnOff(); // available on both branches $ambiguous->dim(); // available on Lamp branches $ambiguous->drip(); // available on Faucet branches } } // ── Parenthesized Assignment ──────────────────────────────────────────────── class ParenthesizedAssignmentDemo { public function demo(): void { $parenPen = (new Pen('red')); $parenPen->write(); // resolves through parentheses } } // ── String Interpolation ──────────────────────────────────────────────────── class StringInterpolationDemo { public function demo(): void { $pen = new Pen('blue'); echo "Ink is {$pen->color()}"; // brace interpolation — full completion echo "Tool: $pen->ink"; // simple interpolation echo 'no $pen-> here'; // single-quoted — suppressed } } // ── Foreach over Generic Collection Classes ───────────────────────────────── class CollectionForeachDemo { public function demo(): void { $src = new ScaffoldingCollectionForeach(); // From method return type foreach ($src->allPens() as $pen) { $pen->write(); // via method return type → collection generics } // From new instance $items = new PenCollection(); foreach ($items as $item) { $item->color(); // resolves to Pen via @extends generics } // From property type foreach ($src->pens as $pen) { $pen->color(); // via property type → collection generics } // From variable $collection = $src->allPens(); foreach ($collection as $pen) { $pen->write(); // via variable assignment scanning } } } // ── Type Aliases (@phpstan-type / @phpstan-import-type) ───────────────────── /** * @phpstan-type UserData array{name: string, email: string, pen: Pen} * @phpstan-type StatusInfo array{code: int, label: string, owner: User} * @phpstan-type UserList array */ class TypeAliasDemo { public function demo(): void { $data = $this->getUserData(); $data['name']; // @phpstan-type → array shape key completion $data['pen']->write(); // object value → method completion $status = $this->getStatus(); $status['label']; // StatusInfo alias → array shape keys $status['owner']->getEmail(); // object value → method completion // Type alias resolves through foreach iteration foreach ($this->getUsers() as $user) { $user->getDisplayName(); // UserList → array → Profile } } /** @return UserData */ public function getUserData(): array { return ['name' => 'Alice', 'email' => 'alice@example.com', 'pen' => new Pen()]; } /** @return StatusInfo */ public function getStatus(): array { return ['code' => 200, 'label' => 'OK', 'owner' => new User('Alice', 'alice@example.com')]; } /** @return UserList */ public function getUsers(): array { return []; } } /** * @phpstan-import-type UserData from TypeAliasDemo * @phpstan-import-type StatusInfo from TypeAliasDemo as AliasedStatus */ class TypeAliasImportDemo { public function demo(): void { $user = $this->fetchUser(); $user['email']; // imported UserData → array shape keys $user['pen']->color(); // object value → method completion $status = $this->fetchStatus(); $status['code']; // AliasedStatus → StatusInfo → array shape keys $status['owner']->getName(); // object value → method completion } /** @return UserData */ public function fetchUser(): array { return ['name' => 'Bob', 'email' => 'bob@example.com', 'pen' => new Pen()]; } /** @return AliasedStatus */ public function fetchStatus(): array { return ['code' => 404, 'label' => 'Not Found', 'owner' => new User('Bob', 'bob@example.com')]; } } // ── Multi-line @return & Broken Docblock Recovery ─────────────────────────── class BrokenDocblockDemo { public function demo(): void { $collection = collect([]); $collection->groupBy('key'); // multi-line @return resolves correctly $recovered = (new BrokenDocRecovery())->broken(); $recovered->working(); // recovers `static` from broken @return static< } } // ── Callable / Closure Variable Invocation ────────────────────────────────── class ClosureInvocationDemo { public function demo(): void { // Closure literal with native return type hint $makePen = function(): Pen { return new Pen(); }; $makePen()->write(); // resolves Pen from closure return type // Arrow function literal $makePencil = fn(): Pencil => new Pencil(); $makePencil()->sketch(); // arrow fn return type // Docblock callable annotation /** @var \Closure(): Pencil $supplier */ $supplier = getUnknownValue(); $supplier()->sharpen(); // @var Closure() annotation // Chaining after callable invocation $builder = function(): Pen { return new Pen(); }; $builder()->rename('Bold')->write(); // chain after $fn() // Variable assigned from callable invocation $fromClosure = $makePen(); $fromClosure->write(); // $result = $fn() resolves return type // Immediately invoked arrow function with return type $result = (fn(): Pen => new Pen())(); $result->write(); // resolves Pen from arrow fn return type // Immediately invoked closure with return type $obj = (function(): Pencil { return new Pencil(); })(); $obj->sketch(); // resolves Pencil from closure return type } } // ── class-string Variable Resolution ──────────────────────────────────────── class ClassStringVarDemo { public function demo(): void { // new $var where $var holds a class-string $cls = Pen::class; $pen = new $cls; $pen->write(); // resolves Pen from class-string // $var::staticMethod() where $var holds a class-string $userClass = User::class; $found = $userClass::findByEmail('test@example.com'); $found->getEmail(); // resolves User from class-string static call } } // ── iterator_to_array Resolution ──────────────────────────────────────────── class IteratorToArrayDemo { public function demo(): void { /** @var \Iterator $iter */ $iter = getUnknownValue(); $items = iterator_to_array($iter); $items[0]->write(); // resolves Pen from iterator value type } } // ── Compound Negated Guard Clause Narrowing ───────────────────────────────── class CompoundNegatedNarrowingDemo { /** @param Rock|Banana|Lamp $thing */ public function demo($thing): void { // After both negated instanceof checks exit, $thing is Rock|Banana if (!$thing instanceof Rock && !$thing instanceof Banana) { return; } $thing->weigh(); // both Rock and Banana have weigh() } } // ── __invoke() Return Type Resolution ─────────────────────────────────────── class InvokeReturnTypeDemo { public function demo(): void { // Objects with __invoke() can be called like functions. // PHPantom resolves the return type through __invoke(). $formatter = new ScaffoldingFormatter(); $formatter()->write(); // __invoke() returns Pen // Chaining through __invoke() return type $factory = new ScaffoldingPenFactory(); $factory()->rename('Fine')->write(); // __invoke() → Pen → rename() → Pen // Parenthesized property invocation: ($this->prop)() ($this->formatter)()->write(); // resolves through __invoke() // Foreach over __invoke() return type with docblock $fetcher = new ScaffoldingPenFetcher(); foreach ($fetcher() as $item) { $item->write(); // @return Pen[] on __invoke() } // Enum from()/tryFrom() chains to instance methods Status::from('Active')->label(); // from() returns Status } private ScaffoldingFormatter $formatter; } // ── Anonymous Classes ─────────────────────────────────────────────────────── class AnonymousClassDemo { public function demo(): object { return new class extends Pen { public string $brand; public function cap(): string { return ''; } public function demo() { $this->cap(); // own method $this->brand; // own property $this->write(); // inherited from Pen // MUST NOT appear: refill() (private — not inherited) } }; } } // ── Match / Ternary / Null-Coalescing Type Accumulation ───────────────────── class ExpressionTypeDemo { public function demo(): void { $src = new ScaffoldingExpressionType(); // Null-coalescing $fallback = $src->backup ?? $src->primary; $fallback->getStatusCode(); // Response method // Match expression — shared members sort above branch-only members $service = match (rand(0, 1)) { 0 => new ElasticProductReviewIndexService(), 1 => new ElasticBrandIndexService(), }; $service->index(); // on both — sorted first $service->reindex(); // one branch only — sorted after // Ternary $svc = rand(0, 1) ? new ElasticProductReviewIndexService() : new ElasticBrandIndexService(); $svc->index(); // on both — sorted first } } // ── Switch Statement Type Tracking ────────────────────────────────────────── class SwitchDemo { public function demo(string $type): void { switch ($type) { case 'reviews': $service = new ElasticProductReviewIndexService(); break; case 'brands': $service = new ElasticBrandIndexService(); break; } $service->index(); // on both classes } } // ── Array & Object Shapes in Methods ──────────────────────────────────────── class ShapeMethodDemo { public function demo(): void { $data = $this->getToolKit(); $data['pen']->write(); // Pen $data['pencil']->sketch(); // Pencil // Nested annotated shape /** @var array{meta: array{page: int, total: int}, items: list} $response */ $response = getUnknownValue(); $response['meta']['page']; // nested shape key $response['items'][0]->write(); // list element type // Nested keys inferred from literal — no annotation needed $config = ['db' => ['host' => 'localhost', 'port' => 3306], 'debug' => true]; $config['db']['host']; // Try: delete 'host' and trigger completion // Object shapes $profile = $this->getProfile(); $profile->name; // Ctrl+Click → jumps to `name:` in @return docblock $result = $this->getResult(); $result->tool->write(); // Ctrl+Click `tool` → jumps to `tool:` in @return docblock $result->meta->page; // Ctrl+Click `meta` → jumps to `meta:` in @return docblock } /** @return array{pen: Pen, pencil: Pencil, active: bool} */ public function getToolKit(): array { return []; } /** @return object{name: string, age: int, active: bool} */ public function getProfile(): object { return (object) []; } /** @return object{tool: Pen, meta: object{page: int, total: int}} */ public function getResult(): object { return (object) []; } /** @param array{host: string, port: int, tool: Pen} $config */ public function fromParam(array $config): void { $config['host']; // string $config['tool']->write(); // Pen } } // ── Named Key Destructuring from Array Shapes ─────────────────────────────── class DestructuringShapeDemo { public function demo(): void { // Named key from method return ['pen' => $pen, 'pencil' => $pencil] = $this->getToolKit(); $pen->write(); // Pen from 'pen' key $pencil->sketch(); // Pencil from 'pencil' key // Named key from @var annotated variable /** @var array{pen: Pen, pencil: Pencil, active: bool} $data */ $data = getUnknownValue(); ['pen' => $myPen, 'pencil' => $myPencil] = $data; $myPen->write(); // Pen from 'pen' key $myPencil->sketch(); // Pencil from 'pencil' key // Positional from shape /** @var array{Pen, Pencil} $pair */ $pair = getUnknownValue(); [$first, $second] = $pair; $first->write(); // Pen (positional index 0) $second->sketch(); // Pencil (positional index 1) // list() syntax /** @var array{recipe: Recipe, servings: int} $meal */ $meal = getUnknownValue(); list('recipe' => $recipe) = $meal; $recipe->ingredients; // Recipe from 'recipe' key } /** @return array{pen: Pen, pencil: Pencil, count: int} */ public function getToolKit(): array { return []; } } // ── Generic Context Preservation ──────────────────────────────────────────── class GenericContextDemo { public function demo(): void { $src = new ScaffoldingGenericContext(); $src->chest->unwrap()->open(); // Box::unwrap() → Gift $src->display()->first()->open(); // TypedCollection::first() → Gift } } // ── Generic Shape Substitution ────────────────────────────────────────────── class GenericShapeDemo { public function demo(): void { $src = new ScaffoldingGenericShape(); // Template params inside array shape bodies are substituted through inheritance $result = $src->getResult(); $result['data']->open(); // array{data: T} with T=Gift → Gift // Chained bracket access walks shape key then list element $first = $result['items'][0]; $first->open(); // list with T=Gift → Gift } } // ── Foreach, Key Types, and Destructuring ─────────────────────────────────── class IterationDemo { public function demo(): void { $src = new ScaffoldingIteration(); // From method foreach ($src->allPens() as $pen) { $pen->write(); // list → Pen } // From property foreach ($src->batch as $pen) { $pen->write(); } // Key types foreach ($src->crossRef() as $pen => $pencil) { $pen->write(); // Pen (key type) $pencil->sketch(); // Pencil (value type) } // WeakMap keys /** @var \WeakMap $mapping */ $mapping = new \WeakMap(); foreach ($mapping as $pen => $pencil) { $pen->write(); // key: Pen $pencil->sketch(); // value: Pencil } // Destructuring [$first, $second] = $src->allPens(); $first->write(); // destructured element type // Foreach destructuring /** @var array $rows */ $rows = [['Alice', 30], ['Bob', 25]]; foreach ($rows as [$name, $age]) { strlen($name); // string from positional shape abs($age); // int from positional shape } // Foreach keyed shape destructuring /** @var array $inv */ $inv = []; foreach ($inv as ['tool' => $tool, 'count' => $count]) { $tool->write(); // Pen from keyed shape abs($count); // int from keyed shape } // Nested destructuring /** @var array{string, array{Pen, Pencil}} $nested */ $nested = ['label', [new Pen(), new Pencil()]]; [$label, [$nestedPen, $nestedPencil]] = $nested; strlen($label); // string from outer position 0 $nestedPen->write(); // Pen from inner position 0 $nestedPencil->sketch(); // Pencil from inner position 1 } } // ── Foreach Array Shape Elements ──────────────────────────────────────────── class ForeachArrayShapeDemo { /** * @param array $inventory */ public function demo(array $inventory): void { // When iterating over an array whose value type is an array shape, // the foreach variable carries the shape type so that bracket // access resolves each key to its declared type. foreach ($inventory as $entry) { $entry['tool']->write(); // array{tool: Pen, count: int} → Pen } } } // ── Variadic Parameter Foreach ────────────────────────────────────────────── class VariadicForeachDemo { public function demo(Pen ...$pens): void { // Variadic parameters are arrays: foreach extracts the element type foreach ($pens as $pen) { $pen->write(); // element type from variadic Pen ...$pens } } public function unionVariadic(Pen|Pencil ...$tools): void { // Union variadic: foreach value is Pen|Pencil foreach ($tools as $tool) { if ($tool instanceof Pen) { $tool->write(); // narrowed to Pen via instanceof } } } } // ── Array Function Type Preservation ──────────────────────────────────────── class ArrayFuncDemo { public function demo(): void { $src = new ScaffoldingArrayFunc(); $active = array_filter($src->members, fn(Pen $pen) => $pen->color() === 'blue'); $active[0]->write(); // Pen preserved through array_filter $vals = array_values($src->members); $vals[0]->write(); // Pen preserved through array_values $pens = $src->roster(); $last = array_pop($pens); $last->write(); // single Pen from array_pop $cur = current($src->members); $cur->write(); // Pen from current() end($src->members)->write(); // inline end() without variable foreach (array_filter($src->members, fn(Pen $pen) => true) as $pen) { $pen->color(); // Pen preserved in foreach } $mapped = array_map(fn($pen) => $pen, $src->members); $mapped[0]->write(); // Pen from array_map fallback // array_reduce: return type inferred from initial value (3rd arg) $merged = array_reduce($src->members, function(Pen $carry, Pen $item): Pen { return $carry; }, new Pen('merged')); $merged->write(); // Pen from initial value argument // array_sum / array_product: always int|float $total = array_sum([10, 20, 30]); $product = array_product([2, 3, 4]); } } // ── @throws Completion and Catch Variable Types ───────────────────────────── class ExceptionDemo extends ScaffoldingException { public function demo(): void { try { $this->riskyOperation(); } catch (ValidationException $e) { $e->getMessage(); // catch variable resolves exception type } } /** * Typing `@` in this docblock suggests @throws for each uncaught exception. * * @throws NotFoundException * @throws ValidationException */ public function findOrFail(int $id): array { if ($id < 0) { throw new ValidationException('ID must be positive'); } $result = $this->lookup($id); if ($result === null) { throw new NotFoundException('Record not found'); } return $result; } /** * Caught exceptions are filtered out of @throws suggestions. * * @throws AuthorizationException */ public function safeOperation(): void { try { throw new \RuntimeException('transient error'); } catch (\RuntimeException $e) { // caught — not suggested } throw new AuthorizationException('Forbidden'); } /** * Called method's @throws propagate to the caller. * * @throws AuthorizationException */ public function delegatedWork(): void { $this->safeOperation(); } } // ── Constructor @param → Promoted Property Override ───────────────────────── class ParamOverrideDemo { public function demo(): void { foreach ($this->ingredients as $ingredient) { $ingredient->name; // Ingredient::$name $ingredient->format(); // Ingredient::format() } $this->recipe->name; // Recipe::$name } /** * @param list $ingredients * @param Recipe $recipe */ public function __construct( public array $ingredients, // @param overrides to list public object $recipe, // @param overrides to Recipe ) {} } // ── Inline @var on Promoted Constructor Properties ────────────────────────── class InlineVarPromotedDemo { public function __construct( /** @var array */ public array $ingredients, ) {} public function demo(): void { // Inline @var on promoted property overrides the native type hint foreach ($this->ingredients as $ingredient) { $ingredient->name; // Ingredient::$name via inline @var $ingredient->format(); // Ingredient::format() via inline @var } } } // ── Generator / Iterable Yield Type Resolution ───────────────────────────── class GeneratorDemo { public function demo(): void { // Generator — value is 2nd param (Pen) foreach ($this->getPens() as $pen) { $pen->write(); // resolves to Pen } // Generator — value is still 2nd param (Pencil) foreach ($this->processPencils() as $pencil) { $pencil->sketch(); // Pencil (2nd param), not Pen (4th) } // @var annotated generator /** @var \Generator $gen */ $gen = $this->getPens(); foreach ($gen as $pen) { $pen->write(); // Generator → Pen } // iterable — single param is the value type foreach ($this->iterablePens() as $pen) { $pen->write(); } // Method chain through generator element foreach ($this->getPens() as $pen) { $pen->rename('Bold')->color(); } } /** @return \Generator */ public function getPens(): \Generator { yield new Pen(); } /** @return \Generator */ public function processPencils(): \Generator { yield new Pencil(); } /** @return iterable */ public function iterablePens(): iterable { return []; } /** * @param \Generator $pencils */ public function foreachGeneratorParam(\Generator $pencils): void { foreach ($pencils as $pencil) { $pencil->sketch(); // @param overrides native \Generator type } } } // ── Generator Yield Type Inference Inside Bodies ──────────────────────────── // // Generator // // - `yield $expr` produces TValue to the consumer. The yielded variable // keeps its own type (from its assignment), not the Generator annotation. // - `$var = yield $expr` assigns TSend (the sent value) to $var. The yield // expression evaluates to whatever was passed via ->send(). class GeneratorYieldDemo { /** @return \Generator */ public function findAll(): \Generator { // The type of $pen comes from `new Pen(...)`, not from the @return. // Completion on $pen-> works because the assignment is known. $pen = new Pen('blue'); yield $pen; $pen->write(); // resolves to Pen $anotherPen = new Pen('red'); yield 0 => $anotherPen; $anotherPen->color(); // key => value yields also work } /** @return \Generator */ public function yieldInsideControlFlow(): \Generator { if (true) { $pen = new Pen('green'); yield $pen; $pen->write(); // resolves inside control flow blocks } } /** @return \Generator */ public function chainingThroughYieldInferred(): \Generator { $pen = new Pen('black'); yield $pen; $pen->rename('Bold')->color(); // chains through yielded variable } /** @return \Generator */ public function coroutine(): \Generator { // TSend inference: $var = yield gets the 3rd Generator type param. // yield produces 'ready' (TValue = string) to the consumer; // the yield expression evaluates to whatever was ->send()'d (TSend = Pencil). $pencil = yield 'ready'; $pencil->sketch(); // resolves to Pencil (TSend) } /** @return \Generator */ public function tsendInsideNestedBlocks(): \Generator { while (true) { if (true) { $pencil = yield 'waiting'; $pencil->sketch(); // resolves inside nested blocks } } } } // ── Template Parameter Bounds ─────────────────────────────────────────────── /** * @template-covariant TNode of AstNode */ class TemplateBoundsDemo { public function demo(): void { $this->node->getChildren(); // resolves via TNode's bound: AstNode $this->node->getParent(); } /** @var TNode */ public $node; /** @param TNode $node */ public function __construct(AstNode $node) { $this->node = $node; } } // ── Match Class-String Forwarding to Conditional Return Types ─────────────── class MatchClassStringDemo { public function demo(): void { $container = new Container(); // Match expression → class-string → conditional return $requestType = match (rand(0, 1)) { 0 => ElasticProductReviewIndexService::class, 1 => ElasticBrandIndexService::class, }; $requestBody = $container->make($requestType); $requestBody->index(); // on both classes $requestBody->reindex(); // ElasticProductReviewIndexService only // Standalone function with @template $resolved = resolve($requestType); $resolved->index(); // on both classes // Inline chain $container->make($requestType)->index(); // Simple class-string variable $cls = Pen::class; $pen = $container->make($cls); $pen->write(); // resolves through simple $cls variable // Ternary class-string $ternary = rand(0, 1) ? Pen::class : Pencil::class; $obj = $container->make($ternary); $obj->label(); // shared by both types } } // ── Closure Parameter Inference ───────────────────────────────────────────── class ClosureParamInferenceDemo { public function demo(): void { $src = new ScaffoldingClosureParamInference(); // $p is inferred as Pen from map's callable(TValue, TKey) signature $src->items->map(fn($p) => $p->write()); // Closure body $src->items->each(function ($pen) { $pen->write(); // resolves to Pen }); // Explicit type hint takes precedence over inference $src->items->map(fn(Pencil $p) => $p->sketch()); // $this in callable param resolves to receiver, not current class $pipeline = new ScaffoldingPipeline(); $pipeline->when(true, function ($pipe) { $pipe->send('data'); // resolves to ScaffoldingPipeline, not this demo class }); // Arrow function variant $pipeline->tap(fn($p) => $p->through([])); // Function-level @template callable inference // array_any(@param array, @param callable(TValue, TKey): bool) // $item is inferred as Pen from the array's element type via template substitution $holder = new ScaffoldingTemplateCallableHolder(); array_any($holder->tools, fn($item) => $item->write() !== ''); } } // ═══════════════════════════════════════════════════════════════════════════ // TRIVIAL — works in most editors, included for completeness // ═══════════════════════════════════════════════════════════════════════════ // ── Static & Enum Completion ──────────────────────────────────────────────── class StaticEnumDemo { public function demo(): void { User::$defaultRole; // static property User::TYPE_ADMIN; // class constant User::findByEmail('a@b.c'); // static method User::make('Bob'); // inherited static (Model) User::query(); // @mixin Builder (Model) Status::Active; // backed enum case Status::Active->label(); // enum method Status::Active->name; // "Active" (from UnitEnum) Status::Active->value; // "active" (from BackedEnum) Priority::High; // int-backed enum Priority::High->name; // "High" (from UnitEnum) Priority::High->value; // 3 (from BackedEnum, int) Mode::Manual; // unit enum Mode::Manual->name; // "Manual" (from UnitEnum) // Enum case assigned to variable $status = Status::Active; $status->name; // resolves through variable $status->value; // self::/static:: inside enum methods resolve to the enum type Status::defaultValue(); // self::Active->value inside enum } } // ── Signature Help ────────────────────────────────────────────────────────── class SignatureHelpDemo { public function demo(): void { // Place cursor inside parentheses to see parameter hints. // The active parameter updates as you type commas. $user = new User('Alice', 'alice@example.com'); createUser('Alice', 'alice@example.com'); // standalone function $user->setStatus(Status::Active); // instance method User::findByEmail('alice@example.com'); // static method new User('Bob', 'bob@example.com'); // constructor // Chains resolve through return types and properties: $user->getProfile()->setBio('Hello'); // method return chain (new User('X', 'x@x.com'))->setStatus(Status::Active); // (new ...)->method new User('X', 'x@x.com')->setStatus(Status::Active); // PHP 8.4 style // Default values appear in parameter labels (e.g. "int $page = 1"): $svc = new ScaffoldingSignatureHelp(); $svc->paginate(2, 50); // Per-parameter @param descriptions appear next to each parameter. // When the effective docblock type differs from the native PHP type // the description is prefixed with the effective type: $svc->search('php', 1, 25); } } // ── Callable Snippet Insertion ────────────────────────────────────────────── class SnippetInsertionDemo { public function demo(): Response { // Completion inserts snippets with tab-stops for required params $user = new User('Alice', 'alice@example.com'); $user->setName('Bob'); // → setName(${1:$name}) $user->toArray(); // → toArray() (no params) $user->addRoles(); // → addRoles() (variadic — no tab-stops) User::findByEmail('a@b.c'); // → findByEmail(${1:$email}) return new Response(200); // → Response(${1:$statusCode}) } } // ── Go-to-Definition ──────────────────────────────────────────────────────── // All jump targets are defined right after the demo so Ctrl+Click lands // within a few lines, making it easy to verify the feature works. // // Member names deliberately collide with names elsewhere in the file // (label, format, CONNECTION, $defaultRole) so a wrong-target bug // would land on the wrong label() or CONNECTION instead of silently passing. class GoToDefinitionDemo { public function demo(): void { // Ctrl+Click on any symbol to jump to its definition $target = new GtdTarget(); $target->label(); // Ctrl+Click → GtdTarget::label() (not Pen::label) $target->format(); // Ctrl+Click → GtdTarget::format() (not User::format) GtdTarget::FORMAT; // Ctrl+Click → class constant (not Renderable::format) GtdParent::CONNECTION; // Ctrl+Click → GtdParent (not Model::CONNECTION) GtdTarget::$defaultRole; // Ctrl+Click → GtdTarget (not User::$defaultRole) $helper = gtdHelper(); echo $helper; // Ctrl+Click on $helper → jumps to assignment define('APP_VERSION', '1.0.0'); echo APP_VERSION; // BUG: Ctrl+Click should jump to define() above } } class GtdParent { public const string CONNECTION = 'gtd'; } class GtdTarget extends GtdParent { public static string $defaultRole = 'gtd'; public const string FORMAT = 'gtd'; public function label(): string { return 'gtd'; } public function format(): string { return 'gtd'; } } function gtdHelper(): GtdTarget { return new GtdTarget(); } // ── Type Hint Go-to-Definition ────────────────────────────────────────────── // Ctrl+Click on class names in type hints, return types, catch blocks, // and docblock annotations to jump to their definitions. // All referenced types are defined right after the demo so the jump is short. // // Support classes have format()/label() methods that collide with names // elsewhere — if GTD resolves the wrong class, you land on the wrong one. class TypeHintGtdDemo { public function demo(): void { // Catch block exception types — Ctrl+Click GtdNotFoundException or GtdAccessException try { $this->paramTypes(new GtdAlpha()); } catch (GtdNotFoundException|GtdAccessException $e) {} } public function paramTypes(GtdAlpha $item): GtdAlpha { return $item; } // Ctrl+Click GtdAlpha public function unionTypes(GtdAlpha|GtdBeta $item): GtdAlpha|GtdBeta { return $item; } // Ctrl+Click either public function intersectionTypes(GtdShape&GtdColor $item): GtdShape&GtdColor { return $item; } // Ctrl+Click either public function returnType(): GtdResult { return new GtdResult(); } // Ctrl+Click GtdResult /** * @param list $items Ctrl+Click GtdAlpha * @return GtdResult Ctrl+Click GtdResult * @throws GtdNotFoundException Ctrl+Click GtdNotFoundException */ public function docblockTypes($items) { return $items; } /** * Callable types in docblocks. Ctrl+Click on any class name inside the * callable signature to jump to its definition. Hover shows the class * info instead of treating the whole callable as one token. * * @param \Closure(GtdAlpha): GtdResult $transform Ctrl+Click GtdAlpha or GtdResult * @param callable(GtdAlpha, GtdBeta): GtdResult $merge Ctrl+Click any of the three * @return callable(): GtdResult Ctrl+Click GtdResult */ public function callableDocblockTypes($transform, $merge) { return $merge; } } class GtdAlpha { public function label(): string { return 'alpha'; } } class GtdBeta { public function label(): string { return 'beta'; } } interface GtdShape { public function format(): string; } interface GtdColor { public function format(): string; } class GtdResult { public function label(): string { return 'ok'; } } class GtdNotFoundException extends \RuntimeException {} class GtdAccessException extends \RuntimeException {} // ── Go-to-Type-Definition ─────────────────────────────────────────────────── // "Go to Type Definition" jumps to the *type's* class declaration, not the // variable's definition site. Compare with regular Go-to-Definition: // • Go-to-Definition on $user → jumps to the $user = ... assignment // • Go-to-Type-Definition on $user → jumps to class User { ... } // // Try: place the cursor on $target, $result, or $pet below and invoke // "Go to Type Definition" (often bound to a secondary shortcut or // right-click menu). Union types produce a peek list with all classes. class GoToTypeDefinitionDemo { public function demo(): void { $target = new GtdTarget(); $target; // Type Definition → GtdTarget $result = $this->getResult(); $result; // Type Definition → GtdResult $pet = $this->getPet(); $pet; // Type Definition → GtdAlpha | GtdBeta (two locations) $this; // Type Definition → GoToTypeDefinitionDemo } public function getResult(): GtdResult { return new GtdResult(); } /** @return GtdAlpha|GtdBeta */ public function getPet(): GtdAlpha|GtdBeta { return new GtdAlpha(); } } // ── Go-to-Implementation ──────────────────────────────────────────────────── // All implementors are defined right after the demo so "Go to Implementations" // lands within a few lines. // // The interface method is format() — same name as Renderable::format(), // User::format(), Ingredient::format(). A resolver bug would jump to one // of those instead of the local implementor. class GoToImplementationDemo { // Right-click → "Go to Implementations" on GtdPrintable // to jump to GtdPlainPrinter and GtdHtmlPrinter below. // Try: Go-to-Implementation on "format" → format() in each implementor public function demo(GtdPrintable $printer): string { return $printer->format(); } } interface GtdPrintable { public function format(): string; } class GtdPlainPrinter implements GtdPrintable { public function format(): string { return 'plain'; } } class GtdHtmlPrinter implements GtdPrintable { public function format(): string { return 'html'; } } // ── Reverse Go-to-Implementation ──────────────────────────────────────────── // Go-to-Implementation also works in reverse: from a concrete method back to // the interface or abstract method it satisfies. class ReverseImplementationDemo implements GtdPrintable { // Try: Go-to-Implementation on "format" below → jumps to // GtdPrintable::format() (the interface prototype). public function format(): string { return 'reverse'; } } // ── Type Hierarchy ────────────────────────────────────────────────────────── // Right-click a class/interface name → "Show Type Hierarchy" to see its // supertypes (parent class, implemented interfaces) and subtypes (classes // that extend or implement it). // // Try on GtdPrintable: supertypes → (none), subtypes → GtdPlainPrinter, GtdHtmlPrinter, ReverseImplementationDemo // Try on ReverseImplementationDemo: supertypes → GtdPrintable, subtypes → (none) // Try on User: supertypes → Model, Renderable, subtypes → AdminUser // Try on Model: supertypes → (none), subtypes → User, ClassFilteringDemo, HoverOriginsDemo // ── Context-Aware Class Name Filtering ────────────────────────────────────── // Try: erase the class name after each keyword and re-trigger completion. // // extends Model → classes only, non-final // MUST show: User, Response, Pen (non-final classes) // MUST NOT show: AdminUser (final), Model (abstract), // Renderable (interface), HasTimestamps (trait), Status (enum) // // extends Renderable → interfaces only (interface-extends-interface) // MUST show: Renderable, GtdShape, Printable // MUST NOT show: User (class), HasTimestamps (trait), Status (enum) // // implements Renderable → interfaces only // MUST show: Renderable, GtdShape, Printable // MUST NOT show: User (class), HasTimestamps (trait), Status (enum) // // use HasTimestamps → traits only (inside class body) // MUST show: HasTimestamps, HasSlug, JsonSerializer // MUST NOT show: User (class), Renderable (interface), Status (enum) // // instanceof User → classes, interfaces, enums (no traits) // MUST show: User, Renderable, Status // MUST NOT show: HasTimestamps (trait) // // new User → concrete non-abstract classes only // MUST show: User, Pen, Response // MUST NOT show: Model (abstract), AdminUser (final is ok for new), // Renderable (interface), HasTimestamps (trait), Status (enum) class ClassFilteringDemo extends Model implements Renderable { use HasTimestamps; public function test(): bool { return $this instanceof User; } public function format(string $template): string { return ''; } public function toArray(): array { return []; } } // ── Type Hint Completion in Definitions ───────────────────────────────────── // Try: trigger completion when typing a type hint — PHP scalars (string, // int, float, bool) appear alongside class names, with no constants or // functions in the list. Traits are excluded because they cannot be used // as type hints in PHP (the type check always fails at runtime). // // The same filtering applies in PHPDoc type positions: @param, @return, // and @var exclude traits, while @throws uses Throwable-filtered // completion (only exception classes and Throwable interfaces). function typeHintDemo(User $user, string $name): string { return $user->displayName . $name; } function unionDemo(string|int $value, ?User $maybe): string { return $maybe . $maybe->displayName; } // ── $_SERVER Superglobal ──────────────────────────────────────────────────── class ServerSuperglobalDemo { public function demo(): void { $_SERVER['']; // Try: key completion for REQUEST_METHOD, HTTP_HOST, etc. } } // ═══════════════════════════════════════════════════════════════════════════ // ADVANCED — specialized features // ═══════════════════════════════════════════════════════════════════════════ // ── Intersection Types ────────────────────────────────────────────────────── class IntersectionDemo { public function demo(Envelope&Printable $item): void { $item->print(); // from Printable $item->seal(); // from Envelope } } // ── Ternary Narrowing ────────────────────────────────────────────────────── class TernaryNarrowingDemo { public function demo(): void { $thing = pickRockOrBanana(); $thing instanceof Rock ? $thing->crush() : $thing->peel(); } } // ── Class Alias ───────────────────────────────────────────────────────────── class ClassAliasDemo { public function demo(): void { $profile = new Profile(new User('Eve', 'eve@example.com')); $profile->getDisplayName(); // Profile → UserProfile via `use ... as` } } // ── self::class / static::class ───────────────────────────────────────────── class SelfClassDemo { public function demo(): string { return self::class; // resolves to SelfClassDemo } } // ── Trait insteadof / as Conflict Resolution ──────────────────────────────── class TraitConflictDemo { use JsonSerializer, XmlSerializer { JsonSerializer::serialize insteadof XmlSerializer; XmlSerializer::serialize as serializeXml; JsonSerializer::serialize as private internalSerialize; } public function demo(): void { $this->internalSerialize(); // aliased as private $this->serialize(); // JsonSerializer wins via insteadof $this->serializeXml(); // XmlSerializer::serialize aliased $this->toJson(); // non-conflicting from JsonSerializer $this->toXml(); // non-conflicting from XmlSerializer } } // ── unset() Tracking ──────────────────────────────────────────────────────── class UnsetDemo { public function demo(): void { $pen = new Pen('blue'); $pen->write(); // resolves to Pen unset($pen); // Try: $pen-> — no completions (variable was unset) // Re-assigning after unset restores type $tool = new Pen('red'); unset($tool); $tool = new Marker(); $tool->highlight(); // resolves to Marker // unset only affects targeted variable $pen2 = new Pen('green'); $pencil = new Pencil(); unset($pen2); $pencil->sketch(); // still resolves to Pencil } } // ── First-Class Callable Syntax (PHP 8.1) ─────────────────────────────────── class FirstClassCallableDemo { public function demo(): void { $src = new ScaffoldingFirstClassCallable(); $fun = makePen(...); $fun()->write(); // function reference → Closure returning Pen $orderFn = $src->dispatch(...); $orderFn()->write(); // instance method → Closure returning Pen $finder = Pen::make(...); $finder()->color(); // static method → Closure returning Pen $make = makePen(...); $pen = $make(); $pen->color(); // assigned result from callable invocation // Immediate invocation: method(...)() returns the method's return type makePen(...)()->write(); // function first-class callable invoked immediately Pen::make(...)()->color(); // static method first-class callable invoked immediately $src->dispatch(...)()->write(); // instance method first-class callable invoked immediately $immediate = Pen::make(...)(); $immediate->color(); // assigned result from immediate static callable invocation } } // ── Array Element Access from Assignments ─────────────────────────────────── class ArrayAccessDemo { public function demo(): void { $src = new ScaffoldingArrayAccess(); $pens = $src->fetchAll(); // Pen[] from method return $pens[0]->write(); // resolves to Pen $gifts = (new ScaffoldingGenericContext()) ->display(); $gifts[0]->open(); // resolves to Gift (element of Gift[]) $first = $pens[0]; $first->color(); // resolves via $first = $pens[0] // Inline method-return array access (no intermediate variable) $src->fetchAll()[0]->write(); // resolves Pen from Pen[] return type $src->fetchAll()[0]->color(); // same, different member } } // ── Closure / Arrow-Function Members ──────────────────────────────────────── class ClosureMembersDemo { public function demo(): void { $typedClosure = function(Pen $pen): string { return $pen->write(); }; $typedClosure->bindTo($this); // resolves to Closure::bindTo $typedClosure->call($this); // resolves to Closure::call $typedArrow = fn(int $posX): float => $posX * 1.5; $typedArrow->bindTo($this); // resolves to Closure::bindTo $fun = function(): void {}; $bound = $fun->bindTo($this); $bound->call($this); // chained: $bound is still Closure } } // ── Deprecation Messages ──────────────────────────────────────────────────── // Hover over deprecated members to see the message text from @deprecated. // When @see tags are present alongside @deprecated, the diagnostic message // includes the @see references so you know what to migrate to. // Completion shows deprecated items with strikethrough styling. class DeprecationDemo { public function demo(): void { $src = new ScaffoldingDeprecation(); // Diagnostic: "'sendLegacy' is deprecated: Use sendAsync() instead. // (see: ScaffoldingDeprecation::sendAsync())" $src->sendLegacy(); // Diagnostic: "'oldProcess' is deprecated: See: ScaffoldingDeprecation::sendAsync()" // (bare @deprecated + @see → "See:" becomes the main text) $src->oldProcess(); // Diagnostic includes @see reference for the property too $src->debugMode; // Diagnostic includes @see reference for the constant ScaffoldingDeprecation::OLD_LIMIT; // Hover on any constant: shows its value inline (e.g. const MAX_LIMIT = 500;) ScaffoldingDeprecation::MAX_LIMIT; // ── #[Deprecated] attribute ───────────────────────────────── // PHPantom reads #[Deprecated] from both phpstorm-stubs // (\JetBrains\PhpStorm\Deprecated with reason:/since:) and // native PHP 8.4 (\Deprecated with message:/since:). // JetBrains stubs style: reason: + since: $src->attrDeprecatedMethod(); // Native PHP 8.4 style: message: + since: $src->nativeDeprecatedMethod(); // Bare #[Deprecated] (no arguments) $src->attrBareMethod(); // Positional reason: #[Deprecated("...")] $src->attrPositionalMethod(); // Attribute on property $src->attrProp; // Attribute on constant ScaffoldingDeprecation::ATTR_OLD; // Docblock @deprecated wins when both are present $src->bothDocAndAttr(); // ── Version-aware suppression ─────────────────────────────── // When #[Deprecated(since: "X.Y")] declares a version and your // project targets an older PHP version (via composer.json or // .phpantom.toml), the deprecation diagnostic is suppressed. // For example, if you target PHP 8.0: // - attrDeprecatedMethod() (since: "8.1") → suppressed // - nativeDeprecatedMethod() (since: "8.4") → suppressed // - sendLegacy() (@deprecated docblock, no since) → still shown // ── Replacement code action ───────────────────────────────── // When #[Deprecated(replacement: "...")] provides a template, // placing the cursor on the call and pressing the quick-fix // shortcut offers "Replace with `newFunc(...)`". // Template variables: %parametersList%, %parameter0%, %class%. $src->legacySetTimezone('UTC'); } } // ── Hover: Origin Indicators ──────────────────────────────────────────────── class HoverOriginsDemo extends Model implements Renderable { public function demo(): void { // Hover on `format` → "◆ implements Renderable" $this->format('earth'); // Hover on `toArray` → "↑ overrides Model" $this->toArray(); // Hover on `getName` → no indicator (inherited, not overridden) $this->getName(); } // Implements Renderable (Model has no format method) public function format(string $template): string { return ''; } // Overrides the abstract toArray() from Model public function toArray(): array { return []; } } // ── Diagnostic: Unknown Class ─────────────────────────────────────────────── // `MutateArrayInsertSpec` and `Cluster` below are not imported and cannot be // resolved — they get a yellow "Class 'X' not found" warning underline. // This diagnostic fires for any ClassReference that PHPantom cannot resolve // through use-map, local classes, same-namespace, class_index, classmap, // PSR-4, or stubs. It pairs with the "Import Class" code action: press // Ctrl+. (Cmd+. on Mac) on the warning to import the class in one step. // ── Code Action: Import Class ─────────────────────────────────────────────── // Place cursor on `MutateArrayInsertSpec` and press Ctrl+. (or Cmd+. on Mac) // to see "Import `Couchbase\MutateArrayInsertSpec`" in the quick-fix menu. // Accepting inserts a `use Couchbase\MutateArrayInsertSpec;` at the top. // // Because this file has two unresolved names, the quick-fix menu also shows // "Import all missing classes" which imports both at once. class ImportClassDemo { public function demo(): void { // Ctrl+. on `MutateArrayInsertSpec` → offers to import $spec = new MutateArrayInsertSpec('path', ['value']); // Ctrl+. on `Cluster` → offers to import Couchbase\Cluster Cluster::connect('couchbase://localhost'); } } // ── Code Action: Remove Unused Import ─────────────────────────────────────── // The `use ReflectionClass;` below is unused — it appears dimmed in the editor. // Place cursor on it and press Ctrl+. → "Remove unused import 'ReflectionClass'" use ReflectionClass; class RemoveUnusedImportDemo { public function demo(): void { // ReflectionClass is deliberately NOT used here so its import stays dimmed. // Ctrl+. on the dimmed `use ReflectionClass;` above → remove it. $x = 42; } } // ── Diagnostic: Unknown Member Access ─────────────────────────────────────── // When PHPantom resolves the subject type but the member does not exist after // full resolution (inheritance, traits, virtual members), a yellow "Method // 'X' not found on class 'Y'" warning appears. Suppressed when __call, // __callStatic, or __get magic methods are present on the resolved class. class UnknownMemberDemo { public function demo(): void { $user = new User('test', 'test@example.com'); // These resolve fine — no warning: $user->getEmail(); $user->getName(); // Try: uncomment the next line to see the warning: $user->nonexistentMethod(); // Static access — unknown constant gets a warning: User::MISSING_CONST; } } // ── PHPDoc Block Generation ───────────────────────────────────────────────── // Typing `/**` above a declaration generates a docblock skeleton. Tags are // only emitted when the native type hint needs enrichment: missing types get // @param/${mixed}, bare `array` gets a placeholder, and classes with @template // parameters get generic type tab stops (e.g. Collection). // Fully-typed scalar params/return types are skipped. Properties and // constants always get @var. Uncaught exceptions always get @throws. // No special treatment for overrides. class PhpDocGenerationDemo extends ScaffoldingException { public const int MAX_ITEMS = 100; const LABEL = 'demo'; public string $title = ''; public $description; public function demo($data, array $items, Closure $handler, callable $fallback, TypedCollection $primary, string $boring, TypedCollection $secondary): array { try { throw new ValidationException('Invalid id'); } catch (ValidationException $e) { // Caught — should NOT appear in @throws. } /** @throws NotFoundException */ getUnknownValue(); $this->throwsException(); return []; } } // Class-level @extends with template tab stops. The parent TypedCollection // has @template TKey and @template TValue, so typing `/**` above this class // generates `@extends TypedCollection` with tab stops. // Try: type `/**` above this class. class DocGenExtendsDemo extends TypedCollection { public function customMethod(): void {} } // ── Diagnostic: Scalar Member Access ──────────────────────────────────────── // Accessing a property or calling a method on a scalar type (int, string, // bool, float, null, void, never) is always a runtime error. PHPantom flags // these with an Error-severity diagnostic, including through method-return // chains. class ScalarMemberAccessDemo { public function demo(User $user): void { // getName() returns string — accessing a method on it is an error: $user->getName()->trim(); // getEmail() returns string — property access is also an error: $user->getEmail()->length; // Chains through intermediate classes work too: $user->getProfile()->getDisplayName()->toUpper(); // Works with Response too — isSuccess() returns bool: $resp = new Response(200, 'OK'); $resp->isSuccess()->flag; } } // ── Diagnostic: Unresolved Member Access (opt-in) ─────────────────────────── // When PHPantom cannot resolve the *subject type* of a member access at all, // it can show a hint-level diagnostic. This is off by default because most // codebases lack full type coverage. Enable it in .phpantom.toml: // // [diagnostics] // unresolved-member-access = true // // This is useful for discovering gaps in type coverage or places where // PHPantom's inference falls short. class UnresolvedMemberAccessDemo { public function demo(): void { // $mystery has type "mixed" — PHPantom cannot resolve it. // With the diagnostic enabled, a hint appears on the next line: $mystery = getUnknownValue(); $mystery->doSomething(); } } // ── Diagnostic: Argument Count ────────────────────────────────────────────── // PHPantom flags calls that pass too few or too many arguments. Variadic // parameters accept unlimited trailing args. Argument unpacking (`...$args`) // suppresses the diagnostic because the actual count is unknown statically. class ArgumentCountDemo { public function demo(): void { $user = new User('Alice', 'alice@test.com'); // Correct — no diagnostic: $user->getEmail(); $user->setName('Bob'); $user->addRoles('admin', 'editor', 'viewer'); // variadic // Too few arguments — error diagnostic appears: $user->setStatus(); // Too many arguments — error diagnostic appears: $user->getEmail('extra'); } } class TypeErrorDemo { public function demo(): void { $user = new User('Alice', 'alice@test.com'); // Correct — no diagnostic: $user->setName('Bob'); $user->setStatus(Status::Active); // Type error — string passed to int parameter: $this->requiresInt("not a number"); // Type error — null passed to non-nullable parameter: $this->requiresString(null); // Type error — wrong class type: $pen = new Pen('blue'); $this->requiresUser($pen); // No diagnostic — subclass is compatible: $admin = new AdminUser('Admin', 'admin@test.com', ['manage']); $this->requiresUser($admin); // No diagnostic — null is valid for nullable parameter: $this->acceptsNullable(null); $this->acceptsNullable("hello"); // No diagnostic — int widens to float: $this->requiresFloat(42); } private function requiresInt(int $value): void {} private function requiresString(string $text): void {} private function requiresUser(User $user): void {} private function acceptsNullable(?string $text): void {} private function requiresFloat(float $value): void {} } // ── Implement Missing Methods (Code Action) ───────────────────────────────── // Uncomment the class below, place the cursor inside it, and trigger // "Quick Fix" or "Code Action" to see "Implement 3 missing methods". // The generated stubs include correct visibility, parameter types, defaults, // and return types. Re-comment when done (PHP fatals on unimplemented // abstract methods). // class ImplementMethodsDemo extends ScaffoldingAbstractShape implements ScaffoldingDrawable // { // } // ── Generate Constructor (Code Action) ────────────────────────────────────── // Place the cursor inside the class below and trigger "Code Action" to see // "Generate constructor". The generated constructor includes a parameter // and assignment for each non-static property. Readonly properties are // included because they must be initialized in the constructor. Default // values are carried over and required parameters are placed before // optional ones. class GenerateConstructorDemo { public int $age; public string $name; public string $status = 'active'; public ?string $email; public readonly string $id; public static int $instanceCount; // excluded (static) } // ── Generate Getter/Setter (Code Action) ──────────────────────────────────── // Place the cursor on a property declaration below and trigger "Code Action" // to see "Generate getter", "Generate setter", and "Generate getter and // setter". Bool properties use an `is` prefix (`isActive()`). Readonly // properties only offer a getter. Static properties generate static // methods. If a getter or setter already exists, the corresponding action // is suppressed. class GenerateGetterSetterDemo { private string $name; private bool $active; public readonly int $id; private static int $count; /** @var list */ public $tags; } // ── Generate Property Hooks (Code Action, PHP 8.4+) ──────────────────────── // Place the cursor on a property declaration below and trigger "Code Action" // to see "Generate get hook", "Generate set hook", and "Generate get and set // hooks". The property declaration is rewritten to include hook blocks // inline. Readonly properties are skipped (PHP 8.4 forbids hooks on readonly // properties). Static properties are also skipped. Interface // properties generate abstract hook signatures without bodies. Properties // that already have one hook only offer the missing one. class GeneratePropertyHooksDemo { // Cursor here → all three hook actions offered public string $title; // Cursor here → no hook actions (readonly properties cannot have hooks) public readonly int $id; // Cursor here → no hook actions (static) public static int $counter; // Cursor here → only "Generate set hook" (get already exists) public string $label { get => $this->label; } // Default values are preserved when hooks are added public string $status = 'draft'; } // ── Property-Level Narrowing ──────────────────────────────────────────────── class PropertyNarrowingDemo { private Pen|Pencil $tool; /** @var Pen|Pencil|null */ public $untyped; public function demo(): void { // instanceof narrows a property inside the then-body if ($this->tool instanceof Pen) { $this->tool->write(); // narrowed to Pen } // Negated instanceof + early return narrows after the guard if (!$this->tool instanceof Pencil) { return; } $this->tool->sketch(); // narrowed to Pencil // assert() narrows an untyped property assert($this->untyped instanceof Pen); $this->untyped->color(); // narrowed to Pen } } // ── Attribute Signature Help ──────────────────────────────────────────────── #[Attribute] class DemoRoute { public function __construct( public string $path, public string $method = 'GET', ) {} } class AttributeSigHelpDemo { // Try: place cursor inside the attribute parens and trigger signature help. // Named parameter completion also works: type "method:" after the first arg. #[DemoRoute('/users', method: 'POST')] public function store(): void {} } // ── Pass-by-Reference Parameter Type ──────────────────────────────────────── class PassByReferenceDemo { public function demo(): void { // When a function takes a typed &$var parameter, the variable // acquires that type after the call. initPen($pen); $pen->write(); // $pen is now Pen // Static method calls with by-ref parameters: PenFactory::create($staticPen); $staticPen->write(); // $staticPen is now Pen // Constructor calls with by-ref parameters: new PenBuilder($ctorPen); $ctorPen->write(); // $ctorPen is now Pen // Instance method calls ($this->method) with by-ref parameters: $this->init($thisPen); $thisPen->write(); // $thisPen is now Pen } private function init(?Pen &$pen): void { $pen = new Pen(); } } // ── Interface Template Inheritance ────────────────────────────────────────── class InterfaceTemplateDemo { public function demo(): void { // When a class implements an interface with @template + class-string, // the implementing class inherits the template machinery. $locator = new ScaffoldingEntityLocator(); $locator->find(Pen::class)->write(); // T resolves to Pen via class-string } } // ── Function-level @template (collect) ────────────────────────────────────── class CollectGenericDemo { public function demo(): void { /** @var Pen[] $pens */ $pens = []; // collect() uses function-level @template to carry element types // through to the returned FluentCollection. $collection = collect($pens); $collection->first()->write(); // TValue resolves to Pen // Inline chaining works too collect($pens)->first()->write(); // same resolution, no intermediate variable } } // ── Generic @phpstan-assert Narrowing ─────────────────────────────────────── class GenericAssertNarrowingDemo { public function demo(object $obj): void { // @phpstan-assert with @template + class-string resolves // the narrowed type from the call-site argument. ScaffoldingAssert::assertInstanceOf(Pen::class, $obj); $obj->write(); // $obj narrowed to Pen } } // ── @param-closure-this ───────────────────────────────────────────────────── class ParamClosureThisDemo { public function demo(): void { $router = new ScaffoldingClosureThisRouter(); // @param-closure-this overrides $this inside the closure to // ScaffoldingClosureThisRoute instead of ParamClosureThisDemo. $router->group(function () { $this->middleware('auth'); // resolves Route::middleware() $this->prefix('/api'); // resolves Route::prefix() }); // Chaining through the overridden $this $router->group(function () { $this->middleware('auth')->prefix('/v2'); }); // @param-closure-this with $this as the type (declares the // closure's $this is the method's declaring class). $router->extend('redis', function () { $this->getDefaultDriver(); // resolves Router::getDefaultDriver() }); } } // ── Code Lens: prototype method annotations ───────────────────────────────── // Open this class and look at the gutter above each method. PHPantom shows // clickable annotations ("↑ ParentClass::method" or "◆ Interface::method") // that navigate to the parent/interface declaration. class CodeLensDemo extends ScaffoldingAbstractShape implements ScaffoldingDrawable { // ↑ ScaffoldingAbstractShape::area — click to jump to abstract declaration public function area(): float { return 3.14; } // ↑ ScaffoldingAbstractShape::perimeter protected function perimeter(): float { return 6.28; } // ◆ ScaffoldingDrawable::draw — interface implementations use ◆ public function draw(string $color, float $opacity = 1.0): void {} } // ── Inlay Hints ───────────────────────────────────────────────────────────── // Enable inlay hints in your editor to see parameter names, by-reference // indicators, and closure type hints. PHPantom shows: // - Parameter name hints: greet(/*name:*/ 'Alice', /*age:*/ 25) // - By-reference indicators: modify(/*&data:*/ $arr) // - Closure param types: $users->map(fn(/*User*/ $u) => $u->name) // - Closure return types: fn($u) /*: string*/ => $u->name // Hints are suppressed when the argument already makes the parameter obvious // (e.g. $name matches $name, or a property ->name matches $name). class InlayHintsDemo { public function demo(): void { // Parameter name hints appear before each argument: $user = createUser('Alice', 'test@example.com'); // name:, email: // By-reference parameters show & before the name: $arr = [1, 2, 3]; $this->modify($arr, 'ascending'); // &data:, direction: // Hints are suppressed when variable name matches parameter: $needle = 'search term'; $this->search($needle, 10); // (no hint for $needle), limit: // Constructor calls also get hints: $recipe = new Recipe('Cake', [new Ingredient('flour', 2)]); // name:, ingredients: // Static method calls: User::findByEmail('alice@example.com'); // email: // Chained method calls: $pen = Pen::make('blue'); // color: $pen->rename('Sky Blue'); // name: // ── Closure / arrow function hints ───────────────────────── // When a closure or arrow function is passed to a callable-typed // parameter, PHPantom infers types from the callable signature. // Untyped params show the inferred type before $var, and the // return type shows after the closing parenthesis. // Arrow function: "User " before $u, ": string" after parens. $names = $this->mapUsers(fn($u) => $u->name); // Long-form closure gets the same treatment: $upper = $this->mapUsers(function ($u) { return strtoupper($u->name); }); // Partial typing: only the untyped $b gets a hint. $sum2 = $this->reduce(fn(int $a, $b) => $a + $b); // Already-typed parameters and return types get no hint: $emails = $this->mapUsers(fn(User $u): string => $u->email); // Standalone functions with callable params work too: $doubled = $this->transformItems([1, 2, 3], fn($x) => $x * 2); // Method call context — filter shows "Order " before $o, ": bool" after. $big = $this->filterOrders(fn($o) => $o->isAdmin); } /** @param array &$data */ public function modify(array &$data, string $direction): void {} public function search(string $needle, int $limit = 10): mixed { return null; } /** * @template T * @param array $items * @param callable(T): T $fn * @return array */ public function transformItems(array $items, callable $fn): array { return $fn(); } /** @param \Closure(User): string $fn */ public function mapUsers(\Closure $fn): array { return []; } /** @param callable(int, int): int $fn */ public function reduce(callable $fn): int { return 0; } /** @param callable(User): bool $fn */ public function filterOrders(callable $fn): array { return []; } } // ── Change Visibility ─────────────────────────────────────────────────────── // Place cursor on any member and trigger code actions (Ctrl+. / Cmd+.). // PHPantom offers "Make protected", "Make private", etc. class ChangeVisibilityDemo { public string $title = ''; protected int $count = 0; private bool $active = true; public function getTitle(): string { return $this->title; } protected function increment(): void { $this->count++; } private function toggle(): void { $this->active = !$this->active; } public const VERSION = 1; protected const LIMIT = 100; private const SECRET = 'shh'; // Promoted constructor parameters also support visibility change: public function __construct( private string $name, protected int $age, public string $role = 'user', ) {} } // ── Update Docblock ───────────────────────────────────────────────────────── // Place cursor on a method with a stale docblock and trigger code actions. // PHPantom offers "Update docblock to match signature" when the @param // tags are out of sync with the actual parameters. class UpdateDocblockDemo { /** * This docblock is out of date: $old was removed, $added is new, * and $renamed had its type changed from string to int. * * @param string $old This param was removed * @param string $renamed Wrong type, should be int * @return string Wrong return type, should be array */ public function staleDocblock(int $renamed, bool $added): array { return []; } /** * Redundant @return void is removed when the signature already says void. * * @param string $name * @return void */ public function redundantReturn(string $name): void {} /** * Refinement types in docblocks are preserved (not overwritten). * * @param non-empty-string $label A descriptive label * @param array $tags Tag list */ public function refinementsPreserved(string $label, array $tags): void {} } // ── Type Specificity in Virtual Property Merging ──────────────────────────── class TypeSpecificityDemo { public function demo(): void { $cfg = new ScaffoldingAppConfig(); // Hover $cfg->locale — should show string (from native type hint), // not mixed (from the trait's @property tag). $cfg->locale; // Hover $cfg->timezone — should show string (from native type hint), // not mixed (from the trait's @property tag). $cfg->timezone; // Hover $cfg->retries — should show int (from native type hint), // not mixed (from the trait's @property tag). $cfg->retries; } } // ── Mixin Generic Substitution ────────────────────────────────────────────── class MixinGenericDemo { public function demo(): void { $line = new ScaffoldingOrderLine(); // @mixin Builder on Relation resolves TModel → Product // through: BelongsTo @extends Relation → @mixin Builder // → TRelatedModel=Product → Builder → firstOrFail(): TModel=Product $line->product()->firstOrFail()->getPrice(); // Same resolution through find() $line->product()->find()->getSku(); } } // ── Constant Type Inference ───────────────────────────────────────────────── // Hover over $timeout, $name, $rate, $enabled, or $hosts to see the type // inferred from the constant's initializer value. class ConstantTypeDemo { const TIMEOUT = 30; const NAME = 'app'; const RATE = 3.14; const ENABLED = true; public function demo(): void { // Class constants without type hints — type inferred from value: $timeout = self::TIMEOUT; // → int $name = self::NAME; // → string $rate = self::RATE; // → float $enabled = self::ENABLED; // → bool // Global constants — type inferred from define()/const value: $hosts = CT_ALLOWED_HOSTS; // → array $version = CT_APP_VERSION; // → string } } // ── Extract Function / Method (Code Action) ──────────────────────────────── // Select one or more complete statements inside a method body and trigger // "Code Action" to see "Extract function" or "Extract method". // // Variables defined before the selection become parameters. Variables // written inside the selection and read afterwards become return values. // When $this is used, the code is extracted as a private method. class ExtractFunctionDemo { private int $factor = 3; public function demo(): void { // Select these two lines and extract: // → creates a function with $x as return value (read after selection) $x = 10; $y = $x * 2; echo $x + $y; } public function methodExtraction(): void { // Select this line and extract: // → creates a private method (uses $this) $result = $this->factor * 42; echo $result; } public static function staticExtraction(): void { // Select these lines and extract: // → creates a private static method $a = 1; $b = 2; echo $a + $b; } } // ── Promote Constructor Parameter ─────────────────────────────────────────── // Place cursor on a constructor parameter (e.g. `string $name`) and trigger // code actions to see "Promote to constructor property". The action removes // the property declaration, removes the `$this->name = $name;` assignment, // and adds the visibility modifier directly on the parameter. class PromoteConstructorParamDemo { private string $name; protected int $age; private readonly string $email; public function __construct(string $name, int $age, string $email) { $this->name = $name; $this->age = $age; $this->email = $email; } } // ── Simplify Null Coalescing / Null-Safe ──────────────────────────────────── // Place your cursor on any ternary below and trigger code actions. // PHPantom offers "Simplify to ??" or "Simplify to ?->" where applicable. class SimplifyNullDemo { public function demo(?Pen $pen, ?User $user): void { // ── isset → ?? ───────────────────────────────────────────── // Code action: "Simplify to ??" → $pen ?? makePen() $tool = isset($pen) ? $pen : makePen(); // ── !== null → ?? ────────────────────────────────────────── // Code action: "Simplify to ??" → $pen ?? makePen() $tool2 = $pen !== null ? $pen : makePen(); // ── === null (reversed) → ?? ─────────────────────────────── // Code action: "Simplify to ??" → $user ?? createUser() $fallback = $user === null ? createUser() : $user; // ── !== null + method call → ?-> ─────────────────────────── // Code action: "Simplify to ?->" → $pen?->color() $color = $pen !== null ? $pen->color() : null; // ── !== null + property access → ?-> ─────────────────────── // Code action: "Simplify to ?->" → $user?->email $email = $user !== null ? $user->email : null; // ── === null + method (reversed) → ?-> ───────────────────── // Code action: "Simplify to ?->" → $pen?->label() $label = $pen === null ? null : $pen->label(); // ── Compound subject → correct ?-> placement ─────────────── // Code action: "Simplify to ?->" → $user->getProfile()?->getDisplayName() $profile = $user->getProfile(); $name = $profile !== null ? $profile->getDisplayName() : null; } } // ── Attribute Completion ──────────────────────────────────────────────────── // Inside `#[…]`, completion only offers classes decorated with // `#[\Attribute]`, filtered by the target of the declaration the // attribute applies to. class AttributeCompletionDemo { public string $property; public function demo(): void { // Nothing to complete at runtime — this demo is about the // completion popup. Open the class below and trigger // completion inside the `#[…]` brackets to see it in action. } } // ── Loop Array Build (variable-key assignment tracking) ───────────────────── class LoopArrayBuildDemo { /** @param list $pens */ public function demo(array $pens): void { // Variable-key assignment inside a loop: `$arr[$var] = $value` // PHPantom tracks the RHS type as the array's element type. $indexed = []; foreach ($pens as $i => $pen) { $key = $pen->color(); $indexed[$key] = $pen; } // Foreach over the built array resolves element members foreach ($indexed as $item) { $item->write(); // Pen method via element type tracking } // Bracket access resolves element type $indexed['red']->color(); // Pen method // Null-coalescing with guard clause $found = $indexed['blue'] ?? null; if ($found === null) { return; } $found->write(); // narrowed to Pen } } class ConditionalLoopShapeDemo { /** @param list $pens */ public function demo(array $pens): void { // Array built with variable keys inside a loop where the assignment // is inside a conditional branch (if/else). The shape type from // the array literal is preserved through foreach iteration. $grouped = []; foreach ($pens as $pen) { $key = $pen->color(); if (array_key_exists($key, $grouped)) { $grouped[$key]['count']++; } else { $grouped[$key] = [ 'tool' => $pen, 'count' => 1, ]; } } // Foreach over the built array resolves shape keys foreach ($grouped as $entry) { $entry['tool']->write(); // Pen method via shape tracking } } } // ── Conditional Shape Key Completion ──────────────────────────────────────── // When an array shape gains a key inside an if-block, completion resolves // through the union of shapes produced by branch merging. class ConditionalShapeKeyDemo { public function demo(?Pen $pen): void { // Base shape with a known key $options = [ 'name' => 'default', ]; // Conditionally add a key with an object value if ($pen !== null) { $options['tool'] = $pen; } // After the if-block, $options is a union of shapes: // array{name: string} | array{name: string, tool: Pen} // Completion on the conditionally-added key resolves to Pen. $options['tool']->write(); // Pen method via conditional shape union } } // ── Invalid Class-Like Kind Diagnostics ───────────────────────────────────── // PHPantom flags class-like names used in positions where their kind is // guaranteed to fail at runtime. Open demo() and look for Error/Warning // squiggles on the class names. class InvalidClassKindDemo { public function demo(): void { // Error: cannot instantiate abstract class $a = new ScaffoldingAbstractShape(); // Error: cannot instantiate enum $b = new Status(); // Warning: instanceof with a trait always evaluates to false $x = new Pen('test'); $result = $x instanceof JsonSerializer; // Warning: trait in a type hint will always fail type checking $this->acceptTrait(new Pen('test')); } private function acceptTrait(JsonSerializer $x): JsonSerializer { return $x; } // These also produce diagnostics but would crash at class-load time, // so they are commented out. See the AGENTS.md hoisting pitfall note. } // ── Untyped Property Inference ────────────────────────────────────────────── // Properties without type declarations have their types inferred from // constructor assignments (`$this->prop = new Foo()`) and promoted // parameter defaults (`private $prop = new Foo()`). Trigger completion // after `->` on the property to see methods from the inferred type. class UntypedPropertyInferenceDemo { private $repository; private $logger; public function __construct( private $defaultRepo = new ScaffoldingUntypedRepo(), ) { $this->repository = new ScaffoldingUntypedRepo(); $this->logger = new ScaffoldingUntypedLogger(); } public function demo(): void { // Constructor body assignment: $this->repository = new ScaffoldingUntypedRepo() $this->repository->findById(1); // resolves ScaffoldingUntypedRepo::findById() // Constructor body assignment: $this->logger = new ScaffoldingUntypedLogger() $this->logger->info('hello'); // resolves ScaffoldingUntypedLogger::info() // Promoted parameter default: private $defaultRepo = new ScaffoldingUntypedRepo() $this->defaultRepo->findById(42); // resolves ScaffoldingUntypedRepo::findById() } } // ── Deep Variable Chain ───────────────────────────────────────────────────── // The variable resolver walks function bodies top-to-bottom in a single pass. // Assignment chains of any depth resolve without recursion or depth limits. // Place the cursor after `->` on any variable below to see completions from // the correct class, regardless of how many intermediate assignments there are. class DeepVariableChainDemo { public function demo(): void { // 5-level chain: each variable is assigned from a method/property on the previous. $brush = new Brush(); $canvas = $brush->getCanvas(); $easel = $canvas->easel; $material = $easel->material; // string from Easel::$material $back = $canvas->getBrush(); $back->stroke(); // Brush::stroke() — full round-trip // Reassignment chains: the resolver picks the most recent assignment. $tool = new Pen(); $tool->write(); // Pen::write() $tool = new Pencil(); $tool->sketch(); // Pencil::sketch() — Pen::write() is gone $tool = new Marker(); $tool->highlight(); // Marker::highlight() } } // ── Closure Scope Inference ───────────────────────────────────────────────── // Closures capture variables from the enclosing scope via `use()`. Arrow // functions inherit the enclosing scope automatically. Untyped closure // parameters are inferred from the callable signature of the enclosing call. class ClosureScopeInferenceDemo { /** @param list $pens */ public function demo(array $pens): void { // Closure captures $pens via use() and iterates over it. $worker = function () use ($pens): void { foreach ($pens as $pen) { $pen->write(); // Pen from captured $pens } }; // Arrow function inherits enclosing scope automatically. $brush = new Brush(); $sized = fn() => $brush->setSize('large'); // Variables survive past closure arguments in chained calls. $product = new Pen(); $items = [1, 2, 3]; array_map(function (int $i) { return $i * 2; }, $items); $product->write(); // Pen — not lost after the closure } } // ── Body Return Type Inference ────────────────────────────────────────────── // When a method has no declared return type and no @return docblock, // PHPantom infers the type by scanning the method body for return statements. class BodyReturnTypeDemo { public function demo(): void { $factory = new ScaffoldingUntypedFactory(); // Single return: `return new Pen()` → Pen $pen = $factory->createPen(); $pen->write(); // Multiple returns: union of `new Pen()` and `new Pencil()` $tool = $factory->createTool(true); $tool->write(); // shared by Pen (also Pencil via sketch) // No return statements → void (no completions) $factory->setup(); $pencils = $factory->getPencils(); foreach ($pencils as $pencil) { $pencil->sketch(); } } } // ── Global Keyword ───────────────────────────────────────────────────────── $globalPen = new Pen(); function globalKeywordDemo(): void { global $globalPen; $globalPen->write(); // Pen — resolved from top-level scope via `global` } // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ SCAFFOLDING — Supporting definitions below this line. ┃ // ── Template-param @mixin scaffolding ─────────────────────────────────────── interface ScaffoldingAstNodeInterface { public function getStartColumn(): int; public function getEndColumn(): int; } /** * @template-covariant TNode of ScaffoldingAstNodeInterface * @mixin TNode */ abstract class ScaffoldingAbstractAstNode { /** @return string */ public function getMetric(): string { return ''; } /** @return mixed */ public function __call(string $name, array $arguments): mixed { return match ($name) { 'getStartColumn', 'getEndColumn' => 0, default => null, }; } } /** * @extends ScaffoldingAbstractAstNode */ class ScaffoldingConcreteAstNode extends ScaffoldingAbstractAstNode {} // ── class-string instantiation scaffolding ─────────────────────────────── class ScaffoldingClassStringFactory { /** * @template T of object * @param class-string $class * @return T */ public static function create(string $class): object { return new $class(); } } // ── Attribute Completion scaffolding ──────────────────────────────────────── #[\Attribute(\Attribute::TARGET_CLASS)] class ClassOnlyAttr {} #[\Attribute(\Attribute::TARGET_METHOD)] class MethodOnlyAttr {} #[\Attribute(\Attribute::TARGET_PROPERTY)] class PropertyOnlyAttr {} #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class ClassOrMethodAttr {} #[\Attribute] class AnyTargetAttr {} // ── Constant Type Demo scaffolding ────────────────────────────────────────── define('CT_ALLOWED_HOSTS', ['localhost', '127.0.0.1']); const CT_APP_VERSION = '2.0.0'; // StaticPropHolder — used by MixedAccessorDemo class StaticPropHolder { public static string $shared = 'hello'; /** @var self */ public self $holder; } // TreeMapperImpl — used by ConditionalReturnDemo (literal string conditional) class TreeMapperImpl { /** * @return ($signature is "foo" ? Pen : Marker) */ public function map(string $signature, mixed $source): Pen|Marker { return new Pen(); } } // ┃ Everything below exists to support the demos above. ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ // // Keep shared classes NARROW (2-4 members). The whole point of the demos // is that a human can glance at the completion list and immediately tell // whether the right type resolved. A 15-member class defeats that because // the expected item could be buried on page two. // // If a demo needs a richer object, create a NEW class in a demo-specific // section instead of expanding a shared one. Every member you add to a // shared class leaks into every demo that uses it. // // RUNTIME ASSERTIONS: When adding a new demo, add matching assert() calls // to runDemoAssertions() at the bottom of the Demo namespace. This catches // cases where our scaffolding stubs don't actually return what their // docblocks claim. Run: php -d zend.assertions=1 examples/demo.php // // HOISTING PITFALL: Do NOT add __toString() to any class that is // forward-referenced via `extends` or `implements`. PHP implicitly adds // `implements \Stringable`, which prevents class hoisting. This is a // known PHP limitation (php-src#7873), not a bug that will be fixed. // The same applies to `interface Foo extends \Stringable`. // ── Untyped Property Inference Scaffolding ────────────────────────────────── class ScaffoldingUntypedRepo { public function findById(int $id): Pen { return new Pen('found'); } public function save(Pen $pen): void {} } class ScaffoldingUntypedLogger { public function info(string $msg): void {} public function error(string $msg): void {} } // ── Demo-Specific Scaffolding ─────────────────────────────────────────────── // ── Body Return Type Inference scaffolding ────────────────────────────────── class ScaffoldingUntypedFactory { public function createPen() { return new Pen(); } public function createTool(bool $flag) { if ($flag) { return new Pen(); } return new Pencil(); } public function setup() { echo 'initializing'; } public function getPencils() { return [new Pencil()]; } } // ── Inherited Docblock Scaffolding ────────────────────────────────────────── interface ScaffoldingPenHolderInterface { /** @return list */ public function getPens(): array; /** @param list $pens */ public function accept(array $pens): void; } class ScaffoldingConcreteHolder implements ScaffoldingPenHolderInterface { public function getPens(): array { return [new Pen()]; } public function accept(array $pens): void {} } class ScaffoldingPenBox implements ScaffoldingPenHolderInterface { public function getPens(): array { return [new Pen()]; } public function accept(array $items): void {} // renamed param } class ScaffoldingBasePenHolder { /** @return list */ public function getPens(): array { return [new Pen()]; } } class ScaffoldingChildHolder extends ScaffoldingBasePenHolder { public function getPens(): array { return [new Pen()]; } } class ScaffoldingMidHolder extends ScaffoldingBasePenHolder { public function getPens(): array { return [new Pen()]; } } class ScaffoldingDeepChild extends ScaffoldingMidHolder { public function getPens(): array { return [new Pen()]; } } class ScaffoldingAnimalStore { /** @return list */ public function getAnimals(): array { return [new Pen()]; } } class ScaffoldingCatStore extends ScaffoldingAnimalStore { /** @return list */ public function getAnimals(): array { return [new Pencil()]; } } class ScaffoldingMotor { public function start(): void {} } class ScaffoldingSedan extends ScaffoldingMotor { public function cruise(): void {} } abstract class ScaffoldingAbstractShape { abstract public function area(): float; abstract protected function perimeter(): float; } interface ScaffoldingDrawable { public function draw(string $color, float $opacity = 1.0): void; } class ScaffoldingSignatureHelp { /** * Paginate a result set. * * @param int $page Current page number. * @param int $limit Max items per page. * @return array The paginated slice of results. */ public function paginate(int $page = 1, int $limit = 25): array { return []; } /** * Search for items matching a query. * * @param non-empty-string $query The search keywords. * @param positive-int $page Page number to return. * @param int $perPage Results per page. * @return list Matching items. */ public function search(string $query, int $page = 1, int $perPage = 20): array { return []; } } class ScaffoldingDeprecation { /** * @deprecated Use sendAsync() instead. * @see ScaffoldingDeprecation::sendAsync() */ public function sendLegacy(): void {} /** * @deprecated * @see ScaffoldingDeprecation::sendAsync() */ public function oldProcess(): void {} public function sendAsync(): void {} /** * @deprecated Use isDebug() instead. * @see ScaffoldingDeprecation::sendAsync() */ public bool $debugMode = false; /** * @deprecated Use MAX_LIMIT instead. * @see ScaffoldingDeprecation::MAX_LIMIT */ const OLD_LIMIT = 100; const MAX_LIMIT = 500; // JetBrains stubs style #[\JetBrains\PhpStorm\Deprecated(reason: "Use modernMethod() instead", since: "8.1")] public function attrDeprecatedMethod(): void {} // Native PHP 8.4 style (\Deprecated) #[\Deprecated(message: "Use nativeModern() instead", since: "8.4")] public function nativeDeprecatedMethod(): void {} #[\Deprecated] public function attrBareMethod(): void {} #[\Deprecated("Use positionalModern() instead")] public function attrPositionalMethod(): void {} #[\JetBrains\PhpStorm\Deprecated(reason: "The property is deprecated", since: "8.4")] public string $attrProp = ''; #[\Deprecated(reason: "Use NEW_SETTING instead")] const ATTR_OLD = 0; /** * @deprecated Docblock message wins. */ #[\Deprecated(reason: "Attribute message loses")] public function bothDocAndAttr(): void {} #[\Deprecated(replacement: "%class%->setTimezone(%parametersList%)", since: "5.5")] public function legacySetTimezone(string $tz): void {} public function setTimezone(string $tz): void {} } /** * @property mixed $locale * @property mixed $timezone * @property mixed $retries */ trait ScaffoldingMixedDefaults {} class ScaffoldingAppConfig { use ScaffoldingMixedDefaults; public string $locale = 'en'; public string $timezone = 'UTC'; public int $retries = 3; } /** * @property string $gorilla * @method bool hyena(string $x) */ class Zoo extends ZooBase implements ZooContract { use ZooTraitA; use ZooTraitB; public string $baboon = ''; protected string $keeper = 'hidden'; // trip wire — must NOT appear on $zoo-> private string $ceo = 'invisible'; // trip wire — must NOT appear on $zoo-> public function aardvark(): void {} private function nocturnal(): void {} // trip wire — must NOT appear on $zoo-> public function __construct( public int $buffalo = 0, ) { parent::__construct(); } public function __get(string $name): mixed { return match ($name) { 'gorilla' => 'gorilla-value', // @property string $gorilla 'iguana' => 'iguana-value', // @property-read string $iguana (ZooContract) default => null, }; } public function __call(string $name, array $args): mixed { return match ($name) { 'hyena' => true, // @method bool hyena(string $x) 'jaguar' => 'jaguar-value', // @method string jaguar() (ZooContract) default => null, }; } } abstract class ZooBase { public function __construct( public readonly string $cheetah = '', ) {} public function falcon(): string { return ''; } } trait ZooTraitA { public function dingo(): void {} } trait ZooTraitB { public function elephant(string $value): string { return $value; } } /** * @property-read string $iguana * @method string jaguar() */ interface ZooContract {} class ScaffoldingChainingDemo { public Brush $brush; public Canvas $canvas; public function __construct() { $this->brush = new Brush(); $this->canvas = new Canvas(); } } class ScaffoldingExpressionType { public ?Response $backup; public Response $primary; public function __construct() { $this->backup = new Response(500, 'Backup'); $this->primary = new Response(200, 'OK'); } } // ScaffoldingGenericShape — used by GenericShapeDemo /** * @template T */ class ScaffoldingGenericShapeBase { /** @return array{data: T, items: list} */ public function getResult(): array { return []; } } /** * @extends ScaffoldingGenericShapeBase */ class ScaffoldingGenericShape extends ScaffoldingGenericShapeBase {} class ScaffoldingCollectionForeach { public PenCollection $pens; public function allPens(): PenCollection { return new PenCollection(); } } class ScaffoldingGenericContext { /** @var Box */ public $chest; public function __construct() { $this->chest = new Box(new Gift()); } /** @return TypedCollection */ public function display(): TypedCollection { return new TypedCollection([new Gift()]); } } class ScaffoldingIteration { /** @var list */ public array $batch; /** @return list */ public function allPens(): array { return []; } /** @return array */ public function crossRef(): array { return []; } } class ScaffoldingArrayFunc { /** @var list */ public array $members; /** @return list */ public function roster(): array { return []; } } class ScaffoldingException { protected function lookup(int $id): ?array { return null; } protected function riskyOperation(): void {} /** @throws AuthorizationException */ protected function throwsException(): void { throw new AuthorizationException('forbidden'); } } class ScaffoldingClosureParamInference { /** @var FluentCollection */ public FluentCollection $items; public function __construct() { $this->items = new FluentCollection([new Pen('red'), new Pen('blue')]); } } class ScaffoldingEventBus { /** * @template T * @param Closure(T): void $callback * @return T */ public function listen(Closure $callback): mixed { $params = (new \ReflectionFunction($callback))->getParameters(); $type = $params[0]->getType(); $class = $type instanceof \ReflectionNamedType ? $type->getName() : 'stdClass'; return (new \ReflectionClass($class))->newInstanceWithoutConstructor(); } } class ScaffoldingBatchProcessor { /** * @template T * @param Closure(int, T): void $handler * @return T */ public function process(Closure $handler): mixed { $params = (new \ReflectionFunction($handler))->getParameters(); $type = $params[1]->getType(); $class = $type instanceof \ReflectionNamedType ? $type->getName() : 'stdClass'; return (new \ReflectionClass($class))->newInstanceWithoutConstructor(); } } class ScaffoldingTemplateCallableHolder { /** @var array */ public array $tools = []; } /** * @template TValue */ class ScaffoldingReducible { /** * @template TReduceInitial * @template TReduceReturnType * * @param callable(TReduceInitial|TReduceReturnType, TValue): TReduceReturnType $callback * @param TReduceInitial $initial * @return TReduceReturnType */ public function reduce(callable $callback, mixed $initial): mixed { return $initial; } } class ScaffoldingPipeline { /** * @param callable($this, mixed): $this $callback * @return $this */ public function when(bool $condition, callable $callback): static { return $this; } /** * @param callable($this): void $callback * @return $this */ public function tap(callable $callback): static { return $this; } public function send(mixed $data): static { return $this; } public function through(array $pipes): static { return $this; } } // ScaffoldingClosureThisRoute / ScaffoldingClosureThisRouter — used by ParamClosureThisDemo class ScaffoldingClosureThisRoute { public function middleware(string $m): self { return $this; } public function prefix(string $p): self { return $this; } } class ScaffoldingClosureThisRouter { public function getDefaultDriver(): string { return ''; } /** * @param-closure-this ScaffoldingClosureThisRoute $callback */ public function group(\Closure $callback): void {} /** * @param string $driver * @param \Closure $callback * @param-closure-this $this $callback * @return $this */ public function extend(string $driver, \Closure $callback): self { return $this; } } class ScaffoldingFirstClassCallable { public function dispatch(): Pen { return new Pen(); } } class ScaffoldingArrayAccess { /** @return Pen[] */ public function fetchAll(): array { return []; } } class ScaffoldingFormatter { public function __invoke(): Pen { return new Pen(); } } class ScaffoldingPenFactory { public function __invoke(): Pen { return new Pen(); } } class ScaffoldingPenFetcher { /** @return Pen[] */ public function __invoke(): array { return []; } } // ── AST Node (template bounds demo) ──────────────────────────────────────── class AstNode { /** @return AstNode|null */ public function getParent(): ?AstNode { return null; } /** @return AstNode[] */ public function getChildren(): array { return []; } public function getType(): string { return ''; } } // ── ObjectMapper (method-level @template demo) ────────────────────────────── class ObjectMapper { /** * @template T * @param T $item * @return TypedCollection */ public function wrap(object $item): TypedCollection { /** @var TypedCollection */ return new TypedCollection([$item]); } /** * @template T * @param T $item * @return T */ public function identity(mixed $item): mixed { return $item; } } // ─── Interfaces ───────────────────────────────────────────────────────────── /** * @method string render() * @property-read string $output */ interface Renderable { public function format(string $template): string; } // ─── Traits ───────────────────────────────────────────────────────────────── trait JsonSerializer { public function serialize(): string { return '{}'; } public function toJson(): string { return $this->serialize(); } } trait XmlSerializer { public function serialize(): string { return ''; } public function toXml(): string { return $this->serialize(); } } trait HasTimestamps { protected ?string $createdAt = null; public function getCreatedAt(): ?string { return $this->createdAt; } public function setCreatedAt(string $date): static { $this->createdAt = $date; return $this; } } trait HasSlug { public function generateSlug(string $value): string { return strtolower(str_replace(' ', '-', $value)); } } /** * @template TFactory */ trait HasFactory { /** @return TFactory */ public static function factory() {} } /** * @template TKey * @template TValue */ trait Indexable { /** @return TValue */ public function get() {} /** @return TKey */ public function key() {} } // ─── Enums ────────────────────────────────────────────────────────────────── enum Status: string { case Active = 'active'; case Inactive = 'inactive'; case Pending = 'pending'; public function label(): string { return match ($this) { self::Active => 'Active', self::Inactive => 'Inactive', self::Pending => 'Pending', }; } public function isActive(): bool { return $this === self::Active; } /** Returns the raw backing value of the Active case. */ public static function defaultValue(): string { return self::Active->value; // self::CaseName->value resolved } } enum Priority: int { case Low = 1; case Medium = 2; case High = 3; } enum Mode { case Automatic; case Manual; } // ─── Builder (@mixin target) ──────────────────────────────────────────────── class Builder { /** @return static */ public static function query(): self { return new static(); } public function where(string $col, mixed $val): self { return $this; } } // ─── Abstract Base Class ──────────────────────────────────────────────────── /** * @property string $magicName * @method static static create(array $attributes) * @mixin Builder */ abstract class Model { protected int $id; public const string CONNECTION = 'default'; protected const int PER_PAGE = 15; public function __construct( protected string $name = '', public readonly string $uuid = '', ) { $this->id = rand(1, 99999); } public function getId(): int { return $this->id; } public function getName(): string { return $this->name; } /** @return static */ public function setName(string $name): static { $this->name = $name; return $this; } /** @deprecated */ public static function find(int $id): ?static { return null; } /** @return static */ public static function make(string $name = ''): static { return new static($name, ''); } abstract public function toArray(): array; } // ─── Concrete Classes ─────────────────────────────────────────────────────── /** * @property string $displayName * @property-read bool $isAdmin * @method bool hasPermission(string $permission) */ class User extends Model implements Renderable { use HasTimestamps; use HasSlug; public string $email; protected Status $status; private array $roles = []; public static string $defaultRole = 'user'; public const string TYPE_ADMIN = 'admin'; public const string TYPE_USER = 'user'; public function __construct( string $name, string $email, private readonly string $password = '', public int $age = 0, ) { parent::__construct($name); $this->email = $email; $this->status = Status::Active; } public function getEmail(): string { return $this->email; } public function getStatus(): Status { return $this->status; } public function setStatus(Status $status): self { $this->status = $status; return $this; } public function addRoles(string ...$roles): void { $this->roles = array_merge($this->roles, $roles); } public function getRoles(): array { return $this->roles; } public function getProfile(): UserProfile { return new UserProfile($this); } public function toArray(): array { return [ 'id' => $this->getId(), 'name' => $this->getName(), 'email' => $this->email, 'status' => $this->status->value, ]; } public function format(string $template): string { return str_replace('{name}', $this->getName(), $template); } public static function findByEmail(string $email): ?self { return null; } protected function hashPassword(string $raw): string { return password_hash($raw, PASSWORD_BCRYPT); } private function secretInternalMethod(): void {} } class UserProfile { public string $bio = ''; public function __construct(private User $user) {} public function getUser(): User { return $this->user; } public function setBio(string $bio): self { $this->bio = $bio; return $this; } public function getDisplayName(): string { return $this->user->getName() . ' (' . $this->user->getEmail() . ')'; } } final class AdminUser extends User { /** @var string[] */ private array $permissions = []; public function __construct(string $name, string $email) { parent::__construct($name, $email); } public function toArray(): array { $base = parent::toArray(); $base['connection'] = parent::CONNECTION; $base['permissions'] = $this->permissions; return $base; } public function grantPermission(string $permission): void { $this->permissions[] = $permission; } } class Response { public function __construct( private string|int $statusCode, private string|array|null $body = null, ) {} public function getStatusCode(): string|int { return $this->statusCode; } public function getBody(): string|array|null { return $this->body; } public function isSuccess(): bool { return $this->statusCode >= 200 && $this->statusCode < 300; } } // ─── Generics (@template / @extends) ─────────────────────────────────────── /** * @template T */ class Repository { /** @var T|null */ protected $cached = null; /** @return T */ public function find(int $id) { return $this->cached; } /** @return T|null */ public function findOrNull(int $id) { return $this->cached; } /** @return T */ public function first() { return $this->cached; } } /** @extends Repository */ class PenRepository extends Repository {} class CachingPenRepository extends PenRepository { public function clearCache(): void {} } // ─── @implements Generic Resolution ───────────────────────────────────────── /** * @template TEntity */ interface Storage { /** @return TEntity */ public function find(int $id); /** @return TEntity[] */ public function findAll(); } /** @implements Storage */ class PenStorage implements Storage { public function find(int $id) { return new Pen(); } public function findAll() { return [new Pen()]; } } /** @template-implements Storage */ class PenCatalog implements Storage { public function find(int $id) { return new Pen(); } public function findAll() { return [new Pen()]; } } /** * @template T * @implements \IteratorAggregate */ class IterableCollection implements \IteratorAggregate { /** @return \ArrayIterator */ public function getIterator(): \ArrayIterator { return new \ArrayIterator([]); } } /** @extends IterableCollection */ class ItemIterableCollection extends IterableCollection {} /** * @template TKey of array-key * @template-covariant TValue */ class TypedCollection { /** @var array */ protected array $items; /** @param array $items */ public function __construct(array $items = []) { $this->items = $items; } /** @return TValue */ public function first() { return reset($this->items); } /** @return ?TValue */ public function last() { return end($this->items) ?: null; } /** @return static */ public function filter(callable $fn): static { return $this; } /** @return int */ public function count(): int { return count($this->items); } /** @return array */ public function all(): array { return $this->items; } } /** @extends TypedCollection */ class PenCollection extends TypedCollection { public function thickOnly(): self { return $this; } } /** @phpstan-extends TypedCollection */ class ResponseCollection extends TypedCollection {} // ─── Container (conditional return types) ─────────────────────────────────── class Container { /** @var array */ private array $bindings = []; /** * @template TClass * @param string|null $abstract * @return ($abstract is class-string ? TClass : mixed) */ public function make(?string $abstract = null): mixed { if ($abstract === null) { return $this; } return $this->bindings[$abstract] ?? new $abstract(); } public function bind(string $abstract, object $obj): void { $this->bindings[$abstract] = $obj; } public function getStatus(): int { return 200; } } // ─── Method-Level @template Classes ───────────────────────────────────────── class ServiceLocator { /** * @template T * @param class-string $id * @return T */ public function get(string $id): object { return new $id(); } /** * @template T * @param class-string ...$ids * @return T */ public function getAny(string ...$ids): object { return new ($ids[0])(); } /** * @template T * @param class-string $id * @return Box */ public function wrap(string $id): object { return new Box(new $id()); } } class Factory { /** * @template T * @param class-string $class * @return T */ public static function create(string $class): object { return new $class(); } } // ─── Generic Wrapper ──────────────────────────────────────────────────────── /** * @template T */ class Box { /** @var T */ public $value; /** @param T $value */ public function __construct(mixed $value = null) { $this->value = $value; } /** @return T */ public function unwrap() { return $this->value; } } class Gift { public function open(): string { return 'surprise!'; } public function getTag(): string { return 'birthday'; } } // ─── Narrowing Demo Support Classes ───────────────────────────────────────── class Rock { public function crush(): string { return 'smash!'; } public function weigh(): float { return 5.0; } } class Banana { public function peel(): string { return 'yum!'; } public function weigh(): float { return 0.2; } } // ─── Ambiguous Variable Support Classes ───────────────────────────────────── class Lamp { public function dim(): void {} public function turnOff(): void {} } class Faucet { public function drip(): void {} public function turnOff(): void {} } // ─── Intersection Demo Support Classes ────────────────────────────────────── interface Printable { public function print(): void; } class Envelope { public function seal(): void {} } // ─── Shared Narrow Classes ────────────────────────────────────────────────── // These are small, purpose-built classes for demos. Keep them narrow (2-4 // members each). If a demo needs a richer object, create a new class in a // demo-specific section below instead of expanding these. class Pen { public function __construct(public string $ink = 'black') {} public function write(): string { return ''; } public function color(): string { return $this->ink; } public function label(): string { return 'pen'; } /** @return static */ public function rename(string $name): static { return $this; } /** @return static */ public static function make(string $color = 'black'): static { return new static($color); } private function refill(): void {} // trip wire — must NOT appear on external $pen-> } class Pencil { public function sketch(): string { return ''; } public function sharpen(): void {} public function label(): string { return 'pencil'; } } class Marker extends Pen { public function highlight(): void {} } // ─── Chaining Demo Support Classes ────────────────────────────────────────── class Brush { public function setSize(string $size): static { return $this; } public function setStyle(string $style): static { return $this; } public function stroke(): string { return ''; } public function getCanvas(): Canvas { return new Canvas(); } protected function calibrate(): void {} // trip wire — must NOT appear on $studio->brush-> public static function find(int $id): ?static { return null; } } class Canvas { public Easel $easel; public function __construct() { $this->easel = new Easel(); } public function getBrush(): Brush { return new Brush(); } public function title(): string { return ''; } } class Easel { public string $material = 'wood'; public function height(): string { return '150cm'; } } // ─── Expression Type Support Classes ──────────────────────────────────────── class ElasticProductReviewIndexService { public function index(array $markets = []): void {} public function reindex(): void {} } class ElasticBrandIndexService { public function index(array $markets = []): void {} public function bulkDelete(array $ids): void {} } // ─── Param Override Support Classes ───────────────────────────────────────── class Ingredient { public function __construct( public string $name = '', public float $quantity = 0.0, ) {} public function format(): string { return "{$this->quantity}x {$this->name}"; } } class Recipe { /** * @param list $ingredients */ public function __construct( public string $name = '', public array $ingredients = [], ) {} } // ─── Trait Generic Support Classes ────────────────────────────────────────── class UserFactory { public function create(): User { return new User('', ''); } public function count(int $n): static { return $this; } public function state(array $state): static { return $this; } public function make(): User { return new User('', ''); } } /** @use HasFactory */ class Product { use HasFactory; public function getPrice(): float { return 0.0; } public function getSku(): string { return ''; } } // ─── Mixin Generic Scaffolding ───────────────────────────────────────────── /** * @template TModel */ class ScaffoldingMixinBuilder { /** @return TModel */ public function firstOrFail(): mixed { return null; } /** @return TModel */ public function find(): mixed { return null; } } /** * @template TRelatedModel * @mixin ScaffoldingMixinBuilder */ class ScaffoldingMixinRelation { } /** * @extends ScaffoldingMixinRelation */ class ScaffoldingMixinBelongsTo extends ScaffoldingMixinRelation { } class ScaffoldingOrderLine { public function product(): ScaffoldingMixinBelongsTo { return new ScaffoldingMixinBelongsTo(); } } /** @use Indexable */ class PenIndex { use Indexable; } // ─── Exception Classes ────────────────────────────────────────────────────── class NotFoundException extends \RuntimeException {} class ValidationException extends \RuntimeException {} class AuthorizationException extends \RuntimeException {} // ─── Standalone Functions ─────────────────────────────────────────────────── /** * @template TClass * @param string|null $abstract * @return ($abstract is class-string ? TClass : Container) */ function app(?string $abstract = null): mixed { static $container = null; if ($container === null) { $container = new Container(); } return $abstract !== null ? $container->make($abstract) : $container; } function createUser(string $name, string $email): User { return new User($name, $email); } function makePen(): Pen { return new Pen(); } function pickPenOrPencil(): Pen|Pencil { return new Pen(); } function getUnknownValue(): mixed { return new AdminUser('', ''); } /** * @template T * @param class-string $class The class name * @return T */ function resolve(string $class): object { return new $class(); } /** * @return array{logger: Pen, debug: bool} */ function getAppConfig(): array { return []; } function pickRockOrBanana(): Rock|Banana { return new Rock(); } /** @phpstan-assert Rock $value */ function assertRock(mixed $value): void { if (!$value instanceof Rock) { throw new \InvalidArgumentException('Expected Rock'); } } /** @phpstan-assert-if-true Rock $value */ function isRock(mixed $value): bool { return $value instanceof Rock; } /** @phpstan-assert-if-false Rock $value */ function isNotRock(mixed $value): bool { return !$value instanceof Rock; } class StaticAssert { /** @phpstan-assert Rock $value */ public static function assertRock(mixed $value): void { if (!$value instanceof Rock) { throw new \InvalidArgumentException('Expected Rock'); } } /** @phpstan-assert-if-true Rock $value */ public static function isRock(mixed $value): bool { return $value instanceof Rock; } /** @phpstan-assert-if-false Rock $value */ public static function isNotRock(mixed $value): bool { return !$value instanceof Rock; } } // ─── Pipe Operator / Pass-by-Reference / Interface Template / Generic Assert ─ function createPenFromString(string $input): Pen { return new Pen(); } function initPen(?Pen &$pen): void { $pen = new Pen(); } class PenFactory { public static function create(?Pen &$pen): void { $pen = new Pen(); } } class PenBuilder { public function __construct(?Pen &$pen) { $pen = new Pen(); } } interface ScaffoldingEntityFinder { /** * @template T * @param class-string $class * @return T */ public function find(string $class): object; } class ScaffoldingEntityLocator implements ScaffoldingEntityFinder { public function find(string $class): object { return new $class(); } } class ScaffoldingAssert { /** * @template ExpectedType of object * @param class-string $expected * @phpstan-assert ExpectedType $actual */ public static function assertInstanceOf(string $expected, object $actual): void { if (!$actual instanceof $expected) { throw new \InvalidArgumentException('Type mismatch'); } } } // ─── Multi-line @return & Broken Docblock Recovery ────────────────────────── /** * @template TKey of array-key * @template TValue */ class FluentCollection { /** @var array */ private array $items; /** @param array $items */ public function __construct(array $items = []) { $this->items = $items; } /** * @template TGroupKey of array-key * * @param (callable(TValue, TKey): TGroupKey)|array|string $groupBy * @param bool $preserveKeys * @return static< * ($groupBy is (array|string) * ? array-key * : TGroupKey), * static<($preserveKeys is true ? TKey : int), TValue> * > */ public function groupBy($groupBy, $preserveKeys = false) { } /** * @template TMapValue * * @param callable(TValue, TKey): TMapValue $callback * @return static */ public function map(callable $callback) { } /** * @param callable(TValue, TKey): void $callback * @return static */ public function each(callable $callback) { foreach ($this->items as $key => $value) { $callback($value, $key); } return $this; } /** @return TValue|null */ public function first(): mixed { return $this->items[array_key_first($this->items)] ?? null; } /** * @return array< * string, * FluentCollection * > */ public function toGroupedArray() { } /** * @return static */ public function values() { } } /** * @template TKey of array-key * @template TValue * @param array $value * @return FluentCollection */ function collect(array $value = []): FluentCollection { return new FluentCollection($value); } class BrokenDocRecovery { /** * Broken multi-line @return — base `static` is recovered. * @return static< */ public function broken(): static { return $this; } public function working(): string { return 'hello'; } } // ── Runtime Assertions ────────────────────────────────────────────────────── // Verify that the type claims in demo comments match reality. // Run: php examples/demo.php function runDemoAssertions(): void { // ── Body Return Type Inference ────────────────────────────────────── $factory = new ScaffoldingUntypedFactory(); $pen = $factory->createPen(); assert($pen instanceof Pen, 'createPen() must return Pen (inferred from body)'); $tool = $factory->createTool(true); assert($tool instanceof Pen || $tool instanceof Pencil, 'createTool() must return Pen|Pencil'); // ── Return Type: static ───────────────────────────────────────────── $pen = Pen::make(); assert($pen instanceof Pen, 'Pen::make() must return Pen'); $marker = Marker::make(); assert($marker instanceof Marker, 'Marker::make() must return Marker (not Pen)'); $fluent = $marker->rename('Bold'); assert($fluent instanceof Marker, 'Marker::rename() returns static, must stay Marker'); // ── Return Type: function ─────────────────────────────────────────── $created = makePen(); assert($created instanceof Pen, 'makePen() must return Pen'); $union = pickPenOrPencil(); assert($union instanceof Pen || $union instanceof Pencil, 'pickPenOrPencil() must return Pen|Pencil'); $rock = pickRockOrBanana(); assert($rock instanceof Rock || $rock instanceof Banana, 'pickRockOrBanana() must return Rock|Banana'); $user = createUser('Alice', 'alice@example.com'); assert($user instanceof User, 'createUser() must return User'); // ── Chaining ──────────────────────────────────────────────────────── $brush = new Brush(); $sized = $brush->setSize('large'); assert($sized instanceof Brush, 'Brush::setSize() returns static, must stay Brush'); $styled = $sized->setStyle('pointed'); assert($styled instanceof Brush, 'Brush::setStyle() returns static, must stay Brush'); $canvas = $brush->getCanvas(); assert($canvas instanceof Canvas, 'Brush::getCanvas() must return Canvas'); $backToBrush = $canvas->getBrush(); assert($backToBrush instanceof Brush, 'Canvas::getBrush() must return Brush'); $easel = $canvas->easel; assert($easel instanceof Easel, 'Canvas::$easel must be Easel'); // ── Fluent Model chains (static return) ───────────────────────────── $userObj = new User('Bob', 'bob@example.com'); $renamed = $userObj->setName('Robert'); assert($renamed instanceof User, 'User::setName() returns static, must stay User'); $timestamped = $userObj->setCreatedAt('2024-01-01'); assert($timestamped instanceof User, 'HasTimestamps::setCreatedAt() returns static, must stay User'); // ── User method return types ──────────────────────────────────────── $profile = $userObj->getProfile(); assert($profile instanceof UserProfile, 'User::getProfile() must return UserProfile'); $status = $userObj->getStatus(); assert($status instanceof Status, 'User::getStatus() must return Status'); // ── Type narrowing: instanceof ────────────────────────────────────── $specimen = pickRockOrBanana(); if ($specimen instanceof Rock) { assert(method_exists($specimen, 'crush'), 'Rock must have crush()'); } else { assert($specimen instanceof Banana, 'Not Rock must be Banana'); assert(method_exists($specimen, 'peel'), 'Banana must have peel()'); } // ── Type narrowing: inline && ─────────────────────────────────────── $sample = pickRockOrBanana(); if ($sample instanceof Rock && $sample->crush()) { assert($sample instanceof Rock, 'RHS of && must see Rock'); } // ── Type narrowing: negated instanceof ────────────────────────────── $specimen2 = pickRockOrBanana(); if (!$specimen2 instanceof Rock) { assert($specimen2 instanceof Banana, 'Not Rock must be Banana'); } // ── Type narrowing: assert() ──────────────────────────────────────── $target = pickRockOrBanana(); if ($target instanceof Banana) { assert(method_exists($target, 'peel'), 'assert narrowed Banana must have peel()'); } // ── Custom assert functions ───────────────────────────────────────── $unknown = new Rock(); assertRock($unknown); assert($unknown instanceof Rock, 'assertRock() must narrow to Rock'); assert(isRock(new Rock()) === true, 'isRock(Rock) must return true'); assert(isRock(new Banana()) === false, 'isRock(Banana) must return false'); assert(isNotRock(new Rock()) === false, 'isNotRock(Rock) must return false'); assert(isNotRock(new Banana()) === true, 'isNotRock(Banana) must return true'); // ── Static assert functions ───────────────────────────────────────── $unknown2 = new Rock(); StaticAssert::assertRock($unknown2); assert($unknown2 instanceof Rock, 'StaticAssert::assertRock() must narrow to Rock'); assert(StaticAssert::isRock(new Rock()) === true, 'StaticAssert::isRock(Rock) must return true'); assert(StaticAssert::isNotRock(new Banana()) === true, 'StaticAssert::isNotRock(Banana) must return true'); // ── Null-init + foreach reassignment (B11) ────────────────────────── $pens = [new Pen('blue'), new Pen('red')]; $found = null; foreach ($pens as $pen) { if ($pen->color() === 'blue') { $found = $pen; } } assert($found instanceof Pen, 'Null-init + foreach reassign must resolve to Pen'); assert(method_exists($found, 'write'), 'Pen from foreach must have write()'); // ── instanceof self/static/parent ─────────────────────────────────── $sedan = new ScaffoldingSedan(); assert($sedan instanceof ScaffoldingMotor, 'ScaffoldingSedan must extend ScaffoldingMotor'); assert(method_exists($sedan, 'cruise'), 'ScaffoldingSedan must have cruise()'); assert(method_exists($sedan, 'start'), 'ScaffoldingSedan must inherit start()'); $demo = new InstanceofSelfDemo(); assert($demo instanceof ScaffoldingSedan, 'InstanceofSelfDemo must extend ScaffoldingSedan'); assert(method_exists($demo, 'sport'), 'InstanceofSelfDemo must have sport()'); assert(method_exists($demo, 'cruise'), 'InstanceofSelfDemo must inherit cruise()'); // ── Method-level @template (runtime resolution) ───────────────────── $locator = new ServiceLocator(); $locatedPen = $locator->get(Pen::class); assert($locatedPen instanceof Pen, 'ServiceLocator::get(Pen::class) must return Pen'); $createdPen = Factory::create(Pen::class); assert($createdPen instanceof Pen, 'Factory::create(Pen::class) must return Pen'); $resolved = resolve(Marker::class); assert($resolved instanceof Marker, 'resolve(Marker::class) must return Marker'); // ── ObjectMapper::wrap() → TypedCollection ────────────────────────── $mapper = new ObjectMapper(); $wrapped = $mapper->wrap(new Pen()); assert($wrapped instanceof TypedCollection, 'ObjectMapper::wrap() must return TypedCollection'); $first = $wrapped->first(); assert($first instanceof Pen, 'wrap(Pen)->first() must return Pen'); // ── ScaffoldingReducible::reduce() — closure return type binding ──── /** @var ScaffoldingReducible $reducible */ $reducible = new ScaffoldingReducible(); $reduced = $reducible->reduce( fn(Pen $carry, Pencil $item): Pen => $carry, new Pen('starter') ); assert($reduced instanceof Pen, 'reduce() with fn(): Pen must return Pen'); // Chained call: reduce() result used directly without intermediate variable. $chainedWrite = $reducible->reduce(fn(Pen $carry, Pencil $item): Pen => $carry, new Pen('starter'))->write(); assert(is_string($chainedWrite), 'reduce()->write() chained must return string (Pen::write() return type)'); // ── ScaffoldingEventBus::listen() — closure param type binding ────── $bus = new ScaffoldingEventBus(); $listened = $bus->listen(function(Pen $p): void { $p->write(); }); assert($listened instanceof Pen, 'listen(fn(Pen $p)) must return Pen (T inferred from closure param)'); $listenedUser = $bus->listen(function(User $u): void { $u->getEmail(); }); assert($listenedUser instanceof User, 'listen(function(User $u)) must return User'); // ── ScaffoldingBatchProcessor::process() — second closure param ───── $proc = new ScaffoldingBatchProcessor(); $processed = $proc->process(function(int $i, Pencil $p): void { $p->sketch(); }); assert($processed instanceof Pencil, 'process(fn(int, Pencil)) must return Pencil (T from position 1)'); // ── Nested generic: ServiceLocator::wrap → Box::unwrap ────────────── $boxed = $locator->wrap(Pen::class); assert($boxed instanceof Box, 'ServiceLocator::wrap() must return Box'); $unboxed = $boxed->unwrap(); assert($unboxed instanceof Pen, 'Box::unwrap() must return Pen (from wrap(Pen::class))'); // ── __invoke() return types ───────────────────────────────────────── $formatter = new ScaffoldingFormatter(); $invoked = $formatter(); assert($invoked instanceof Pen, 'ScaffoldingFormatter::__invoke() must return Pen'); $factory = new ScaffoldingPenFactory(); $factoryResult = $factory(); assert($factoryResult instanceof Pen, 'ScaffoldingPenFactory::__invoke() must return Pen'); // ── Enum from() ───────────────────────────────────────────────────── $active = Status::from('active'); assert($active instanceof Status, 'Status::from() must return Status'); assert($active === Status::Active, 'Status::from("active") must be Status::Active'); // ── Clone preserves type ──────────────────────────────────────────── $original = new Pen('blue'); $copy = clone $original; assert($copy instanceof Pen, 'clone must preserve Pen type'); assert($copy !== $original, 'clone must be a different instance'); // ── class-string variable → new $var ──────────────────────────────── $cls = Pen::class; $fromClassString = new $cls(); assert($fromClassString instanceof Pen, 'new $cls where $cls = Pen::class must be Pen'); // ── Zoo: inheritance, traits, promoted properties ──────────────────── $zoo = new Zoo(); assert($zoo instanceof Zoo, 'new Zoo() must be Zoo'); assert($zoo instanceof ZooBase, 'Zoo must extend ZooBase'); assert(method_exists($zoo, 'aardvark'), 'Zoo must have own method aardvark()'); assert(method_exists($zoo, 'dingo'), 'Zoo must have trait method dingo()'); assert(method_exists($zoo, 'elephant'), 'Zoo must have trait method elephant()'); assert(method_exists($zoo, 'falcon'), 'Zoo must have inherited method falcon()'); // @property and @method via __get/__call assert($zoo->gorilla === 'gorilla-value', '@property $gorilla must work via __get'); assert($zoo->iguana === 'iguana-value', '@property-read $iguana (ZooContract) must work via __get'); assert($zoo->hyena('x') === true, '@method hyena() must work via __call'); assert($zoo->jaguar() === 'jaguar-value', '@method jaguar() (ZooContract) must work via __call'); // Visibility: protected/private must not be accessible assert(property_exists($zoo, 'baboon'), 'Zoo must have public $baboon'); assert((new \ReflectionProperty($zoo, 'keeper'))->isProtected(), '$keeper must be protected'); assert((new \ReflectionProperty($zoo, 'ceo'))->isPrivate(), '$ceo must be private'); assert((new \ReflectionMethod($zoo, 'nocturnal'))->isPrivate(), 'nocturnal() must be private'); // ── Expression types: null-coalescing ──────────────────────────────── $src = new ScaffoldingExpressionType(); $fallback = $src->backup ?? $src->primary; assert($fallback instanceof Response, 'Null-coalescing must resolve to Response'); // ── ChainingDemo scaffolding ──────────────────────────────────────── $studio = new ScaffoldingChainingDemo(); assert($studio->brush instanceof Brush, 'ScaffoldingChainingDemo::$brush must be Brush'); assert($studio->canvas instanceof Canvas, 'ScaffoldingChainingDemo::$canvas must be Canvas'); // ── Trait conflict resolution ─────────────────────────────────────── $tc = new TraitConflictDemo(); assert(method_exists($tc, 'serialize'), 'TraitConflictDemo must have serialize()'); assert(method_exists($tc, 'toJson'), 'TraitConflictDemo must have toJson()'); assert(method_exists($tc, 'toXml'), 'TraitConflictDemo must have toXml()'); // ── AdminUser extends User extends Model ──────────────────────────── $admin = new AdminUser('Admin', 'admin@example.com'); assert($admin instanceof AdminUser, 'new AdminUser() must be AdminUser'); assert($admin instanceof User, 'AdminUser must extend User'); assert($admin instanceof Model, 'AdminUser must extend Model (via User)'); // ── ClassFilteringDemo extends Model implements Renderable ─────────── $cfd = new ClassFilteringDemo(); assert($cfd instanceof Model, 'ClassFilteringDemo must extend Model'); assert($cfd instanceof Renderable, 'ClassFilteringDemo must implement Renderable'); // ── Inline new chaining ───────────────────────────────────────────── $fromNew = (new Canvas())->getBrush(); assert($fromNew instanceof Brush, '(new Canvas())->getBrush() must be Brush'); // ── Parenthesized assignment ──────────────────────────────────────── $parenPen = (new Pen('red')); assert($parenPen instanceof Pen, 'Parenthesized new must still be Pen'); // ── Constructor @param override (ParamOverrideDemo) ───────────────── $ingredient = new Ingredient(); assert($ingredient instanceof Ingredient, 'new Ingredient() must be Ingredient'); assert(property_exists($ingredient, 'name'), 'Ingredient must have $name'); $recipe = new Recipe('Test', [new Ingredient()]); assert($recipe instanceof Recipe, 'new Recipe() must be Recipe'); // ── Inline @var on promoted property (InlineVarPromotedDemo) ──────── $inlineDemo = new InlineVarPromotedDemo([new Ingredient()]); assert(is_array($inlineDemo->ingredients), 'InlineVarPromotedDemo->ingredients must be array'); assert($inlineDemo->ingredients[0] instanceof Ingredient, 'InlineVarPromotedDemo->ingredients[0] must be Ingredient'); // ── Container / app() conditional return types ────────────────────── $container = new Container(); $containerPen = $container->make(Pen::class); assert($containerPen instanceof Pen, 'Container::make(Pen::class) must return Pen'); $appPen = app(Pen::class); assert($appPen instanceof Pen, 'app(Pen::class) must return Pen'); $appSelf = app(); assert($appSelf instanceof Container, 'app() with no args must return Container'); // ── Closure / arrow function return types ─────────────────────────── $makePenClosure = function(): Pen { return new Pen(); }; assert($makePenClosure() instanceof Pen, 'Closure returning Pen must return Pen'); $makePencilArrow = fn(): Pencil => new Pencil(); assert($makePencilArrow() instanceof Pencil, 'Arrow fn returning Pencil must return Pencil'); $builder = function(): Pen { return new Pen(); }; $chained = $builder()->rename('Bold'); assert($chained instanceof Pen, 'Closure()->rename() must chain to Pen'); // ── Closure members ───────────────────────────────────────────────── $typedClosure = function(Pen $pen): string { return $pen->write(); }; assert(method_exists($typedClosure, 'bindTo'), 'Closure must have bindTo()'); assert(method_exists($typedClosure, 'call'), 'Closure must have call()'); assert($typedClosure instanceof \Closure, 'Function expression must be Closure'); $typedArrow = fn(int $x): float => $x * 1.5; assert($typedArrow instanceof \Closure, 'Arrow function must be Closure'); // ── Enum methods and properties ───────────────────────────────────── $activeStatus = Status::Active; assert($activeStatus instanceof Status, 'Status::Active must be Status'); assert($activeStatus->name === 'Active', 'Status::Active->name must be "Active"'); assert($activeStatus->value === 'active', 'Status::Active->value must be "active"'); assert($activeStatus->label() === 'Active', 'Status::Active->label() must return "Active"'); assert($activeStatus->isActive() === true, 'Status::Active->isActive() must be true'); $pending = Status::Pending; assert($pending->isActive() === false, 'Status::Pending->isActive() must be false'); $high = Priority::High; assert($high instanceof Priority, 'Priority::High must be Priority'); assert($high->name === 'High', 'Priority::High->name must be "High"'); assert($high->value === 3, 'Priority::High->value must be 3'); $manual = Mode::Manual; assert($manual instanceof Mode, 'Mode::Manual must be Mode'); assert($manual->name === 'Manual', 'Mode::Manual->name must be "Manual"'); $fromString = Status::from('active'); assert($fromString === Status::Active, 'Status::from("active") must be Status::Active'); $tryFrom = Status::tryFrom('nonexistent'); assert($tryFrom === null, 'Status::tryFrom("nonexistent") must be null'); $defaultVal = Status::defaultValue(); assert($defaultVal === 'active', 'Status::defaultValue() must return "active" (self::Active->value)'); // ── Response methods ──────────────────────────────────────────────── $response = new Response(200, 'OK'); assert($response->getStatusCode() === 200, 'Response::getStatusCode() must return 200'); assert($response->getBody() === 'OK', 'Response::getBody() must return "OK"'); assert($response->isSuccess() === true, 'Response(200) must be success'); $errResponse = new Response(500); assert($errResponse->isSuccess() === false, 'Response(500) must not be success'); // ── UserProfile methods ───────────────────────────────────────────── $userForProfile = new User('Eve', 'eve@example.com'); $prof = $userForProfile->getProfile(); assert($prof instanceof UserProfile, 'User::getProfile() must return UserProfile'); assert(method_exists($prof, 'getDisplayName'), 'UserProfile must have getDisplayName()'); assert(method_exists($prof, 'setBio'), 'UserProfile must have setBio()'); $bioResult = $prof->setBio('Hello'); assert($bioResult instanceof UserProfile, 'UserProfile::setBio() returns static'); // ── Generator yield types ─────────────────────────────────────────── $genDemo = new GeneratorDemo(); $gen = $genDemo->getPens(); assert($gen instanceof \Generator, 'getPens() must return Generator'); foreach ($gen as $genPen) { assert($genPen instanceof Pen, 'Generator must yield Pen'); break; } $pencilGen = $genDemo->processPencils(); foreach ($pencilGen as $genPencil) { assert($genPencil instanceof Pencil, 'Generator must yield Pencil'); break; } // ── Generator yield inference (GeneratorYieldDemo) ─────────────────── $yieldDemo = new GeneratorYieldDemo(); foreach ($yieldDemo->findAll() as $yieldedPen) { assert($yieldedPen instanceof Pen, 'GeneratorYieldDemo::findAll() must yield Pen'); break; } foreach ($yieldDemo->chainingThroughYieldInferred() as $chainPen) { assert($chainPen instanceof Pen, 'chainingThroughYieldInferred() must yield Pen'); break; } $coroutineGen = $yieldDemo->coroutine(); $yielded = $coroutineGen->current(); assert($yielded === 'ready', 'coroutine() must yield string (TValue)'); $coroutineGen->send(new Pencil()); // ── GenericContext: Box and TypedCollection ───────── $gcSrc = new ScaffoldingGenericContext(); $unwrapped = $gcSrc->chest->unwrap(); assert($unwrapped instanceof Gift, 'Box::unwrap() must return Gift'); $displayFirst = $gcSrc->display()->first(); assert($displayFirst instanceof Gift, 'TypedCollection::first() must return Gift'); // ── CompoundNegatedNarrowing ──────────────────────────────────────── $compoundRock = new Rock(); $compoundDemo = new CompoundNegatedNarrowingDemo(); // Rock passes both negated checks (is Rock, is not "not Rock") // so it doesn't return early — weigh() must exist assert(method_exists($compoundRock, 'weigh'), 'Rock must have weigh()'); $compoundBanana = new Banana(); assert(method_exists($compoundBanana, 'weigh'), 'Banana must have weigh()'); // Lamp would cause the early return — verify it lacks weigh() assert(!method_exists(new Lamp(), 'weigh'), 'Lamp must NOT have weigh()'); // ── InArrayNarrowing ──────────────────────────────────────────────── $rockList = [new Rock()]; $testRock = new Rock(); assert(in_array($testRock, $rockList, true) === false, 'Different Rock instances are not strictly identical'); $sameRock = $rockList[0]; assert(in_array($sameRock, $rockList, true) === true, 'Same Rock instance must be in_array strict'); // ── MatchClassStringDemo: class-string through match → Container ──── $mcsContainer = new Container(); $mcsType = match (0) { 0 => ElasticProductReviewIndexService::class, 1 => ElasticBrandIndexService::class, }; $mcsResult = $mcsContainer->make($mcsType); assert($mcsResult instanceof ElasticProductReviewIndexService, 'Container::make(match class-string) must return the matched class'); assert(method_exists($mcsResult, 'index'), 'Match-resolved instance must have index()'); $mcsCls = Pen::class; $mcsPen = $mcsContainer->make($mcsCls); assert($mcsPen instanceof Pen, 'Container::make(Pen::class via variable) must return Pen'); $mcsTernary = true ? Pen::class : Pencil::class; $mcsObj = $mcsContainer->make($mcsTernary); assert($mcsObj instanceof Pen, 'Container::make(ternary class-string) must return Pen'); // ── ExceptionDemo: exception hierarchy ────────────────────────────── assert(is_subclass_of(NotFoundException::class, \RuntimeException::class), 'NotFoundException must extend RuntimeException'); assert(is_subclass_of(ValidationException::class, \RuntimeException::class), 'ValidationException must extend RuntimeException'); assert(is_subclass_of(AuthorizationException::class, \RuntimeException::class), 'AuthorizationException must extend RuntimeException'); try { throw new ValidationException('test'); } catch (ValidationException $e) { assert($e instanceof ValidationException, 'Caught exception must be ValidationException'); assert($e->getMessage() === 'test', 'Exception message must propagate'); } // ── Closure parameter inference ───────────────────────────────────── $closureSrc = new ScaffoldingClosureParamInference(); $closureReceived = []; $closureSrc->items->each(function ($pen) use (&$closureReceived) { assert($pen instanceof Pen, 'Closure param from FluentCollection::each() must be Pen'); $closureReceived[] = $pen; }); assert(count($closureReceived) === 2, 'each() must invoke callback for every item'); // Function-level @template callable inference (array_any pattern) $tplHolder = new ScaffoldingTemplateCallableHolder(); $tplHolder->tools = [new Pen('red'), new Pen('blue')]; $tplResult = array_any($tplHolder->tools, fn($t) => $t->color() === 'red'); assert($tplResult === true, 'array_any with template callable must work'); // ── Type alias resolution ─────────────────────────────────────────── $aliasDemo = new TypeAliasDemo(); $userData = $aliasDemo->getUserData(); assert(is_string($userData['name']), 'UserData["name"] must be string'); assert($userData['pen'] instanceof Pen, 'UserData["pen"] must be Pen'); $statusInfo = $aliasDemo->getStatus(); assert(is_int($statusInfo['code']), 'StatusInfo["code"] must be int'); assert($statusInfo['owner'] instanceof User, 'StatusInfo["owner"] must be User'); $importDemo = new TypeAliasImportDemo(); $imported = $importDemo->fetchUser(); assert($imported['pen'] instanceof Pen, 'Imported UserData["pen"] must be Pen'); $importedStatus = $importDemo->fetchStatus(); assert($importedStatus['owner'] instanceof User, 'Imported StatusInfo["owner"] must be User'); // ── String interpolation ──────────────────────────────────────────── $interpPen = new Pen('blue'); ob_start(); echo "Ink is {$interpPen->color()}"; $braceOutput = ob_get_clean(); assert($braceOutput === 'Ink is blue', 'Brace interpolation must call method: got ' . $braceOutput); ob_start(); echo "Tool: $interpPen->ink"; $simpleOutput = ob_get_clean(); assert($simpleOutput === 'Tool: blue', 'Simple interpolation must access property: got ' . $simpleOutput); ob_start(); echo 'no $interpPen-> here'; $singleOutput = ob_get_clean(); assert($singleOutput === 'no $interpPen-> here', 'Single-quoted must stay literal: got ' . $singleOutput); // ── Diagnostics: class/method/property existence ──────────────────── // These verify the claims made by the UnknownMemberDemo and related demos. assert(class_exists(User::class), 'User class must exist'); assert(class_exists(Pen::class), 'Pen class must exist'); assert(class_exists(Model::class), 'Model class must exist'); assert(class_exists(AdminUser::class), 'AdminUser class must exist'); assert(interface_exists(Renderable::class), 'Renderable interface must exist'); assert(trait_exists(HasTimestamps::class), 'HasTimestamps trait must exist'); assert(trait_exists(HasSlug::class), 'HasSlug trait must exist'); assert(enum_exists(Status::class), 'Status enum must exist'); assert(enum_exists(Priority::class), 'Priority enum must exist'); // User members that demos reference assert(method_exists(User::class, 'getEmail'), 'User must have getEmail()'); assert(method_exists(User::class, 'getName'), 'User must have getName() (inherited)'); assert(method_exists(User::class, 'getProfile'), 'User must have getProfile()'); assert(method_exists(User::class, 'getStatus'), 'User must have getStatus()'); assert(method_exists(User::class, 'setName'), 'User must have setName() (inherited)'); assert(method_exists(User::class, 'findByEmail'), 'User must have static findByEmail()'); assert(method_exists(User::class, 'hashPassword'), 'User must have static hashPassword()'); assert(property_exists(User::class, 'email'), 'User must have $email'); assert(property_exists(User::class, 'defaultRole'), 'User must have static $defaultRole'); // UnknownMemberDemo: nonexistentMethod must NOT exist assert(!method_exists(User::class, 'nonexistentMethod'), 'User must NOT have nonexistentMethod()'); // Pen members assert(method_exists(Pen::class, 'write'), 'Pen must have write()'); assert(method_exists(Pen::class, 'color'), 'Pen must have color()'); assert(method_exists(Pen::class, 'label'), 'Pen must have label()'); assert(method_exists(Pen::class, 'rename'), 'Pen must have rename()'); assert(method_exists(Pen::class, 'make'), 'Pen must have static make()'); // Marker extends Pen assert(method_exists(Marker::class, 'highlight'), 'Marker must have highlight()'); assert(method_exists(Marker::class, 'write'), 'Marker must inherit write() from Pen'); // Pencil members assert(method_exists(Pencil::class, 'sketch'), 'Pencil must have sketch()'); assert(method_exists(Pencil::class, 'sharpen'), 'Pencil must have sharpen()'); // Rock and Banana members (narrowing demos rely on these) assert(method_exists(Rock::class, 'crush'), 'Rock must have crush()'); assert(method_exists(Rock::class, 'weigh'), 'Rock must have weigh()'); assert(!method_exists(Rock::class, 'peel'), 'Rock must NOT have peel()'); assert(method_exists(Banana::class, 'peel'), 'Banana must have peel()'); assert(method_exists(Banana::class, 'weigh'), 'Banana must have weigh()'); assert(!method_exists(Banana::class, 'crush'), 'Banana must NOT have crush()'); // ── Array functions preserve types ─────────────────────────────────── $penArray = [new Pen('red'), new Pen('blue'), new Pen('green')]; $filtered = array_filter($penArray, fn(Pen $p) => $p->color() === 'blue'); assert(count($filtered) === 1, 'array_filter must filter correctly'); assert(reset($filtered) instanceof Pen, 'array_filter must preserve Pen type'); $vals = array_values($penArray); assert($vals[0] instanceof Pen, 'array_values must preserve Pen type'); $popped = array_pop($penArray); assert($popped instanceof Pen, 'array_pop must return Pen'); $penArray2 = [new Pen('a'), new Pen('b')]; $cur = current($penArray2); assert($cur instanceof Pen, 'current() must return Pen'); $last = end($penArray2); assert($last instanceof Pen, 'end() must return Pen'); $reduced = array_reduce($penArray2, function(Pen $carry, Pen $item): Pen { return $carry; }, new Pen('merged')); assert($reduced instanceof Pen, 'array_reduce must return type of initial value'); $sum = array_sum([10, 20, 30]); assert(is_int($sum) || is_float($sum), 'array_sum must return int or float'); $product = array_product([2, 3, 4]); assert(is_int($product) || is_float($product), 'array_product must return int or float'); // ── Match expression types ────────────────────────────────────────── $matchResult = match (0) { 0 => new ElasticProductReviewIndexService(), 1 => new ElasticBrandIndexService(), }; assert($matchResult instanceof ElasticProductReviewIndexService || $matchResult instanceof ElasticBrandIndexService, 'Match expression must return one of the branch types'); assert(method_exists($matchResult, 'index'), 'Match result must have shared index() method'); // ── Ternary expression types ──────────────────────────────────────── $ternaryResult = true ? new ElasticProductReviewIndexService() : new ElasticBrandIndexService(); assert(method_exists($ternaryResult, 'index'), 'Ternary result must have shared index() method'); // ── Intersection types ────────────────────────────────────────────── // Can't instantiate an intersection directly, but we can verify interfaces assert(method_exists(Envelope::class, 'seal'), 'Envelope must have seal()'); assert(interface_exists(Printable::class), 'Printable must be an interface'); // ── First-class callable syntax ───────────────────────────────────── $fun = makePen(...); assert($fun instanceof \Closure, 'makePen(...) must be a Closure'); $funResult = $fun(); assert($funResult instanceof Pen, 'makePen(...)() must return Pen'); $staticCallable = Pen::make(...); assert($staticCallable instanceof \Closure, 'Pen::make(...) must be a Closure'); $staticResult = $staticCallable(); assert($staticResult instanceof Pen, 'Pen::make(...)() must return Pen'); $src2 = new ScaffoldingFirstClassCallable(); $methodCallable = $src2->dispatch(...); assert($methodCallable instanceof \Closure, '$obj->method(...) must be a Closure'); $methodResult = $methodCallable(); assert($methodResult instanceof Pen, 'dispatch(...)() must return Pen'); // Immediate invocation: method(...)() returns the method's return type $immediateFunc = makePen(...)(); assert($immediateFunc instanceof Pen, 'makePen(...)() immediate must return Pen'); $immediateStatic = Pen::make(...)(); assert($immediateStatic instanceof Pen, 'Pen::make(...)() immediate must return Pen'); $immediateMethod = $src2->dispatch(...)(); assert($immediateMethod instanceof Pen, '$obj->dispatch(...)() immediate must return Pen'); // ── Class alias (use ... as) ──────────────────────────────────────── $aliasProfile = new Profile($userForProfile); assert($aliasProfile instanceof UserProfile, 'Profile alias must be UserProfile'); assert($aliasProfile instanceof Profile, 'Profile alias instanceof must work'); // ── HoverOriginsDemo extends Model implements Renderable ──────────── $hod = new HoverOriginsDemo(); assert($hod instanceof Model, 'HoverOriginsDemo must extend Model'); assert($hod instanceof Renderable, 'HoverOriginsDemo must implement Renderable'); assert(method_exists($hod, 'format'), 'HoverOriginsDemo must have format()'); assert(method_exists($hod, 'toArray'), 'HoverOriginsDemo must have toArray()'); assert(method_exists($hod, 'getName'), 'HoverOriginsDemo must inherit getName()'); // ── Switch statement type tracking ────────────────────────────────── $switchType = 'reviews'; switch ($switchType) { case 'reviews': $switchService = new ElasticProductReviewIndexService(); break; default: $switchService = new ElasticBrandIndexService(); break; } assert(method_exists($switchService, 'index'), 'Switch-assigned variable must have index()'); // ── Spread operator ───────────────────────────────────────────────── $spreadSource = [new Pen('a'), new Pen('b')]; $spread = [...$spreadSource]; assert($spread[0] instanceof Pen, 'Spread must preserve Pen type'); assert(count($spread) === 2, 'Spread must preserve array length'); $pencilSource = [new Pencil()]; $mixed = [...$spreadSource, ...$pencilSource]; assert($mixed[0] instanceof Pen || $mixed[0] instanceof Pencil, 'Multi-spread must contain Pen|Pencil'); // ── Array destructuring ───────────────────────────────────────────── $destructSource = [new Pen('x'), new Pen('y')]; [$dFirst, $dSecond] = $destructSource; assert($dFirst instanceof Pen, 'Destructured element must be Pen'); assert($dSecond instanceof Pen, 'Second destructured element must be Pen'); // ── Named key destructuring from shape ────────────────────────────── $shapeSource = ['pen' => new Pen(), 'pencil' => new Pencil()]; ['pen' => $dPen, 'pencil' => $dPencil] = $shapeSource; assert($dPen instanceof Pen, 'Named destructured pen must be Pen'); assert($dPencil instanceof Pencil, 'Named destructured pencil must be Pencil'); // ── Nested destructuring ──────────────────────────────────────────── /** @var array{string, array{Pen, Pencil}} $nestedDestr */ $nestedDestr = ['label', [new Pen(), new Pencil()]]; [$nLabel, [$nPen, $nPencil]] = $nestedDestr; assert(is_string($nLabel), 'Nested destructured label must be string'); assert($nPen instanceof Pen, 'Nested destructured pen must be Pen'); assert($nPencil instanceof Pencil, 'Nested destructured pencil must be Pencil'); // ── Foreach destructuring ─────────────────────────────────────────── /** @var array $foreachDestrInv */ $foreachDestrInv = [['tool' => new Pen(), 'count' => 5]]; foreach ($foreachDestrInv as ['tool' => $fTool, 'count' => $fCount]) { assert($fTool instanceof Pen, 'Foreach destructured tool must be Pen'); assert(is_int($fCount), 'Foreach destructured count must be int'); } // ── Ambiguous variables ───────────────────────────────────────────── if (rand(0, 1)) { $ambiguous = new Lamp(); } else { $ambiguous = new Faucet(); } assert($ambiguous instanceof Lamp || $ambiguous instanceof Faucet, 'Ambiguous var must be Lamp|Faucet'); assert(method_exists($ambiguous, 'turnOff'), 'Both Lamp and Faucet have turnOff()'); // ── Guard clause narrowing ────────────────────────────────────────── $guardSubject = pickRockOrBanana(); if (!$guardSubject instanceof Banana) { // would return in real code; just verify type assert($guardSubject instanceof Rock, 'Guard: not Banana must be Rock'); } else { assert($guardSubject instanceof Banana, 'Guard: else must be Banana'); } // ── Guard clause: positive instanceof + early return on mixed ──── // After `if ($x instanceof Y) { return; }`, $x is NOT Y. $mixedGuardVal = rand(0, 1) ? new Rock() : 'scalar'; if ($mixedGuardVal instanceof Banana) { // would return in real code assert(false, 'Guard: should not reach here (Banana branch)'); } // $mixedGuardVal is NOT Banana after the guard if ($mixedGuardVal instanceof Rock) { assert(is_string($mixedGuardVal->crush()), 'Guard: mixed narrowed to Rock'); } // ── Null coalesce refinement ──────────────────────────────────────── $ncA = new Pen() ?? new Marker(); assert($ncA instanceof Pen, 'Null coalesce: non-nullable LHS must be Pen'); $ncNullable = rand(0, 1) ? new Pen() : null; $ncB = $ncNullable ?? new Marker(); assert($ncB instanceof Pen || $ncB instanceof Marker, 'Null coalesce: nullable LHS must be Pen or Marker'); $ncClone = clone new Pen() ?? new Marker(); assert($ncClone instanceof Pen, 'Null coalesce: clone LHS must be Pen'); // ── Ternary narrowing ─────────────────────────────────────────────── $ternaryThing = pickRockOrBanana(); $ternaryResult2 = $ternaryThing instanceof Rock ? $ternaryThing->crush() : $ternaryThing->peel(); assert(is_string($ternaryResult2), 'Ternary narrowed call must return string'); // ── User::toArray() ───────────────────────────────────────────────── $userArr = (new User('Test', 'test@example.com'))->toArray(); assert(is_array($userArr), 'User::toArray() must return array'); // ── AstNode (template bounds) ─────────────────────────────────────── $astNode = new AstNode(); assert($astNode->getType() === '' || is_string($astNode->getType()), 'AstNode::getType() must return string'); $children = $astNode->getChildren(); assert(is_array($children), 'AstNode::getChildren() must return array'); // ── Pass-by-reference parameter type ──────────────────────────────── $refPen = null; initPen($refPen); assert($refPen instanceof Pen, 'initPen(&$pen) must give $pen type Pen'); $staticPen = null; PenFactory::create($staticPen); assert($staticPen instanceof Pen, 'PenFactory::create(&$pen) must give $pen type Pen'); $ctorPen = null; new PenBuilder($ctorPen); assert($ctorPen instanceof Pen, 'new PenBuilder(&$pen) must give $pen type Pen'); // ── Interface template inheritance (class-string) ──────────────── $locator = new ScaffoldingEntityLocator(); $locatorResult = $locator->find(Pen::class); assert($locatorResult instanceof Pen, 'ScaffoldingEntityLocator::find(Pen::class) must return Pen'); // ── Function-level @template (collect) ────────────────────────────── $collectPens = [new Pen()]; $collected = collect($collectPens); assert($collected instanceof FluentCollection, 'collect() must return FluentCollection'); $firstPen = $collected->first(); assert($firstPen instanceof Pen, 'collect(Pen[])->first() must return Pen'); // ── Generic @phpstan-assert narrowing ──────────────────────────────── $assertObj = new Pen(); ScaffoldingAssert::assertInstanceOf(Pen::class, $assertObj); assert($assertObj instanceof Pen, 'ScaffoldingAssert::assertInstanceOf(Pen::class, $obj) must narrow to Pen'); // ── @param-closure-this scaffolding ────────────────────────────────── $ctRoute = new ScaffoldingClosureThisRoute(); $ctMw = $ctRoute->middleware('auth'); assert($ctMw instanceof ScaffoldingClosureThisRoute, 'Route::middleware() must return self'); $ctPfx = $ctRoute->prefix('/api'); assert($ctPfx instanceof ScaffoldingClosureThisRoute, 'Route::prefix() must return self'); $ctRouter = new ScaffoldingClosureThisRouter(); assert(is_string($ctRouter->getDefaultDriver()), 'Router::getDefaultDriver() must return string'); $ctExt = $ctRouter->extend('redis', function () {}); assert($ctExt instanceof ScaffoldingClosureThisRouter, 'Router::extend() must return self'); // ── @mixin generic substitution scaffolding ───────────────────────── $mixinBuilder = new ScaffoldingMixinBuilder(); assert($mixinBuilder->firstOrFail() === null, 'ScaffoldingMixinBuilder::firstOrFail() must return mixed'); $mixinRelation = new ScaffoldingMixinRelation(); assert($mixinRelation instanceof ScaffoldingMixinRelation, 'ScaffoldingMixinRelation instantiates'); $mixinBelongsTo = new ScaffoldingMixinBelongsTo(); assert($mixinBelongsTo instanceof ScaffoldingMixinRelation, 'ScaffoldingMixinBelongsTo extends ScaffoldingMixinRelation'); $orderLine = new ScaffoldingOrderLine(); $productRel = $orderLine->product(); assert($productRel instanceof ScaffoldingMixinBelongsTo, 'OrderLine::product() must return ScaffoldingMixinBelongsTo'); // ── @mixin with template parameter ────────────────────────────────── $tplMixinNode = new ScaffoldingConcreteAstNode(); $col = $tplMixinNode->getStartColumn(); assert(is_int($col), 'ConcreteAstNode (via @mixin TNode bound) getStartColumn() must return int'); // ── new $var() with class-string ───────────────────────────────── $penFromClassString = ScaffoldingClassStringFactory::create(Pen::class); assert($penFromClassString instanceof Pen, 'ClassStringFactory::create(Pen::class) must return Pen'); // ── Inherited docblock type propagation ───────────────────────────── $iHolder = new ScaffoldingConcreteHolder(); $iHolderPens = $iHolder->getPens(); assert(is_array($iHolderPens), 'ScaffoldingConcreteHolder::getPens() must return array'); assert($iHolderPens[0] instanceof Pen, 'ScaffoldingConcreteHolder::getPens()[0] must be Pen'); $iChild = new ScaffoldingChildHolder(); $iChildPens = $iChild->getPens(); assert(is_array($iChildPens), 'ScaffoldingChildHolder::getPens() must return array'); assert($iChildPens[0] instanceof Pen, 'ScaffoldingChildHolder::getPens()[0] must be Pen'); $iDeep = new ScaffoldingDeepChild(); $iDeepPens = $iDeep->getPens(); assert(is_array($iDeepPens), 'ScaffoldingDeepChild::getPens() must return array'); assert($iDeepPens[0] instanceof Pen, 'ScaffoldingDeepChild::getPens()[0] must be Pen'); $iCat = new ScaffoldingCatStore(); $iCatAnimals = $iCat->getAnimals(); assert(is_array($iCatAnimals), 'ScaffoldingCatStore::getAnimals() must return array'); assert($iCatAnimals[0] instanceof Pencil, 'ScaffoldingCatStore::getAnimals()[0] must be Pencil'); $iBox = new ScaffoldingPenBox(); $iBoxPens = $iBox->getPens(); assert(is_array($iBoxPens), 'ScaffoldingPenBox::getPens() must return array'); assert($iBoxPens[0] instanceof Pen, 'ScaffoldingPenBox::getPens()[0] must be Pen'); // ── Loop-carried assignment ───────────────────────────────────────── $lcPens = [new Pen('a'), new Pen('b')]; $lcPrev = null; foreach ($lcPens as $lcPen) { if ($lcPrev !== null) { assert($lcPrev instanceof Pen, 'Loop-carried $lcPrev must be Pen on second iteration'); } $lcPrev = $lcPen; } assert($lcPrev instanceof Pen, '$lcPrev must be Pen after foreach'); $lcLast = null; $lcIter = 0; while ($lcIter < 2) { if ($lcLast !== null) { assert($lcLast instanceof Response, 'Loop-carried $lcLast must be Response'); } $lcLast = new Response(200, 'ok'); $lcIter++; } assert($lcLast instanceof Response, '$lcLast must be Response after while'); // ── Constant type inference ───────────────────────────────────────── assert(ConstantTypeDemo::TIMEOUT === 30, 'ConstantTypeDemo::TIMEOUT must be 30'); assert(ConstantTypeDemo::NAME === 'app', 'ConstantTypeDemo::NAME must be "app"'); assert(ConstantTypeDemo::RATE === 3.14, 'ConstantTypeDemo::RATE must be 3.14'); assert(ConstantTypeDemo::ENABLED === true, 'ConstantTypeDemo::ENABLED must be true'); assert(CT_ALLOWED_HOSTS === ['localhost', '127.0.0.1'], 'CT_ALLOWED_HOSTS must match'); assert(CT_APP_VERSION === '2.0.0', 'CT_APP_VERSION must be "2.0.0"'); // ── Variadic foreach ──────────────────────────────────────────────── $vfDemo = new VariadicForeachDemo(); $vfPens = [new Pen('a'), new Pen('b')]; // demo() accepts Pen ...$pens — foreach inside should see Pen elements $vfDemo->demo(...$vfPens); foreach ($vfPens as $vfPen) { assert($vfPen instanceof Pen, 'Variadic Pen element must be Pen'); } $vfTools = [new Pen('x'), new Pencil()]; foreach ($vfTools as $vfTool) { assert( $vfTool instanceof Pen || $vfTool instanceof Pencil, 'Variadic union element must be Pen or Pencil' ); } // ── Type guard narrowing ──────────────────────────────────────────── /** @var list $tgPens */ $tgPens = [new Pen('a'), new Pen('b')]; /** @var null|list|Pen $tgInput */ $tgInput = $tgPens; if (is_array($tgInput)) { foreach ($tgInput as $tgPen) { assert($tgPen instanceof Pen, 'is_array() narrowed foreach element must be Pen'); } } $tgSingle = new Pen('solo'); /** @var list|Pen $tgMixed */ $tgMixed = $tgSingle; if (!is_array($tgMixed)) { assert($tgMixed instanceof Pen, 'Else branch of is_array() must be Pen'); } // ── Foreach array shape elements ──────────────────────────────────── /** @var array $fasInventory */ $fasInventory = [['tool' => new Pen('red'), 'count' => 3]]; foreach ($fasInventory as $fasEntry) { assert($fasEntry['tool'] instanceof Pen, 'Foreach over array shape must resolve key type'); } // ── Loop array build (variable-key assignment) ────────────────────── $labPens = [new Pen('red'), new Pen('blue')]; $labIndexed = []; foreach ($labPens as $labPen) { $labKey = $labPen->color(); $labIndexed[$labKey] = $labPen; } assert($labIndexed['red'] instanceof Pen, 'Variable-key array element must be Pen'); foreach ($labIndexed as $labItem) { assert($labItem instanceof Pen, 'Foreach over variable-key array must yield Pen'); } $labFound = $labIndexed['blue'] ?? null; assert($labFound instanceof Pen, 'Null-coalesce on variable-key array must resolve to Pen'); // ── Conditional shape key addition ────────────────────────────────── $cskOptions = ['name' => 'default']; $cskPen = new Pen('blue'); $cskOptions['tool'] = $cskPen; assert($cskOptions['tool'] instanceof Pen, 'Conditional shape key must resolve to Pen'); // ── Conditional loop shape (keyed assignment in if/else) ──────────── $shapePens = [new Pen('red'), new Pen('blue'), new Pen('red')]; $shapeGrouped = []; foreach ($shapePens as $shapePen) { $shapeKey = $shapePen->color(); if (array_key_exists($shapeKey, $shapeGrouped)) { $shapeGrouped[$shapeKey]['count']++; } else { $shapeGrouped[$shapeKey] = [ 'tool' => $shapePen, 'count' => 1, ]; } } foreach ($shapeGrouped as $shapeEntry) { assert($shapeEntry['tool'] instanceof Pen, 'Shape key from conditional loop must resolve to Pen'); } // ── Untyped property inference from constructor ───────────────────── $untypedDemo = new UntypedPropertyInferenceDemo(); // The scaffolding repo's findById() returns Pen, so we can verify // that the inferred type propagates through the property chain. $repoRef = new ScaffoldingUntypedRepo(); $found = $repoRef->findById(1); assert($found instanceof Pen, 'ScaffoldingUntypedRepo::findById() must return Pen'); // ── Deep variable chain ──────────────────────────────────────────── $chainBrush = new Brush(); $chainCanvas = $chainBrush->getCanvas(); assert($chainCanvas instanceof Canvas, 'Brush::getCanvas() must return Canvas'); $chainEasel = $chainCanvas->easel; assert($chainEasel instanceof Easel, 'Canvas::$easel must be Easel'); $chainMaterial = $chainEasel->material; assert(is_string($chainMaterial), 'Easel::$material must be string'); $chainBack = $chainCanvas->getBrush(); assert($chainBack instanceof Brush, 'Canvas::getBrush() must return Brush'); // ── Closure scope inference ──────────────────────────────────────── $scopePens = [new Pen()]; $scopeWorker = function () use ($scopePens): void { foreach ($scopePens as $sp) { assert($sp instanceof Pen, 'Captured $pens element must be Pen'); } }; $scopeWorker(); // ── Global keyword ───────────────────────────────────────────────── global $globalPen; assert($globalPen instanceof Pen, '$globalPen must be Pen at top level'); globalKeywordDemo(); // ── Built-in generic collections ──────────────────────────────────── $demo = new BuiltinGenericCollectionDemo(); $pen = $demo->getPens()->current(); assert($pen instanceof Pen, 'ArrayIterator::current() must return Pen'); echo "All assertions passed.\n"; } runDemoAssertions();