PHPackages                             liquidrazor/registry-loader - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. liquidrazor/registry-loader

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

liquidrazor/registry-loader
===========================

Compile-time bridge from class discovery to a compiled DIRegistry for LiquidRazor.

v0.1.2(3mo ago)00MITPHPPHP &gt;=8.3

Since Apr 3Pushed 3mo agoCompare

[ Source](https://github.com/LiquidRazor/RegistryLoader)[ Packagist](https://packagist.org/packages/liquidrazor/registry-loader)[ Docs](https://github.com/LiquidRazor/RegistryLoader)[ RSS](/packages/liquidrazor-registry-loader/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (3)Used By (0)

LiquidRazor Framework - RegistryLoader
======================================

[](#liquidrazor-framework---registryloader)

Overview
--------

[](#overview)

The `RegistryLoader` is the bridge between class discovery and a compiled dependency registry.

It exists to transform the output of:

- `liquidrazor/file-locator`
- `liquidrazor/class-locator`

into normalized descriptor definitions consumable by:

- `liquidrazor/diregistry`

In practical terms, the `RegistryLoader` is responsible for turning a discovered class collection into a strict, deterministic, compile-time validated service definition graph.

It is **not** a container on its own. It is **not** a runtime resolver. It is **not** an object instantiator. It is **not** responsible for service lifetime handling at runtime.

Those concerns belong to `DIRegistry`.

The `RegistryLoader` is the compile-time transformation layer that prepares data for `DIRegistry`.

---

Position in the LiquidRazor pipeline
------------------------------------

[](#position-in-the-liquidrazor-pipeline)

The intended pipeline is:

```
filesystem
  -> FileLocator
  -> ClassLocator
  -> RegistryLoader
  -> DIRegistry RegistryCompiler
  -> DescriptorResolver
  -> DescriptorInstantiator
  -> RuntimeInstanceStore

```

### Responsibility split

[](#responsibility-split)

#### FileLocator

[](#filelocator)

Responsible only for deterministic filesystem scanning.

It discovers relevant PHP files according to configured roots and project conventions. It does not parse classes semantically and does not perform DI logic.

#### ClassLocator

[](#classlocator)

Responsible for mapping files to expected classes and validating class-level structure.

It uses FileLocator output, applies PSR-4 mapping rules, validates FQCN expectations, and produces a reliable class collection. It does not register services and does not perform dependency-injection decisions.

#### RegistryLoader

[](#registryloader)

Responsible for transforming discovered classes into normalized descriptor definitions.

It applies:

- conventions
- attributes
- explicit configuration
- strict validation rules

and outputs a descriptor-definition graph ready to be compiled into `DIRegistry`.

#### DIRegistry

[](#diregistry)

Responsible for the actual dependency-injection runtime model.

It owns:

- descriptor compilation
- descriptor storage/indexing
- descriptor resolution
- service instantiation
- runtime instance retention

The `RegistryLoader` must never duplicate these responsibilities.

---

Core purpose
------------

[](#core-purpose)

The `RegistryLoader` answers one question:

> Given a validated class collection, which classes should become services, under which contracts, with which lifecycle, using which scalar values, and under which compile-time rules?

Its output must be:

- deterministic
- explicit where ambiguity exists
- compile-time validated
- free of runtime guessing

The entire philosophy is simple:

- discovery may be broad
- registration must be strict
- ambiguity must fail early
- runtime must not make architectural decisions

---

Design principles
-----------------

[](#design-principles)

### 1. Compile-time first

[](#1-compile-time-first)

All service definition decisions must be made before runtime.

The RegistryLoader is part of the compile phase of the framework. It exists so that runtime behavior is reduced to resolution and instantiation, not discovery and interpretation.

### 2. Deterministic behavior

[](#2-deterministic-behavior)

The same codebase and configuration must always produce the same descriptor graph. No load-order tricks. No priority roulette. No best-effort inference.

### 3. Explicit over clever

[](#3-explicit-over-clever)

Conventions are allowed only where they are trivial and safe. When a decision becomes non-trivial, the system must require attributes or config.

### 4. Fail hard on ambiguity

[](#4-fail-hard-on-ambiguity)

If the system cannot know the correct answer with certainty, compilation must fail. Warnings are not enough for architecture-level ambiguity.

### 5. Runtime must stay boring

[](#5-runtime-must-stay-boring)

By the time `DIRegistry` receives the normalized definitions, the meaningful architectural decisions are already made. Runtime should not negotiate with the environment or attempt to infer service topology.

---

Input sources
-------------

[](#input-sources)

The RegistryLoader operates on three input categories.

### 1. Class collection

[](#1-class-collection)

Produced by `ClassLocator`. This is the authoritative discovered set of classes eligible for further evaluation.

The RegistryLoader does not scan the filesystem directly. It consumes the output of the locator layer.

### 2. Attributes

[](#2-attributes)

Attributes provide explicit metadata directly on classes or constructor parameters. They refine or override convention-based inference.

Attributes are not mandatory for the common case. They are used where explicit control is needed.

### 3. Explicit configuration

[](#3-explicit-configuration)

Explicit configuration has the highest precedence. It exists to:

- override convention and attribute defaults
- bind scalars
- define explicit defaults among competing implementations
- enable or disable services where necessary
- define metadata not suitable for inference

RegistryLoader delegates config file resolution, format handling, parsing, layered merge behavior, and environment interpolation to `liquidrazor/config-loader`. Its own responsibility starts after config has already been loaded into normalized arrays.

---

Precedence model
----------------

[](#precedence-model)

The precedence order is fixed:

1. explicit configuration
2. attributes
3. convention

This rule applies across all supported metadata categories.

### Convention

[](#convention)

Convention provides the baseline candidate definition.

### Attributes

[](#attributes)

Attributes refine or replace convention-derived values.

### Configuration

[](#configuration)

Configuration is authoritative and overrides both attributes and convention.

There is no “last loaded wins” behavior. There is no scanner-order dependency. There is no merge order determined by accident.

Configuration files are loaded through `liquidrazor/config-loader` in YAML mode. RegistryLoader does not parse YAML or JSON itself.

---

Service candidate evaluation
----------------------------

[](#service-candidate-evaluation)

Every discovered class from `ClassLocator` is evaluated as a possible service candidate.

A class is considered eligible for convention-based evaluation only if all of the following are true:

- it is a concrete class
- it is instantiable
- it is not an interface
- it is not a trait
- it is not an enum
- it is not abstract
- it is not explicitly excluded

### Root-aware convention behavior

[](#root-aware-convention-behavior)

The project layout strongly influences service candidacy.

Convention-based loading should apply by default to:

- `src/`
- `lib/`

Convention-based loading should **not** apply blindly to:

- `include/`

This is intentional.

`include/` is expected to contain contracts, types, enums, exceptions, descriptors, value objects, and other definition-level structures that are not automatically runtime services.

Classes inside `include/` may still become services through explicit attributes or configuration, but they should not be loaded by naive convention.

---

Hybrid loading model
--------------------

[](#hybrid-loading-model)

The RegistryLoader uses a hybrid model:

- convention creates the baseline
- attributes refine the baseline
- configuration overrides both

This gives the framework both low boilerplate and strict control.

### Why hybrid is the correct model

[](#why-hybrid-is-the-correct-model)

Pure convention is too implicit for non-trivial systems. Pure attributes are too verbose for ordinary services. The hybrid model allows the common case to remain clean while still forcing explicitness where necessary.

---

Baseline convention definition
------------------------------

[](#baseline-convention-definition)

For each valid service candidate, convention produces an initial descriptor-definition candidate.

Typical baseline values include:

- service identifier
- concrete class path
- construction strategy
- lifecycle
- constructor dependency list
- inferred contracts
- source metadata
- environment metadata
- default flags

### Default identifier

[](#default-identifier)

The default identifier is the fully qualified class name.

This ensures uniqueness and removes the need for string aliases or arbitrary service names.

### Default construction strategy

[](#default-construction-strategy)

The default construction strategy is constructor-based instantiation.

Factory-based construction is never inferred by convention in version 1. Factories must be explicit.

### Source metadata

[](#source-metadata)

The loader should preserve enough source information to generate useful compile-time errors. That usually includes:

- FQCN
- source file path
- origin type (convention, attribute, config)

---

Lifecycle inference rules
-------------------------

[](#lifecycle-inference-rules)

Lifecycle inference is intentionally strict and architecture-aware.

### Core lifecycle philosophy

[](#core-lifecycle-philosophy)

Shared services are safe only when architecture makes them safe. Long-lived workers and future fork-based models make careless singleton defaults dangerous.

Therefore, the framework does **not** treat singleton as the universal default.

### Default lifecycle rules

[](#default-lifecycle-rules)

#### Constructor-based class services

[](#constructor-based-class-services)

- if the class is declared as a **readonly class**, default lifecycle = `singleton`
- otherwise, default lifecycle = `transient`

#### Factory-based services

[](#factory-based-services)

- default lifecycle = `transient`

#### Pooled services

[](#pooled-services)

- never inferred by convention
- allowed only through explicit attribute declaration

#### Scoped services

[](#scoped-services)

- explicit only unless a concrete scope model exists in the wider framework

### Why readonly matters

[](#why-readonly-matters)

Readonly classes provide a language-level architectural signal that the service is intended to be immutable after construction. That makes them viable candidates for safe sharing.

Non-readonly classes are assumed mutable and therefore isolated by default.

### Important limitations

[](#important-limitations)

Readonly is not absolute purity. A readonly class may still hold references to mutable objects. Therefore:

- readonly is a strong default signal
- it is not proof of perfect share-safety

The framework still allows explicit lifecycle overrides where necessary.

---

Contract inference rules
------------------------

[](#contract-inference-rules)

Contract inference is one of the most sensitive parts of the system. The framework intentionally avoids magic here.

### Core principles

[](#core-principles)

- only interfaces can be inferred as contracts
- parent classes are never inferred as contracts
- abstract base classes are never inferred as contracts
- multiple implementations require explicit choice
- no numeric priority is used for default contract resolution

### No parent class inference

[](#no-parent-class-inference)

Inheritance is treated as implementation detail, not as DI contract surface. This avoids accidental exposure of scaffolding, internal hierarchies, and long inheritance chains as injectable contracts.

### Convention-based contract inference

[](#convention-based-contract-inference)

A class may be auto-exposed under a contract **only if**:

- it is a valid service candidate
- it implements exactly one interface
- that interface belongs to allowed contract roots or namespaces
- no attribute or config overrides contract exposure

### Outcomes

[](#outcomes)

#### Zero interfaces

[](#zero-interfaces)

No inferred contract. The class remains resolvable only by concrete class path.

#### Exactly one interface

[](#exactly-one-interface)

That interface may be inferred as the contract, provided it matches allowed contract roots.

#### More than one interface

[](#more-than-one-interface)

No contract is inferred. Explicit declaration is required.

### Allowed contract roots

[](#allowed-contract-roots)

Convention-based contract inference should be limited to configured contract namespaces or roots. This avoids exposing technical or incidental interfaces such as generic PHP or support-level interfaces.

A typical project-level convention is to treat interfaces under a contract namespace such as `...\Contract\` as eligible for inference.

### Multiple implementations of the same contract

[](#multiple-implementations-of-the-same-contract)

If more than one service exposes the same contract, resolution is valid only if exactly one implementation is explicitly marked as the default.

#### Valid cases

[](#valid-cases)

- exactly one implementation exists
- multiple implementations exist and exactly one is explicitly marked as default

#### Invalid cases

[](#invalid-cases)

- multiple implementations exist and none is marked as default
- multiple implementations exist and more than one is marked as default

All invalid cases must fail at compile time.

### No priority-based default selection

[](#no-priority-based-default-selection)

Numeric priority is intentionally excluded from default contract selection. The framework does not guess which implementation should win.

If multiple candidates exist, the developer must declare the intended default explicitly.

Priority may exist later for ordered collections or aggregation scenarios, but not for ordinary single-contract default resolution.

---

Scalar binding rules
--------------------

[](#scalar-binding-rules)

Scalar values are compile-time concerns. They must never remain unresolved until runtime.

### Core scalar philosophy

[](#core-scalar-philosophy)

Scalar dependencies are too ambiguous to autowire safely. A string, integer, float, or boolean carries no architectural intent by itself.

Therefore, scalar resolution must be deterministic and complete during compilation.

### Scalar resolution order

[](#scalar-resolution-order)

For every scalar constructor dependency, the RegistryLoader applies the following precedence order:

1. explicit attribute override
2. explicit configuration binding
3. exact environment-variable inference
4. constructor default value
5. hard compile-time failure

### Compile-time only

[](#compile-time-only)

Scalars must always be resolved during compile time.

This guarantees:

- deterministic compiled registries
- early failure for missing configuration
- fewer production surprises
- no runtime negotiation with process environment

### Default environment-variable inference

[](#default-environment-variable-inference)

If no explicit scalar binding exists, the loader may attempt exact env inference.

The default convention is:

```
App\Service\DbClient::dsn
-> APP_SERVICE_DBCLIENT_DSN

```

This convention must be exact and deterministic. There is no fuzzy matching based only on parameter names such as `HOST`, `PORT`, or `DSN`.

### Constructor defaults

[](#constructor-defaults)

If no attribute, config binding, or env value exists, the loader may use the constructor’s declared default value.

### Failure rule

[](#failure-rule)

If a required scalar cannot be resolved after all resolution stages, compilation must fail.

The framework never injects `null` silently and never falls back to guesswork.

---

Attribute model
---------------

[](#attribute-model)

The attribute surface should remain intentionally small. Attributes are used to express meaningful architectural decisions, not to recreate an annotation-heavy mini language.

### Recommended attribute surface

[](#recommended-attribute-surface)

#### `#[Service(...)]`

[](#service)

Used to define or override service-level metadata.

Supported concerns may include:

- explicit identifier
- explicit contracts
- lifecycle override
- environments
- explicit default marker
- enable/disable behavior

#### `#[Scalar(...)]`

[](#scalar)

Used on constructor parameters to override scalar binding metadata.

Typical use:

- explicit scalar key override
- explicit environment key override

#### `#[IgnoreService]`

[](#ignoreservice)

Used to exclude a discovered class from service registration.

### Attribute philosophy

[](#attribute-philosophy)

Attributes should:

- refine convention
- express intent where convention is insufficient
- remain small and readable

Attributes should **not** become a general-purpose configuration language.

---

Factory handling
----------------

[](#factory-handling)

Factories are supported, but they are intentionally explicit.

### Rules

[](#rules)

- factories are never inferred by convention in version 1
- factories must be declared through attributes or explicit configuration
- factory-based services default to `transient`
- pooled lifecycle is never inferred and must be explicit

### Why factories are explicit-only

[](#why-factories-are-explicit-only)

Factory methods are a common source of hidden state and side effects. Convention-based factory discovery would introduce ambiguity and surprise. The framework chooses predictability over convenience.

---

Error model
-----------

[](#error-model)

Errors are a first-class part of the design. In a strict compile-time system, error messages are effectively part of the user interface.

### Error principles

[](#error-principles)

Errors must be:

- precise
- actionable
- localizable to class and dependency
- explicit about the violated rule

### Example failure categories

[](#example-failure-categories)

- contract has multiple implementations and no default
- contract has multiple explicit defaults
- unresolved scalar dependency
- invalid lifecycle declaration
- pooled lifecycle declared without explicit opt-in
- invalid service candidate
- conflicting explicit configuration
- ignored class referenced as a service

### Example error style

[](#example-error-style)

Good errors should look like:

- `Contract App\Contract\CacheInterface has 2 implementations and no explicit default.`
- `Scalar App\Service\DbClient::dsn could not be resolved from attribute, config, env, or constructor default.`
- `Class App\Internal\Foo is excluded by #[IgnoreService] but was referenced by explicit service config.`

The goal is immediate diagnosis, not generic exception noise.

---

Compile-time validation expectations
------------------------------------

[](#compile-time-validation-expectations)

The RegistryLoader should validate the full descriptor graph before handing it to `DIRegistry`.

Validation should include at least:

- service candidate validity
- contract ambiguity
- duplicate explicit defaults
- invalid lifecycle combinations
- scalar resolution completeness
- explicit metadata conflicts
- unsupported convention outcomes

The loader should fail before runtime whenever architectural ambiguity or invalid state exists.

---

Loading into DIRegistry
-----------------------

[](#loading-into-diregistry)

The RegistryLoader does not stop at producing abstract metadata in the void. Its purpose is to produce normalized descriptor definitions **and then hand them into `DIRegistry` for compilation and registry population**.

That means the loading flow is not merely:

- discover classes
- infer metadata
- validate definitions

It must continue with the actual transfer into the DI system.

### Effective loading flow

[](#effective-loading-flow)

The full compile pipeline is:

```
filesystem
  -> FileLocator
  -> ClassLocator
  -> RegistryLoader
      -> service candidate evaluation
      -> convention baseline creation
      -> attribute refinement
      -> config override
      -> scalar resolution
      -> contract validation
      -> normalized descriptor definition set
  -> DIRegistry RegistryCompiler
      -> compile normalized definitions into immutable descriptor graph
      -> build indexes / internal lookup structures
      -> validate registry-level constraints
  -> Compiled DIRegistry

```

### What “loading into the registry” actually means

[](#what-loading-into-the-registry-actually-means)

The RegistryLoader must convert its final internal representation into the exact normalized definition shape expected by `DIRegistry`.

It then passes that complete normalized definition set to `RegistryCompiler`.

`RegistryCompiler` is responsible for:

- compiling raw normalized definitions into actual registry descriptors
- building the immutable descriptor repository
- constructing resolution indexes
- enforcing final registry-level validation rules

So the RegistryLoader does **not** manually mutate runtime registry state entry by entry like an ad-hoc service bag. It prepares the full descriptor definition graph and then **loads the graph into DIRegistry through the compiler boundary**.

### Compiler boundary as the official integration point

[](#compiler-boundary-as-the-official-integration-point)

The integration point between RegistryLoader and DIRegistry is the compiler boundary.

That boundary exists for a reason:

- RegistryLoader owns transformation from class world to definition world
- DIRegistry owns transformation from definition world to compiled descriptor registry

This separation keeps both libraries honest.

The RegistryLoader must therefore output definitions in the canonical format required by DIRegistry and invoke compilation explicitly.

### Output of the RegistryLoader

[](#output-of-the-registryloader)

The practical output of the RegistryLoader should be one of the following, depending on API style:

#### Option A: compiled registry as final product

[](#option-a-compiled-registry-as-final-product)

The RegistryLoader returns a compiled `DIRegistry` instance, having internally:

1. collected classes
2. built normalized definitions
3. passed them into `RegistryCompiler`
4. returned the compiled registry

This is the most ergonomic option for framework bootstrapping.

#### Option B: normalized definitions as an intermediate product

[](#option-b-normalized-definitions-as-an-intermediate-product)

The RegistryLoader returns the normalized definition set, and a higher bootstrap layer passes it to `RegistryCompiler`.

This is more explicit, but also more fragmented.

### Recommended approach

[](#recommended-approach)

For framework use, the preferred behavior is:

- RegistryLoader internally builds normalized definitions
- RegistryLoader invokes `RegistryCompiler`
- RegistryLoader returns a compiled `DIRegistry`

That way, “loading” actually means loading, not merely preparing data and hoping something else remembers to finish the job.

### Why this matters

[](#why-this-matters)

Without this final step, the RegistryLoader would be incomplete. It would only be a descriptor-definition builder, not the actual bridge from class discovery into a usable compiled dependency registry.

The whole point of the component is to connect:

- discovered classes
- inferred and explicit metadata
- scalar/config resolution
- contract/lifecycle validation

with the actual compiled registry consumed later by:

- `DescriptorResolver`
- `DescriptorInstantiator`
- `RuntimeInstanceStore`

So the final act of the RegistryLoader is:

> produce normalized definitions, compile them through `DIRegistry`, and hand back a usable compiled registry.

Runtime boundary
----------------

[](#runtime-boundary)

The RegistryLoader ends where compiled `DIRegistry` begins.

Once normalized descriptor definitions are produced, validated, and compiled, responsibility transfers to `DIRegistry` for:

- descriptor storage/indexing
- descriptor resolution
- object instantiation
- runtime instance storage

This boundary must remain clean.

The RegistryLoader must never become:

- a runtime resolver
- a hidden container runtime
- an instance cache
- a lazy-instantiation engine

Its job is to:

1. transform class discovery into normalized definitions
2. resolve compile-time metadata
3. validate the graph
4. compile the graph into `DIRegistry`
5. stop

---

Version 1 scope
---------------

[](#version-1-scope)

Version 1 should remain intentionally focused.

### Included in version 1

[](#included-in-version-1)

- hybrid convention + attribute + config loading
- readonly-aware lifecycle inference
- explicit factory support
- compile-time scalar binding
- strict interface-only contract inference
- explicit default selection for multiple implementations
- hard compile-time failure on ambiguity

### Deliberately excluded from version 1

[](#deliberately-excluded-from-version-1)

- parent class contract inference
- priority-based default resolution
- convention-based factory inference
- runtime scalar resolution
- silent fallbacks
- property injection
- contextual binding
- autoconfiguration magic
- service decoration
- lazy proxies
- tag ecosystems

The system should first be correct, explicit, and stable. Additional features should be added only if they preserve determinism and strictness.

---

Summary
-------

[](#summary)

The `RegistryLoader` is the compile-time transformation layer between class discovery and dependency-injection runtime.

It exists to produce a normalized, validated descriptor-definition graph from:

- discovered classes
- attributes
- explicit configuration

Its design is based on a few non-negotiable rules:

- compile-time first
- deterministic behavior
- explicit conflict resolution
- no runtime scalar guessing
- no priority roulette
- no inheritance-as-contract nonsense
- no silent ambiguity handling

The result is a DI preparation layer that actively enforces architecture rather than merely accommodating it.

That is the intended role of the RegistryLoader inside the LiquidRazor Framework.

###  Health Score

33

—

LowBetter than 72% of packages

Maintenance82

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

92d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4306922?v=4)[Noramarth](/maintainers/Noramarth)[@Noramarth](https://github.com/Noramarth)

---

Top Contributors

[![Noramarth](https://avatars.githubusercontent.com/u/4306922?v=4)](https://github.com/Noramarth "Noramarth (3 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/liquidrazor-registry-loader/health.svg)

```
[![Health](https://phpackages.com/badges/liquidrazor-registry-loader/health.svg)](https://phpackages.com/packages/liquidrazor-registry-loader)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
