Skip to content

Feature/advanced features#46

Merged
milinsky merged 8 commits into
devfrom
feature/advanced-features
Nov 24, 2025
Merged

Feature/advanced features#46
milinsky merged 8 commits into
devfrom
feature/advanced-features

Conversation

@milinsky
Copy link
Copy Markdown
Member

No description provided.

- Add Scope enum with Singleton and Transient options
- Add ScopeStorage for managing service scopes
- Implement transient scope: new instance on each get()
- Add ContainerConfig::withScope() method
- Add buildTransient() method to Injector
- Add 5 new tests for scope functionality
- All tests passing (107 tests, 181 assertions)
- Psalm: No errors, 99.88% type inference
**Bind Validation:**
- Add InvalidBindingException for binding errors
- Validate interface/abstract class existence
- Validate implementation class exists and implements interface
- Support abstract class bindings
- Update DependencyMapper to handle abstract classes
- Add 7 tests for bind validation

**Callback Factories:**
- Add FactoryStorage for factory management
- Add factory() method to ContainerInterface
- Factories have priority over autowiring
- Factories create singletons by default
- Container can resolve dependencies in factory callbacks
- Add 6 tests for factory functionality

Tests: 120 tests, 205 assertions
Psalm: No errors (99.89% type coverage)
**Compile Method:**
- Add compile() method to ContainerInterface
- Validates all registered bindings without instantiation
- Returns array of error messages (empty = all OK)
- Detects missing dependencies and circular references
- Helps find configuration errors before runtime
- Add 5 tests for compile functionality

Tests: 125 tests, 215 assertions
Psalm: No errors (99.89% type coverage)
Added comprehensive documentation for new features:
- Service Scopes (Singleton/Transient)
- Callback Factories
- Binding Validation
- Compile-time Validation

All sections include detailed examples and use cases.
For better clarity and consistency, explicitly declare has() method
in ContainerInterface even though it's inherited from PSR-11.
This makes the contract more explicit and improves IDE support.
Changed validation strategy to rely on ReflectionClass autoloading
instead of manual interface_exists/class_exists checks. This fixes
issues when binding classes from other packages that haven't been
loaded yet.

Changes:
- Use ReflectionClass with try-catch for validation
- Catch ReflectionException and wrap in InvalidBindingException
- Remove premature existence checks
- Update test expectations to match new error messages
- Add Psalm type hints and suppressions

This maintains validation quality while being compatible with
Composer autoloading across multiple packages.

Fixes: Fatal error when binding interfaces from external packages
BREAKING CHANGE: Binding validation no longer happens at bind() time

Why this change?
================
The original implementation validated bindings immediately when bind()
was called. This caused fatal errors when binding classes from external
packages that haven't been autoloaded yet:

  Fatal error: InvalidBindingException: Invalid binding:
  "Duyler\Builder\Loader\ApplicationLoaderInterface" =>
  "Duyler\Builder\ApplicationLoader".
  Class "Duyler\Builder\Loader\ApplicationLoaderInterface" does not exist

The problem: ReflectionClass cannot validate classes that haven't been
loaded by the autoloader. In multi-package projects, classes from
external packages may not be loaded when bind() is called in the
Container constructor.

Solution:
=========
Move validation to compile() method, which is explicitly designed for
configuration validation. This makes validation:
- Lazy: Only runs when compile() is explicitly called
- Safe: Works with any class that can be autoloaded
- Optional: Runtime code works without validation

In production, dependencies are validated naturally when resolved.
For pre-deployment checks, use compile() to validate all bindings.

Changes:
========
1. Remove validation from DependencyMapper::bind()
2. Add DependencyMapper::validateBindings() for lazy validation
3. Update Container::compile() to validate bindings first
4. Update tests to check validation via compile() not exceptions

About @psalm-suppress:
======================
Used because Psalm cannot statically prove that strings from
array<string, string> are valid class-string types. This is safe
because validation catches all errors at runtime and returns them
as error messages instead of throwing exceptions.

Tests: 125 tests, 215 assertions - all passing
@sonarqubecloud
Copy link
Copy Markdown

@milinsky milinsky merged commit 36f9e1a into dev Nov 24, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant