Class hierarchy resolver¶
Lucasp\Loom\Support\ClassHierarchyResolver answers transitive extends /
implements / use Trait questions across files in app/. It is an internal
support service; scanners consume it, no JSON output references it.
For the rationale behind the load-bearing choices (opaque leaves, eager walk, class-graph only), see ADR 0001. This page documents what the resolver does today.
Placement¶
- Class:
Lucasp\Loom\Support\ClassHierarchyResolver - File:
src/Support/ClassHierarchyResolver.php - Visitor:
src/Scanners/Visitors/ClassDeclarationVisitor.php - Constructed once per
IndexBuilder::build()call against the scanned$appRoot, alongsideAstWalkerandPsr4ClassLocator. Inject into scanner constructors that need it; do not share across builds.
Public API¶
final class ClassHierarchyResolver
{
public function __construct(string $appRoot, AstWalker $walker);
/** Immediate-to-root parents. Stops at first unknown FQCN. */
public function extendsChain(string $fqcn): array; // list<string>
/** Every interface implemented transitively (own + parents' + via interface extends). */
public function implementsAll(string $fqcn): array; // list<string>
/** Every trait used transitively (own + parents' + traits used by traits). */
public function traitsAll(string $fqcn): array; // list<string>
/** Convenience predicate: walks chain + interface graph + traits' implements. */
public function implementsInterface(string $fqcn, string $interface): bool;
/** Convenience predicate: extends-chain membership. */
public function isSubclassOf(string $fqcn, string $ancestor): bool;
/** Methods a class exposes after trait composition and inheritance, keyed by lower-cased name. */
public function effectiveMethods(string $fqcn): array; // array<string, ResolvedMethod>
/** A known class that is not abstract. False for interfaces, traits and unknown classes. */
public function isInstantiable(string $fqcn): bool;
/** Class names in a method's first parameter type; `self` and `parent` resolved. */
public function firstParameterClasses(ResolvedMethod $method): array; // list<string>
/** True when the resolver has indexed a declaration for $fqcn. */
public function knows(string $fqcn): bool;
}
Return semantics¶
- All FQCNs are returned without leading
\. Input FQCNs may carry a leading\and are normalised internally. - Order is deterministic: depth-first, parent before its own parents,
declaration order preserved within a level.
implementsAllreturns the direct interfaces first, then interfaces inherited from the parent class, then interfaces extended by interfaces, in encounter order. - Duplicates are removed; the first occurrence wins.
extendsChainon a class with no parent returns[]. On an unknown FQCN it also returns[]— useknows()to distinguish.implementsAll($interfaceFqcn)returns the interface-extends closure (including the interface itself).traitsAll($traitFqcn)returns the trait-use closure (including the trait itself).- Cycles in the inheritance graph are handled:
A extends B, B extends AyieldsextendsChain(A) === ['B', 'A']and terminates. PHP would fatal at runtime, but Loom must not loop.
Indexing¶
Two phases:
- Declaration index (eager, single walk). On first use, walk
$appRoot/app/**/*.phponce, parse each file via the sharedAstWalker+ClassDeclarationVisitor, and store everyStmt\Class_/Stmt\Interface_/Stmt\Trait_with anamespacedName. Anonymous classes are skipped. Multiple declarations in one file are all captured. - Resolution (lazy, memoised per query). Each public method memoises its result per FQCN. Repeat calls hit the cache.
External / vendor classes¶
Unknown FQCNs (anything not declared in app/) are opaque leaves: they
appear in the return list, but traversal stops at them. Callers match by
string. See ADR 0001 for the
rationale.
Example: a job App\Jobs\SendInvoice extends App\Jobs\AbstractInvoiceJob
implements \Illuminate\Contracts\Queue\ShouldQueue:
extendsChain('App\Jobs\SendInvoice')→['App\Jobs\AbstractInvoiceJob']implementsAll('App\Jobs\SendInvoice')→['Illuminate\Contracts\Queue\ShouldQueue']implementsInterface('App\Jobs\SendInvoice', '…\ShouldQueue')→trueknows('App\Jobs\AbstractInvoiceJob')→trueknows('Illuminate\Contracts\Queue\ShouldQueue')→false
Method resolution¶
effectiveMethods() follows PHP's precedence. For a class it starts from the
parent's effective methods (private ones are not inherited), overlays the
methods its traits provide, then overlays its own. Trait composition honours
insteadof (the excluded trait's method is dropped) and as (a new name, a
new visibility, or both; a visibility-only as changes the method in place and
handle as protected hides it). An abstract trait method never replaces a
concrete inherited one. Names are keyed lower-cased because PHP method names are
case-insensitive; ResolvedMethod::$name keeps the declared spelling.
Each ResolvedMethod records definedIn (the class or trait holding the code)
and declaredIn (the class self means: the using class for a trait method).
A vendor parent is an opaque leaf, so its methods are absent. Inheritance cycles
terminate with [].
Consumers¶
ListenerScanner— listener auto-discovery (publichandle*/__invokewith a first parameter) andqueuedthroughimplementsInterface().ObserverScanner— observer hooks fromeffectiveMethods().
Known limitations¶
- Methods only, no properties or constants. Nothing reads inherited
property values (a parent's
$queue, for example) through the resolver. app/only. Class declarations underroutes/,database/, or other Laravel directories are not indexed. No current consumer needs them.- No persistent cache. The index is rebuilt each
IndexBuilder::build(). ClassDeclarationrecords are not exposed publicly. Future consumers that needkind/file/linewill require a small API addition.