diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 855c42d..5907267 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -57,7 +57,7 @@ body:
id: reproduce
attributes:
label: Minimal reproduction
- description: Provide the smallest code sample, command, configuration, or repository that reproduces the problem.
+ description: Provide the smallest code sample, command, configuration or repository that reproduces the problem.
placeholder: |
1. Install or configure ...
2. Run ...
@@ -108,7 +108,7 @@ body:
id: environment
attributes:
label: Environment
- description: Include the operating system, relevant extensions, dependency mode, runtime, and CI provider when applicable.
+ description: Include the operating system, relevant extensions, dependency mode, runtime and CI provider when applicable.
placeholder: Ubuntu 24.04, locked dependencies, ext-json enabled, GitHub Actions...
validations:
required: true
@@ -117,4 +117,4 @@ body:
id: additional
attributes:
label: Additional context
- description: Add related issues, screenshots, logs, workarounds, or other useful context.
+ description: Add related issues, screenshots, logs, workarounds or other useful context.
diff --git a/.github/ISSUE_TEMPLATE/ci_failure.yml b/.github/ISSUE_TEMPLATE/ci_failure.yml
index e1897bf..9c3883f 100644
--- a/.github/ISSUE_TEMPLATE/ci_failure.yml
+++ b/.github/ISSUE_TEMPLATE/ci_failure.yml
@@ -60,7 +60,7 @@ body:
id: local_details
attributes:
label: Local reproduction details
- description: Include the command, result, and any differences from CI.
+ description: Include the command, result and any differences from CI.
placeholder: composer ic:ci fails locally with the same error...
- type: input
@@ -83,7 +83,7 @@ body:
id: environment
attributes:
label: Runner and dependency environment
- description: Include the runner OS, dependency mode, relevant extensions, matrix values, and PHPForge version when known.
+ description: Include the runner OS, dependency mode, relevant extensions, matrix values and PHPForge version when known.
placeholder: ubuntu-latest, prefer-lowest, PHPForge 1.x, ext-json enabled...
validations:
required: true
@@ -92,10 +92,10 @@ body:
id: recent_changes
attributes:
label: Relevant recent changes
- description: Mention dependency, configuration, workflow, or source changes that may be related.
+ description: Mention dependency, configuration, workflow or source changes that may be related.
- type: textarea
id: additional
attributes:
label: Additional context
- description: Add related issues, screenshots, logs, or other useful context.
+ description: Add related issues, screenshots, logs or other useful context.
diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.yml b/.github/ISSUE_TEMPLATE/docs_improvement.yml
index 13b4f60..2ea49e9 100644
--- a/.github/ISSUE_TEMPLATE/docs_improvement.yml
+++ b/.github/ISSUE_TEMPLATE/docs_improvement.yml
@@ -1,5 +1,5 @@
name: Documentation improvement
-description: Report missing, outdated, unclear, or incorrect documentation
+description: Report missing, outdated, unclear or incorrect documentation
labels:
- documentation
body:
@@ -21,7 +21,7 @@ body:
id: location
attributes:
label: Documentation location
- description: Provide the file path, section, symbol, or URL.
+ description: Provide the file path, section, symbol or URL.
placeholder: README.md — Quick Start
validations:
required: true
@@ -30,7 +30,7 @@ body:
id: problem
attributes:
label: Problem
- description: Explain what is missing, unclear, outdated, or incorrect.
+ description: Explain what is missing, unclear, outdated or incorrect.
placeholder: The current documentation says or omits...
validations:
required: true
@@ -48,11 +48,11 @@ body:
id: suggestion
attributes:
label: Suggested improvement
- description: Optionally propose wording, structure, examples, or references.
+ description: Optionally propose wording, structure, examples or references.
placeholder: It may be clearer to...
- type: textarea
id: additional
attributes:
label: Additional context
- description: Add related links, screenshots, discussions, or examples.
+ description: Add related links, screenshots, discussions or examples.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 22df828..bbee6d0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -8,13 +8,13 @@ body:
value: |
Describe the problem or use case before proposing an implementation.
- For substantial public API, architectural, or compatibility changes, discussion may be requested before implementation.
+ For substantial public API, architectural or compatibility changes, discussion may be requested before implementation.
- type: textarea
id: problem
attributes:
label: Problem or use case
- description: Explain the limitation, repeated difficulty, or capability you need.
+ description: Explain the limitation, repeated difficulty or capability you need.
placeholder: I need to... because...
validations:
required: true
@@ -32,7 +32,7 @@ body:
id: example
attributes:
label: Example usage
- description: Optionally show the proposed API, configuration, command, or workflow.
+ description: Optionally show the proposed API, configuration, command or workflow.
render: php
- type: textarea
@@ -45,10 +45,10 @@ body:
id: compatibility
attributes:
label: Compatibility considerations
- description: Mention possible public API, behavior, PHP-version, extension, platform, or dependency implications.
+ description: Mention possible public API, behavior, PHP-version, extension, platform or dependency implications.
- type: textarea
id: additional
attributes:
label: Additional context
- description: Add related issues, prior art, links, benchmarks, or other supporting information.
+ description: Add related issues, prior art, links, benchmarks or other supporting information.
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
index 5a322e3..62e91a7 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -1,5 +1,5 @@
name: Question
-description: Ask about usage, behavior, integration, or configuration
+description: Ask about usage, behavior, integration or configuration
labels:
- question
body:
@@ -23,7 +23,7 @@ body:
id: attempted
attributes:
label: What have you tried?
- description: Include relevant code, commands, configuration, documentation, or approaches already checked.
+ description: Include relevant code, commands, configuration, documentation or approaches already checked.
placeholder: I tried...
validations:
required: true
@@ -46,11 +46,11 @@ body:
id: environment
attributes:
label: Environment
- description: Include package, PHP, Composer, OS, extensions, or CI details only when relevant.
+ description: Include package, PHP, Composer, OS, extensions or CI details only when relevant.
placeholder: Package 2.4.1, PHP 8.4, Composer 2.9, Ubuntu 24.04...
- type: textarea
id: additional
attributes:
label: Additional context
- description: Add related links, screenshots, or prior discussions.
+ description: Add related links, screenshots or prior discussions.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4d91309..dcab1f5 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -32,26 +32,22 @@ Use `Closes #...`, `Fixes #...` or `Relates #...` where applicable.
## Validation
-### Complete Suite
-
* [ ] `composer ic:ci`
-
+
Focused validation
* [ ] `composer ic:test:syntax`
@@ -68,21 +64,11 @@ Select every command that passed.
-### Coverage
-
-* [ ] Expected or newly introduced behavior
-* [ ] Regression scenario
-* [ ] Boundary and edge cases
-* [ ] Failure and exception paths
-* [ ] Public API compatibility
-* [ ] Version, dependency, extension or platform-specific behavior
-* [ ] Not applicable
-
@@ -95,16 +81,10 @@ is being made.
* [ ] Relevant benchmarks were added or updated
* [ ] Results were compared against a relevant baseline
-
-
-Benchmark commands
-
* [ ] `composer ic:benchmark`
* [ ] `composer ic:bench:quick`
* [ ] `composer ic:bench:chart`
-
-
## Checklist
@@ -140,4 +118,4 @@ version compatibility or security-sensitive logic.
* [ ] Documentation, examples and type information were updated where required.
* [ ] Performance claims are supported by reproducible benchmarks.
* [ ] No credentials, secrets, personal data or sensitive debug output are included.
-* [ ] I followed `CONTRIBUTING.md`.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md
new file mode 100644
index 0000000..66759b4
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md
@@ -0,0 +1,94 @@
+## Problem
+
+Describe the incorrect behavior, its impact and how it can be reproduced.
+
+
+
+## Root Cause
+
+
+
+## Fix
+
+Describe how the change corrects the problem and the expected behavior after the fix.
+
+## Behavior and Compatibility
+
+* [ ] Existing documented behavior was restored
+* [ ] Existing undocumented behavior was corrected
+* [ ] Public API remains compatible
+* [ ] Public API or documented behavior changed
+* [ ] Backward compatibility may be affected
+* [ ] PHP, extension, platform or dependency requirements changed
+
+
+
+## Validation
+
+* [ ] `composer ic:ci`
+* [ ] The original failure no longer reproduces
+* [ ] A regression test was added or updated
+* [ ] Relevant boundary and failure paths were tested
+
+
+
+
+Focused validation
+
+
+
+* [ ] `composer ic:test:syntax`
+* [ ] `composer ic:test:code`
+* [ ] `composer ic:test:lint`
+* [ ] `composer ic:test:sniff`
+* [ ] `composer ic:test:duplicates`
+* [ ] `composer ic:test:probe`
+* [ ] `composer ic:test:comments`
+* [ ] `composer ic:test:architecture`
+* [ ] `composer ic:test:static`
+* [ ] `composer ic:test:security`
+* [ ] `composer ic:test:refactor`
+
+
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] The fix is focused and excludes unrelated changes.
+* [ ] The fix addresses the root cause rather than only masking symptoms.
+* [ ] Regression-prone behavior is covered by tests.
+* [ ] Public API and backward-compatibility implications were considered.
+* [ ] Documentation and examples were updated where required.
+* [ ] No credentials, secrets, personal data or sensitive debug output are included.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/documentation.md b/.github/PULL_REQUEST_TEMPLATE/documentation.md
new file mode 100644
index 0000000..f983b85
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/documentation.md
@@ -0,0 +1,51 @@
+## Documentation Change
+
+Describe what is being added, corrected, clarified or removed and why.
+
+
+
+## Affected Content
+
+* [ ] README or getting-started guidance
+* [ ] API or reference documentation
+* [ ] Configuration documentation
+* [ ] Examples or tutorials
+* [ ] Contribution or community documentation
+* [ ] Changelog or release documentation
+* [ ] Other
+
+## Verification
+
+* [ ] Links and references were checked
+* [ ] Code examples were executed or otherwise verified
+* [ ] Commands and configuration examples match current behavior
+* [ ] Terminology is consistent with the project
+* [ ] `composer ic:ci`
+* [ ] No executable behavior changed
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] The change is focused and excludes unrelated code changes.
+* [ ] Documentation reflects the current public behavior.
+* [ ] Examples are minimal, accurate and safe to copy.
+* [ ] Sensitive or private information is not included.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md
new file mode 100644
index 0000000..f6ce9c6
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/feature.md
@@ -0,0 +1,106 @@
+## Motivation
+
+Describe the problem, use case or capability this feature addresses.
+
+
+
+## Solution
+
+Describe the proposed behavior and how consumers are expected to use it.
+
+## API and Compatibility
+
+* [ ] No new public API
+* [ ] New backward-compatible public API
+* [ ] Existing public API or documented behavior changed
+* [ ] Backward compatibility may be affected
+* [ ] PHP, extension, platform or dependency requirements changed
+
+
+
+## Validation
+
+* [ ] `composer ic:ci`
+* [ ] Expected behavior is covered
+* [ ] Boundary and edge cases are covered
+* [ ] Failure and exception paths are covered
+* [ ] Public API usage is covered
+
+
+
+
+Focused validation
+
+
+
+* [ ] `composer ic:test:syntax`
+* [ ] `composer ic:test:code`
+* [ ] `composer ic:test:lint`
+* [ ] `composer ic:test:sniff`
+* [ ] `composer ic:test:duplicates`
+* [ ] `composer ic:test:probe`
+* [ ] `composer ic:test:comments`
+* [ ] `composer ic:test:architecture`
+* [ ] `composer ic:test:static`
+* [ ] `composer ic:test:security`
+* [ ] `composer ic:test:refactor`
+
+
+
+
+
+## Performance
+
+
+
+* [ ] Relevant benchmarks were added or updated
+* [ ] Results were compared against a relevant baseline
+* [ ] `composer ic:benchmark`
+* [ ] `composer ic:bench:quick`
+* [ ] `composer ic:bench:chart`
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] The feature is focused and excludes unrelated changes.
+* [ ] Tests cover the public contract and failure behavior.
+* [ ] Public API and backward-compatibility implications were considered.
+* [ ] Documentation, examples and type information were updated.
+* [ ] Performance claims are supported by reproducible benchmarks.
+* [ ] No credentials, secrets, personal data or sensitive debug output are included.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/maintenance.md b/.github/PULL_REQUEST_TEMPLATE/maintenance.md
new file mode 100644
index 0000000..a4cab0a
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/maintenance.md
@@ -0,0 +1,91 @@
+## Maintenance Change
+
+Describe what changed, why it was needed and the expected effect on development, CI or releases.
+
+
+
+## Category
+
+* [ ] Dependency update
+* [ ] CI or workflow change
+* [ ] Build or release tooling
+* [ ] PHPForge configuration
+* [ ] Development tooling
+* [ ] Repository maintenance
+* [ ] Other
+
+## Impact and Compatibility
+
+* [ ] Runtime behavior is unaffected
+* [ ] Development workflow changed
+* [ ] CI or release behavior changed
+* [ ] Supported PHP, extension, platform or dependency requirements changed
+* [ ] Generated files or configuration changed
+* [ ] Backward compatibility may be affected
+
+
+
+## Validation
+
+* [ ] `composer ic:ci`
+* [ ] Relevant workflow or job was exercised
+* [ ] Supported matrix or dependency mode was considered
+* [ ] Generated or published files were verified
+* [ ] Failure and rollback behavior was considered
+
+
+
+
+Focused validation
+
+
+
+* [ ] `composer ic:test:syntax`
+* [ ] `composer ic:test:code`
+* [ ] `composer ic:test:lint`
+* [ ] `composer ic:test:sniff`
+* [ ] `composer ic:test:duplicates`
+* [ ] `composer ic:test:probe`
+* [ ] `composer ic:test:comments`
+* [ ] `composer ic:test:architecture`
+* [ ] `composer ic:test:static`
+* [ ] `composer ic:test:security`
+* [ ] `composer ic:test:refactor`
+
+
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] The change is focused and excludes unrelated source refactoring.
+* [ ] Dependency or workflow changes are minimal and justified.
+* [ ] Public API and backward-compatibility implications were considered.
+* [ ] Documentation and generated files were updated where required.
+* [ ] No credentials, secrets, personal data or sensitive debug output are included.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/performance.md b/.github/PULL_REQUEST_TEMPLATE/performance.md
new file mode 100644
index 0000000..ce2d10d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/performance.md
@@ -0,0 +1,99 @@
+## Bottleneck
+
+Describe the measured performance problem, affected execution path and practical impact.
+
+
+
+## Optimization
+
+Describe the change, why it improves the measured path and any trade-offs introduced.
+
+## Correctness and Compatibility
+
+* [ ] Observable behavior remains unchanged
+* [ ] Public API remains compatible
+* [ ] Error and exception behavior remains compatible
+* [ ] Behavior or public API changed intentionally
+* [ ] PHP, extension, platform or dependency requirements changed
+
+
+
+## Benchmark Evidence
+
+* [ ] Relevant benchmarks were added or updated
+* [ ] Results were compared against a relevant baseline
+* [ ] Multiple stable runs were considered
+* [ ] Runtime impact was measured
+* [ ] Memory or allocation impact was measured where relevant
+* [ ] `composer ic:benchmark`
+* [ ] `composer ic:bench:quick`
+* [ ] `composer ic:bench:chart`
+
+
+
+## Validation
+
+* [ ] `composer ic:ci`
+* [ ] Expected behavior remains covered
+* [ ] Boundary and failure paths remain covered
+* [ ] Performance-sensitive behavior is covered
+
+
+
+
+Focused validation
+
+
+
+* [ ] `composer ic:test:syntax`
+* [ ] `composer ic:test:code`
+* [ ] `composer ic:test:lint`
+* [ ] `composer ic:test:sniff`
+* [ ] `composer ic:test:duplicates`
+* [ ] `composer ic:test:probe`
+* [ ] `composer ic:test:comments`
+* [ ] `composer ic:test:architecture`
+* [ ] `composer ic:test:static`
+* [ ] `composer ic:test:security`
+* [ ] `composer ic:test:refactor`
+
+
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] The optimization targets a measured bottleneck.
+* [ ] Results are reproducible in comparable environments.
+* [ ] Correctness was not traded for an unverified micro-optimization.
+* [ ] Public API and backward-compatibility implications were considered.
+* [ ] Benchmark and documentation changes are included where required.
+* [ ] No credentials, secrets, personal data or sensitive debug output are included.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/refactor.md b/.github/PULL_REQUEST_TEMPLATE/refactor.md
new file mode 100644
index 0000000..588be4f
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/refactor.md
@@ -0,0 +1,108 @@
+## Intent and Scope
+
+Describe what was restructured, why it was necessary and what remains intentionally unchanged.
+
+
+
+## Behavioral Guarantee
+
+* [ ] No observable behavior changed
+* [ ] Public API remains unchanged
+* [ ] Existing behavior was intentionally corrected
+* [ ] Public API or documented behavior changed
+* [ ] Backward compatibility may be affected
+
+
+
+## Design Notes
+
+
+
+## Validation
+
+* [ ] `composer ic:ci`
+* [ ] Existing behavior remains covered
+* [ ] Relevant regression and edge cases are covered
+* [ ] Public API compatibility was verified
+
+
+
+
+Focused validation
+
+
+
+* [ ] `composer ic:test:syntax`
+* [ ] `composer ic:test:code`
+* [ ] `composer ic:test:lint`
+* [ ] `composer ic:test:sniff`
+* [ ] `composer ic:test:duplicates`
+* [ ] `composer ic:test:probe`
+* [ ] `composer ic:test:comments`
+* [ ] `composer ic:test:architecture`
+* [ ] `composer ic:test:static`
+* [ ] `composer ic:test:security`
+* [ ] `composer ic:test:refactor`
+
+
+
+
+
+## Performance
+
+
+
+* [ ] Results were compared against a relevant baseline
+* [ ] `composer ic:benchmark`
+* [ ] `composer ic:bench:quick`
+* [ ] `composer ic:bench:chart`
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] The refactor is focused and excludes unrelated behavior changes.
+* [ ] Complexity was reduced without unnecessary abstraction or file growth.
+* [ ] Existing contracts and failure behavior remain covered.
+* [ ] Public API and backward-compatibility implications were considered.
+* [ ] Documentation and type information were updated where required.
+* [ ] No credentials, secrets, personal data or sensitive debug output are included.
+* [ ] I followed `CONTRIBUTING.md` and the engineering principles.
diff --git a/.github/PULL_REQUEST_TEMPLATE/security_reliability.md b/.github/PULL_REQUEST_TEMPLATE/security_reliability.md
new file mode 100644
index 0000000..ef2ca28
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/security_reliability.md
@@ -0,0 +1,92 @@
+
+
+## Concern
+
+Describe the security weakness, reliability failure mode or defensive gap being addressed.
+
+
+
+## Mitigation
+
+Describe how the change reduces the risk and what assumptions or limitations remain.
+
+## Impact and Compatibility
+
+* [ ] Security hardening with no observable behavior change
+* [ ] Reliability improvement with no public API change
+* [ ] Failure or exception behavior changed
+* [ ] Public API or documented behavior changed
+* [ ] Backward compatibility may be affected
+* [ ] PHP, extension, platform or dependency requirements changed
+
+
+
+## Validation
+
+* [ ] `composer ic:ci`
+* [ ] Security-sensitive or failure behavior is covered
+* [ ] Abuse, malformed-input or failure paths are covered
+* [ ] Regression coverage was added or updated
+* [ ] `composer ic:test:security`
+
+
+
+
+Focused validation
+
+
+
+* [ ] `composer ic:test:syntax`
+* [ ] `composer ic:test:code`
+* [ ] `composer ic:test:lint`
+* [ ] `composer ic:test:sniff`
+* [ ] `composer ic:test:duplicates`
+* [ ] `composer ic:test:probe`
+* [ ] `composer ic:test:comments`
+* [ ] `composer ic:test:architecture`
+* [ ] `composer ic:test:static`
+* [ ] `composer ic:test:security`
+* [ ] `composer ic:test:refactor`
+
+
+
+
+
+## Review Focus
+
+
+
+## Checklist
+
+* [ ] Confidential vulnerability details are not exposed publicly.
+* [ ] The change is focused and avoids unrelated refactoring.
+* [ ] Security or reliability claims are supported by tests.
+* [ ] Failure paths and backward-compatibility implications were considered.
+* [ ] Documentation and upgrade guidance were updated where required.
+* [ ] No credentials, secrets, personal data or sensitive debug output are included.
+* [ ] I followed `SECURITY.md`, `CONTRIBUTING.md` and the engineering principles.
diff --git a/.gitignore b/.gitignore
index f02dfc8..de37c51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ test.php
var
vendor
.codex
+plan.md
diff --git a/README.md b/README.md
index 1a5985d..3079492 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@

[](https://docs.infocyph.com/projects/intermix/)
-`InterMix` is a modern, lightweight PHP toolkit for developers who value class-oriented design, clean architecture, and fast execution. It combines dependency injection, serialization, macro-style extensibility, and helper utilities with minimal config and maximum control.
+`InterMix` is a lightweight, high-performance PHP dependency injection and runtime utility toolkit. Dependency injection and invocation are the primary capabilities, supported by compiled resolution, Closure serialization, Fence, and fluent Remix utilities.
> Global helper functions are optional: core APIs are namespaced and helper loading is opt-in.
@@ -20,7 +20,7 @@
- lazy loading
- environment-specific overrides
- debug tracing & definition-cache integration via assignable PSR-6 pool
-- **Serializer** — Closure-aware value serialization and resource handlers
+- **Closure Serialization** — Versioned unsigned and explicitly signed Closure payloads
- **Fence** — Enforce singleton-style class safety
- **Remix** — Fluent traits, proxies, and global helper functions
- **MacroMix** — Dynamically extend objects or classes with macros
@@ -122,7 +122,7 @@ echo (new MacroTest)->hello('Ali'); // Hey, Ali!
```php
use Psr\Cache\CacheItemPoolInterface;
-$pool = /* any PSR-6 pool, e.g. from infocyph/cachelayer */;
+$pool = /* any PSR-6 pool, e.g. from infocyph/Intermix */;
$c->definitions()->enableDefinitionCache($pool, cacheRuntimeObjects: false);
```
@@ -159,14 +159,22 @@ disables the active map until it is rebuilt. Ordinary closures and
See the [compiled resolver guide](https://docs.infocyph.com/projects/InterMix/en/latest/di/compiled-resolvers.html).
-### Signed Serialization
+### Closure Serialization
```php
-$signed = \Infocyph\InterMix\Serializer\ValueSerializer::signed($_ENV['APP_KEY']);
-$token = $signed->encode(['user_id' => 1]);
-$payload = $signed->decode($token);
+use Infocyph\InterMix\Serializer\ClosureSerializer;
+
+$payload = ClosureSerializer::serialize(static fn (int $value): int => $value * 2);
+$closure = ClosureSerializer::unserialize($payload);
+
+$signed = ClosureSerializer::signed($_ENV['APP_KEY']);
+$signedPayload = $signed->serialize(static fn (): string => 'queued work');
+$signedClosure = $signed->unserialize($signedPayload);
```
+Ordinary PHP values use native PHP facilities. Resources remain the application's
+responsibility. Signing is instance-scoped and adds no work to normal invocation.
+
## Testing
```bash
@@ -177,16 +185,33 @@ composer ic:tests
## Security
-Protected by [PHPForge](https://github.com/infocyph/PHPForge) — an automated quality and security gate for PHP projects.
+Do not disclose suspected vulnerabilities in a public issue, discussion or pull request. Follow [SECURITY.md](SECURITY.md) and use [GitHub private vulnerability reporting](https://github.com/infocyph/Intermix/security/advisories/new).
+
+Intermix is protected by [PHPForge](https://github.com/infocyph/PHPForge), which provides automated tests, static and taint analysis, dependency auditing, architecture checks and release-readiness gates. Automated controls do not replace responsible disclosure or manual review.
+
---
diff --git a/benchmarks/IntermixBench.php b/benchmarks/IntermixBench.php
index aa0a713..3f38a8f 100644
--- a/benchmarks/IntermixBench.php
+++ b/benchmarks/IntermixBench.php
@@ -35,6 +35,12 @@ public function benchClosureCallWithDi(): void
$this->container->call($this->diHandler);
}
+ #[BeforeMethods('setUpContainer')]
+ public function benchContainerHasHotPath(): void
+ {
+ $this->container->has('bench.config');
+ }
+
#[BeforeMethods('setUpContainer')]
public function benchDirectFactoryTransientResolution(): void
{
@@ -143,6 +149,16 @@ public function benchTaggedLookupFindByTag(): void
$this->container->findByTag('bench.pipeline.pre');
}
+ #[BeforeMethods('setUpContainer')]
+ public function benchTaggedLookupLazy(): void
+ {
+ foreach ($this->container->tagged('bench.pipeline.pre') as $factory) {
+ $factory();
+
+ break;
+ }
+ }
+
#[BeforeMethods('setUpContainer')]
public function benchTransientMake(): void
{
diff --git a/benchmarks/RuntimeFeaturesBench.php b/benchmarks/RuntimeFeaturesBench.php
new file mode 100644
index 0000000..b4b389f
--- /dev/null
+++ b/benchmarks/RuntimeFeaturesBench.php
@@ -0,0 +1,190 @@
+container = new Container('__runtime_benchmark__' . spl_object_id($this));
+ $this->container->singleton('runtime.singleton', new RuntimeInvokable());
+ $this->container->get('runtime.singleton');
+ $this->invoker = Invoker::with($this->container);
+ $this->invokable = new RuntimeInvokable();
+ $this->closure = static fn(): int => 1;
+ $this->serializedClosure = ClosureSerializer::serialize($this->closure);
+ $this->signedSerializer = ClosureSerializer::signed('runtime-benchmark-key');
+ $this->signedSerializedClosure = $this->signedSerializer->serialize($this->closure);
+
+ RuntimeUnlockedMacros::macro('instanceMacro', fn(): int => 1);
+ RuntimeUnlockedMacros::macro('staticMacro', static fn(): int => 1);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchClosureSerialize(): void
+ {
+ ClosureSerializer::serialize($this->closure);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchClosureUnserialize(): void
+ {
+ ClosureSerializer::unserialize($this->serializedClosure);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchContainerHas(): void
+ {
+ $this->container->has('runtime.singleton');
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchInvokerClass(): void
+ {
+ $this->invoker->invoke(RuntimeInvokable::class);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchInvokerFunction(): void
+ {
+ $this->invoker->invoke(__NAMESPACE__ . '\\runtimeBenchmarkFunction');
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchInvokerInvokableObject(): void
+ {
+ $this->invoker->invoke($this->invokable);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchInvokerSerializedClosure(): void
+ {
+ $this->invoker->invoke($this->serializedClosure);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchInvokerStaticMethodString(): void
+ {
+ $this->invoker->invoke(RuntimeStaticTarget::class . '::run');
+ }
+
+ public function benchMacroBulkMixLockDisabled(): void
+ {
+ RuntimeUnlockedMacros::mix(new RuntimeMixin());
+ }
+
+ public function benchMacroBulkMixLockEnabled(): void
+ {
+ RuntimeLockedMacros::mix(new RuntimeMixin());
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchMacroInstanceInvocation(): void
+ {
+ (new RuntimeUnlockedMacros())->instanceMacro();
+ }
+
+ public function benchMacroRegistrationLockDisabled(): void
+ {
+ RuntimeUnlockedMacros::macro('registered', static fn(): int => 1);
+ }
+
+ public function benchMacroRegistrationLockEnabled(): void
+ {
+ RuntimeLockedMacros::macro('registered', static fn(): int => 1);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchMacroStaticInvocation(): void
+ {
+ RuntimeUnlockedMacros::staticMacro();
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchSignedClosureSerialize(): void
+ {
+ $this->signedSerializer->serialize($this->closure);
+ }
+
+ #[BeforeMethods('setUp')]
+ public function benchSignedClosureUnserialize(): void
+ {
+ $this->signedSerializer->unserialize($this->signedSerializedClosure);
+ }
+}
+
+final class RuntimeInvokable
+{
+ public function __invoke(): int
+ {
+ return 1;
+ }
+}
+
+final class RuntimeLockedMacros
+{
+ use MacroMix;
+
+ public const ENABLE_LOCK = true;
+}
+
+final class RuntimeMixin
+{
+ public function first(): int
+ {
+ return 1;
+ }
+
+ public function second(): int
+ {
+ return 2;
+ }
+}
+
+final class RuntimeStaticTarget
+{
+ public static function run(): int
+ {
+ return 1;
+ }
+}
+
+final class RuntimeUnlockedMacros
+{
+ use MacroMix;
+}
diff --git a/composer.json b/composer.json
index d41f3bd..4aa07bc 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "infocyph/intermix",
- "description": "A lightweight PHP DI container, invoker, serializer, and utility toolkit.",
+ "description": "A lightweight, high-performance PHP dependency injection and runtime utility toolkit with invocation, scoped services, compiled resolution, Closure serialization, Fence and fluent helpers.",
"license": "MIT",
"type": "library",
"keywords": [
@@ -10,7 +10,7 @@
"dependency-injection",
"psr-11",
"invoker",
- "serializer",
+ "closure-serialization",
"attributes",
"scoped-services",
"macro",
diff --git a/docs/backstory.rst b/docs/backstory.rst
index df1644c..e540fe1 100644
--- a/docs/backstory.rst
+++ b/docs/backstory.rst
@@ -8,11 +8,12 @@ I did not build InterMix as a large framework from the beginning. I built it bec
Back in October 2020, I started with a small project called DI-Container. At that time, the goal was straightforward. I wanted a compact utility that could resolve constructors and callables cleanly, without unnecessary heaviness. It began as an experiment, but as I kept working on it, I found myself solving more than just container-related problems. Each improvement opened the door to another need, better invocation flow, more flexible resolution, cleaner abstractions and utilities that naturally belonged around the core idea.
-That was the point where I realized I was no longer building just a DI container.
+That was the point where I realized DI and invocation could remain the center of
+a small runtime toolkit.
In May 2021, that earlier work evolved into InterMix. I wanted a better foundation, something that could grow without losing the original simplicity that made the first project useful. The new direction was not about making the project bigger for the sake of being bigger. It was about giving it the structure and freedom to become a toolkit I could genuinely rely on across real applications.
-I built InterMix incrementally. There was no single moment where everything was fully designed in advance. The project grew through repeated iteration: building, using, refining, rethinking and improving. Over time, dependency injection remained the center, but the ecosystem around it expanded naturally into caching, macro-style extensibility, memoization, helper utilities, and safety-focused tools. Those additions were not random. They came from practical needs that kept appearing during development.
+I built InterMix incrementally. There was no single moment where everything was fully designed in advance. The project grew through repeated iteration: building, using, refining, rethinking and improving. Over time, dependency injection remained the center, while invocation, compiled resolution, Closure serialization, macro-style extensibility, fluent helpers, and Fence grew around it. Those additions came from practical needs that kept appearing during development.
That is really the reason InterMix exists.
diff --git a/docs/benchmark.rst b/docs/benchmark.rst
index cd80a71..964b410 100644
--- a/docs/benchmark.rst
+++ b/docs/benchmark.rst
@@ -4,9 +4,12 @@
Benchmarking InterMix
=====================
-InterMix ships with a phpbench benchmark suite at:
+InterMix ships with PhpBench suites at:
- ``benchmarks/IntermixBench.php``
+- ``benchmarks/RuntimeFeaturesBench.php``
+- ``benchmarks/CompiledResolverBench.php``
+- ``benchmarks/FenceBench.php``
Run via Composer:
@@ -27,6 +30,7 @@ What it measures
The suite covers DI paths end-to-end:
- Singleton ``get()`` hot-path throughput
+- Scoped ``get()`` and ``has()`` hot paths
- Transient object graph creation via ``make()``
- Closure invocation through container DI
- Reflected and direct transient factory resolution
@@ -35,9 +39,16 @@ The suite covers DI paths end-to-end:
- Immediate resolution via ``resolveNow()`` (class and method paths)
- Scoped lifetime behavior with ``enterScope()`` / ``leaveScope()``
- Tagged service lookup via ``findByTag()``
+- Lazy tagged iteration via ``tagged()``
- ``Invoker`` wrapper method invocation path
- ``Invoker`` static-method callable fast path
- ``Invoker`` zero-argument closure fast path
+- ``Invoker`` function, invokable object, class-string, and static-method string paths
+- Serialized Closure invocation as a cold fallback
+- Unsigned and signed Closure serialization/deserialization
+- MacroMix instance/static invocation
+- MacroMix direct and bulk registration with mutation locking on and off
+- Compiled artifact generation, boot, prevalidated boot, and resolution
- Definition-free scope seeding for ready request/job instances
- Service-provider registration path
- Environment-conditional interface binding path
diff --git a/docs/conf.py b/docs/conf.py
index e760f6d..0d126a7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -45,6 +45,7 @@ def get_version() -> str:
"sphinxcontrib.phpdomain",
"sphinx.ext.extlinks",
]
+autosectionlabel_prefix_document = True
# MyST (Markdown)
myst_enable_extensions = [
diff --git a/docs/container.rst b/docs/container.rst
index a8933da..afcdb9d 100644
--- a/docs/container.rst
+++ b/docs/container.rst
@@ -26,9 +26,9 @@ or the day you must **swap** an implementation in *production* only.
InterMix automates construction, honours interfaces, supports
environment-specific overrides and gives you sugar for 1-liners.
-------------------------------------------------
+---------------------------------------------------------
15-second “Hello World” with constructor autowire
-------------------------------------------------
+---------------------------------------------------------
.. code-block:: php
@@ -74,7 +74,7 @@ CLI workers, micro-modules, etc.
Container Features
--------------------------------
-* **Scoped services** – ``enterScope()``, ``leaveScope()``, ``withinScope()`` for request/fiber isolation
+* **Scoped services** – ``enterScope()``, ``leaveScope()``, ``withinScope()`` for logical request or job scopes
* **Dependency graph export** – ``exportGraph()`` for debugging and analysis
* **Environment-specific bindings** – ``setEnvironment()`` for different deployment configs
* **Debug tracing** – ``debug()`` and ``tracer()`` for development insight
@@ -82,12 +82,25 @@ Container Features
* **Container locking** – ``lock()`` to prevent runtime modifications
* **Multiple container instances** – ``Container::instance($alias)`` for isolated contexts
----------------------------------------------
+Container API Classification
+----------------------------
+
+The supported surface is grouped by intended use:
+
+* **Core** – ``get()``, ``has()``, ``bind()``, ``singleton()``, ``scoped()``,
+ ``transient()``, ``make()``, ``call()``, ``factory()``, and ``withinScope()``.
+* **Advanced** – compiled/prevalidated resolution, validation, pipelines,
+ lifecycle hooks, contextual/environment bindings, graph export, and tracing.
+* **Internal/tooling** – repository access, resolver selection, and callable
+ parsing. Public methods in this group exist for package tooling and are marked
+ ``@internal``; they are not quick-start application APIs.
+
+----------------------------------------------------------
Modifying behaviour with ``options()->setOptions()``
----------------------------------------------
+----------------------------------------------------------
+ **injection** – reflection autowiring engine
-+ **methodAttributes / propertyAttributes** – enable ``#[Infuse]``
++ **methodAttributes / propertyAttributes** – enable ``#[Inject]``
+ **defaultMethod** – method to call when none supplied
+ **lazyLoading** – defer heavy construction until first use
@@ -128,9 +141,9 @@ Common quick patterns (copy-paste as you learn the rest)
// scoped services now unique to this request
$c->leaveScope();
------------
+--------------------
Dive in Details
------------
+--------------------
Dive into the detailed sub-chapters. Happy mixing! Questions? Open an issue or drop by the discussion board.
diff --git a/docs/di/attribute-deferred-initializer.rst b/docs/di/attribute-deferred-initializer.rst
deleted file mode 100644
index 55278f6..0000000
--- a/docs/di/attribute-deferred-initializer.rst
+++ /dev/null
@@ -1,49 +0,0 @@
-.. _di.attribute.deferred_initializer:
-=========================================
-DeferredInitializer (Lazy Wrapper)
-=========================================
-
-``DeferredInitializer`` is an internal lazy wrapper used by InterMix when
-lazy-loading is enabled.
-
-It is **not** a PHP attribute annotation. Instead, the container stores this
-wrapper for eligible definitions and resolves the real service on first access.
-
-**Usage**
-
-.. code-block:: php
-
- use function Infocyph\InterMix\container;
-
- $c = container();
- $c->options()->enableLazyLoading(true);
- $c->definitions()->bind('expensive', ExpensiveService::class);
-
- // first get() triggers the deferred initializer internally
- $svc = $c->get('expensive');
-
-**Behavior**
-
-- The container may store a ``DeferredInitializer`` placeholder for class/array
- definitions while lazy loading is enabled.
-- The first time the service is accessed, the real constructor/factory is executed.
-- Subsequent accesses return the already-created instance
-- This pattern is useful for services that are expensive to initialize but may not
- always be needed during a request
-
-**Integration with Container**
-
-The wrapper is created by the container's lazy loading mechanism:
-
-.. code-block:: php
-
- $container = Container::instance('lazy-demo');
- $container->options()->enableLazyLoading(true);
- $container->definitions()->bind(LazyService::class, LazyService::class);
-
- $lazyService = $container->get(LazyService::class); // resolves on first access
-
-**See Also**
-
-- :doc:`attribute`
-- :doc:`lazy_loading`
diff --git a/docs/di/attribute.rst b/docs/di/attribute.rst
index 0f20e7c..95a13c3 100644
--- a/docs/di/attribute.rst
+++ b/docs/di/attribute.rst
@@ -4,237 +4,114 @@
Attribute Injection
===================
-InterMix supports **PHP 8+ native attributes** for expressive, declarative
-injection.
-Two families exist:
+InterMix provides one canonical injection attribute: ``#[Inject]``. It supports
+properties, parameters, and whole-method defaults without maintaining synonymous
+public APIs.
-* **Built-in** tags shipped with InterMix (``Infuse`` / ``Autowire`` / ``Inject``)
-* **Custom** attributes you register at runtime through
- :php:meth:`Infocyph\\InterMix\\DI\\Attribute\\AttributeRegistry::register`
+Built-in Injection
+==================
--------------------------------------------------
-Built-in Tags (Infuse / Autowire / Inject)
--------------------------------------------------
+.. code-block:: php
-* ``#[Infuse]`` – canonical
-* ``#[Autowire]`` – Spring-style alias
-* ``#[Inject]`` – common DI alias
-* ``#[DeferredInitializer]`` – marks classes for lazy initialization
+ use Infocyph\InterMix\DI\Attribute\Inject;
-They are **identical** and can inject via :
+ final class Service
+ {
+ #[Inject]
+ private LoggerInterface $logger;
-* **Type-hint** (class / interface)
-* **Container key** (``'cfg.debug'``, ``'db.host'``, …)
-* **Global callable** (e.g. ``#[Inject(strtotime: 'next monday')]``)
+ #[Inject('cfg.debug')]
+ private bool $debug;
--------------------------------------------------
-Quick syntax
--------------------------------------------------
+ #[Inject(strtotime: '+1 day')]
+ private int $expires;
-.. code-block:: php
+ #[Inject(retries: 2)]
+ public function run(
+ int $retries,
+ #[Inject('cfg.env')] string $environment,
+ ): void {}
+ }
- use Infocyph\InterMix\DI\Attribute\{Infuse, Autowire, Inject};
+``#[Inject]`` without arguments resolves by type. A positional string selects a
+container definition, function, class, or interface. Named arguments provide
+whole-method defaults or function arguments.
- class Service {
- #[Infuse] private LoggerInterface $logger; // by type
- #[Autowire('cfg.debug')] private bool $debug; // by key
- #[Inject(strtotime: '+1 day')] private int $expires; // via function
- }
+Enable only the resolution surfaces the application uses:
- class App {
- #[Infuse(user: 'admin')] // method-level default
- public function boot(
- #[Inject('cfg.env')] string $env // parameter-level override
- ) {}
- }
+.. code-block:: php
+
+ $container->options()->setOptions(
+ injection: true,
+ methodAttributes: true,
+ propertyAttributes: true,
+ );
-.. toctree::
- :maxdepth: 1
- :hidden:
- attribute-deferred-initializer
+Explicit values registered through ``registerClass()``, ``registerMethod()``, or
+``registerProperty()`` take priority over attribute values.
--------------------------------------------------
-Custom Attribute Support
--------------------------------------------------
+Custom Attributes
+=================
-Create any attribute & a resolver that implements
-:php:class:`Infocyph\\InterMix\\DI\\Attribute\\AttributeResolverInterface`.
+Custom resolvers implement ``AttributeResolverInterface`` and are registered at
+bootstrap.
.. code-block:: php
+ use Infocyph\InterMix\DI\Attribute\AttributeResolution;
+ use Infocyph\InterMix\DI\Attribute\AttributeResolverInterface;
+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
- class UpperCase {
+ final readonly class UpperCase
+ {
public function __construct(public string $text) {}
}
- use Infocyph\InterMix\DI\Attribute\AttributeResolverInterface;
-
- class UpperCaseResolver implements AttributeResolverInterface {
+ final class UpperCaseResolver implements AttributeResolverInterface
+ {
public function resolve(
- object $attribute,
- Reflector $target,
- Container $c
+ object $attribute,
+ Reflector $target,
+ Container $container,
): mixed {
return strtoupper($attribute->text);
}
}
- // Register once during bootstrap
- $c->attributeRegistry()->register(
+ $container->attributeRegistry()->register(
UpperCase::class,
- UpperCaseResolver::class
+ UpperCaseResolver::class,
);
-Usage:
+A resolver may inject any value, including ``null``. It returns
+``AttributeResolution::Unresolved`` only when it declines to provide a value:
.. code-block:: php
- class Banner {
- #[UpperCase('hello world')]
- public string $title;
- }
-
- echo $c->get(Banner::class)->title; // HELLO WORLD
-
-.. hint::
-
- *Multiple* attributes may decorate the **same** target.
- InterMix calls each registered resolver in discovery order; the **first**
- non-null, non-``IMStdClass`` result becomes the injected value.
- Later resolvers can still run side-effect logic even if they don’t inject.
-
--------------------------------------------------
-Method & Parameter Injection
--------------------------------------------------
+ public function resolve(
+ object $attribute,
+ Reflector $target,
+ Container $container,
+ ): mixed {
+ $container->get('logger')->notice($target->getName());
-.. code-block:: php
-
- class Mailer {
- public function send(
- #[Infuse('cfg.smtp')] array $config,
- #[Inject] LoggerInterface $log
- ) {}
- }
-
-Whole-method defaults:
-
-.. code-block:: php
-
- class Worker {
- #[Autowire(retries: 2, delay: 5)]
- public function execute(int $retries, int $delay) {}
- }
-
-*Arguments provided* via :php:meth:`Infocyph\\InterMix\\DI\\Container::call`,
-:php:meth:`Infocyph\\InterMix\\DI\\Managers\\RegistrationManager::registerMethod`
-or explicit arrays always **override** attributes.
-
--------------------------------------------------
-Property Injection
--------------------------------------------------
-
-Enable with ``propertyAttributes: true``:
-
-.. code-block:: php
-
- class Controller {
- #[Infuse] private Request $request; // by type
- #[Autowire('cfg.csrf')] private string $csrf; // by key
- #[UpperCase('admin')] private string $role; // custom
+ return AttributeResolution::Unresolved;
}
-Properties are injected *after* construction.
-Values set via :php:meth:`Infocyph\\InterMix\\DI\\Managers\\RegistrationManager::registerProperty`
-win over attributes.
-
--------------------------------------------------
-Resolution Workflow
--------------------------------------------------
-
-#. **Built-in tag** (``Infuse`` / ``Autowire`` / ``Inject``) – first match wins
-#. **Custom attributes** – executed in registration order:
+For multiple registered attributes on one target, the first value other than the
+unresolved sentinel is injected. Registered resolvers may still perform explicit
+logic before declining.
- * if a resolver returns **non-null & not ``IMStdClass``** → injected
- * if resolver returns ``null`` or ``IMStdClass`` → treated as “logic-only”
-
--------------------------------------------------
-Enabling Attributes
--------------------------------------------------
-
-.. code-block:: php
-
- $c->options()->setOptions(
- injection: true,
- methodAttributes: true, // enable #[Infuse] on params / methods
- propertyAttributes: true // enable #[Infuse] on properties
- );
-
-You may enable only one flag to limit scope.
-
--------------------------------------------------
-Resolution Priority (high → low)
--------------------------------------------------
-
-1. ``registerClass()`` / ``registerMethod()`` / ``registerProperty()``
-2. Supplied args (``call()``, ``make()``, etc.)
-3. ``definitions()`` map
-4. Built-in tags (Infuse / Autowire / Inject)
-5. Custom attributes via **AttributeRegistry**
-
--------------------------------------------------
-Examples
--------------------------------------------------
-
-Inject scalar config:
-
-.. code-block:: php
-
- class Analytics {
- #[Inject('cfg.api_key')] private string $apiKey;
- }
-
-Global callable:
-
-.. code-block:: php
-
- class Session {
- #[Infuse('uuid_create')] private string $sessionId;
- }
-
-Logic-only attribute (no injection):
-
-.. code-block:: php
-
- #[Attribute(Attribute::TARGET_METHOD)]
- class LogCall {
- public function __construct(public string $level = 'info') {}
- }
-
- class LogCallResolver implements AttributeResolverInterface {
- public function resolve(object $attr, Reflector $target, Container $c): mixed {
- $c->get('logger')->log($attr->level, "[DI] $target handled");
- return null; // no injection, marks as handled
- }
- }
-
--------------------------------------------------
-Debugging
--------------------------------------------------
-
-.. code-block:: php
-
- $c->options()->enableDebugTracing(true);
- $c->get(MyService::class);
- print_r($c->debug(MyService::class));
+Resolution Priority
+===================
--------------------------------------------------
-Summary
--------------------------------------------------
+1. Values registered for the class, method, or property
+2. Arguments supplied to ``call()`` or ``make()``
+3. Container definitions
+4. ``#[Inject]``
+5. Registered custom attribute resolvers
-* Built-in tags: **Infuse / Autowire / Inject**
-* Register unlimited **custom** attributes with resolvers
-* Works on properties, parameters, or whole methods
-* First non-null result wins; others may perform side-effects only
-* Fully traceable with ``enableDebugTracing()``
+The lazy initializer used internally by the container is not an attribute or a
+supported application API.
Next → :ref:`di.lifetimes`
diff --git a/docs/di/cache.rst b/docs/di/cache.rst
index e206a5a..f2f58f2 100644
--- a/docs/di/cache.rst
+++ b/docs/di/cache.rst
@@ -14,9 +14,9 @@ Why care?
* **Predictability** – warm and reuse cacheable definition data.
* **Flexibility** – use any PSR-6 implementation (for example from ``infocyph/cachelayer``).
----------------
+--------------------
Quick Example 🚀
----------------
+--------------------
.. code-block:: php
@@ -53,13 +53,19 @@ Eager warm-up:
One-liner Cheat-Sheet
---------------------
-=========== =================================================================
-Action Code
-=========== =================================================================
-Assign pool ``$c->definitions()->enableDefinitionCache($pool)``
-Warm all ``$c->definitions()->cacheAllDefinitions()``
-Clear + warm ``…->cacheAllDefinitions(forceClearFirst:true)``
-Disable Omit ``enableDefinitionCache(...)``
-=========== =================================================================
+.. list-table::
+ :header-rows: 1
+ :widths: 20 80
+
+ * - Action
+ - Code
+ * - Assign pool
+ - ``$c->definitions()->enableDefinitionCache($pool)``
+ * - Warm all
+ - ``$c->definitions()->cacheAllDefinitions()``
+ * - Clear + warm
+ - ``…->cacheAllDefinitions(forceClearFirst:true)``
+ * - Disable
+ - Omit ``enableDefinitionCache(...)``
Next stop » :doc:`debug_tracing`
diff --git a/docs/di/debug_tracing.rst b/docs/di/debug_tracing.rst
index b5ca7ac..bb08c39 100644
--- a/docs/di/debug_tracing.rst
+++ b/docs/di/debug_tracing.rst
@@ -33,9 +33,9 @@ repository flag; the tracer and its entry/graph arrays are created only when
tracing is explicitly requested. Dependency edges reuse the resolver's existing
cycle-detection stacks rather than maintaining another resolution stack.
----------------------
+--------------------------
See the Trace Output 👀
----------------------
+--------------------------
.. code-block:: php
diff --git a/docs/di/definitions.rst b/docs/di/definitions.rst
index f9cff5d..95d9119 100644
--- a/docs/di/definitions.rst
+++ b/docs/di/definitions.rst
@@ -163,7 +163,7 @@ Toggle globally:
$c->options()->enableLazyLoading(false); // eager – resolve immediately
-User-supplied **closures** are **not wrapped** in ``DeferredInitializer``. They
+User-supplied **closures** are **not wrapped** in another deferred object. They
execute when the ID is resolved (for example on first ``get()`` for singleton/scoped,
or every ``get()`` for transient), not at bind-time.
diff --git a/docs/di/environment.rst b/docs/di/environment.rst
index 74622a1..73ee25e 100644
--- a/docs/di/environment.rst
+++ b/docs/di/environment.rst
@@ -79,7 +79,7 @@ Resolution priority:
1. **Environment-bound class** (if active)
2. **Globally bound class** via
:php:meth:`Infocyph\\InterMix\\DI\\Managers\\DefinitionManager::bind`
-3. **Autowire fallback** (if ``injection=true``)
+3. **Autowiring fallback** (if ``injection=true``)
-------------------
Best Practices 💡
diff --git a/docs/di/invocation.rst b/docs/di/invocation.rst
index 9e5d046..7e836af 100644
--- a/docs/di/invocation.rst
+++ b/docs/di/invocation.rst
@@ -98,14 +98,30 @@ apply everywhere.
Reference Table
-------------------------------------------------------
-================= ============== ========= =====================================================
-Helper Caches object? Returns Typical use-case
-================= ============== ========= =====================================================
-``get($id)`` **Yes** object Core PSR-11 retrieval.
-``call()`` n/a mixed Invoke a callable with DI.
-``getReturn()`` **Yes** mixed Reads cached singleton method result; use ``call()``/``make()`` for fresh execution.
-``make()`` **No** object\|mixed Factory pattern, transient workflows.
-================= ============== ========= =====================================================
+.. list-table::
+ :header-rows: 1
+ :widths: 20 15 15 50
+
+ * - Helper
+ - Caches object?
+ - Returns
+ - Typical use-case
+ * - ``get($id)``
+ - **Yes**
+ - object
+ - Core PSR-11 retrieval.
+ * - ``call()``
+ - n/a
+ - mixed
+ - Invoke a callable with DI.
+ * - ``getReturn()``
+ - **Yes**
+ - mixed
+ - Reads cached singleton method result; use ``call()``/``make()`` for fresh execution.
+ * - ``make()``
+ - **No**
+ - object\|mixed
+ - Factory pattern, transient workflows.
See also: :ref:`di.options` (``defaultMethod``), :ref:`di.registration`
(for pre-registered class metadata).
diff --git a/docs/di/invoker.rst b/docs/di/invoker.rst
index 3e18842..f2c76ec 100644
--- a/docs/di/invoker.rst
+++ b/docs/di/invoker.rst
@@ -34,10 +34,8 @@ Key Features
- Build object plus optionally call a method
* - ``resolve()``
- Retrieve from container by key
- * - ``serialize()``
- - Serialize closures and values
- * - ``unserialize()``
- - Restore serialized closures or data
+ * - ``callableFor()``
+ - Resolve and cache an invokable class for this Invoker instance
---------------
Usage Examples
@@ -63,8 +61,10 @@ the container so constructor and method dependencies can be resolved.
.. code-block:: php
- $packed = $invoker->serialize(fn () => 42);
- $fn = $invoker->unserialize($packed);
+ use Infocyph\InterMix\Serializer\ClosureSerializer;
+
+ $packed = ClosureSerializer::serialize(fn () => 42);
+ $fn = ClosureSerializer::unserialize($packed);
echo $fn(); // 42
**4. Shared global instance**
@@ -78,12 +78,18 @@ the container so constructor and method dependencies can be resolved.
Internals
---------
-The invoker uses:
+The invoker routes common callables in this order:
+
+- native closures
+- invokable objects
+- callable arrays
+- function strings
+- static ``Class::method`` strings
+- class strings
+- unsigned InterMix Closure envelopes as a cold fallback
-- a direct callable path for static ``[class, method]`` targets
-- ``routeCallable()`` — detects callable types: closures, invokable classes, strings, or serialized closures
-- ``viaClosure()`` — injects closures into the container for contextual execution
-- Integration with ``ValueSerializer`` for full closure support
+Normal callable routing performs no Opis, payload decoding, signing, or HMAC
+work. Serialization remains the responsibility of ``ClosureSerializer``.
--------
diff --git a/docs/di/lazy_loading.rst b/docs/di/lazy_loading.rst
index 8bca45f..3af9f90 100644
--- a/docs/di/lazy_loading.rst
+++ b/docs/di/lazy_loading.rst
@@ -6,7 +6,7 @@ Lazy Loading
Lazy loading delays service construction until the **first time** you access it.
Instead of creating the object right away, InterMix stores a lightweight
-:php:class:`DeferredInitializer`.
+internal deferred entry.
When enabled (default), this reduces **startup cost** for services that might
never be used in a request or command.
@@ -22,7 +22,7 @@ How It Works ⚙️
BigService::class
);
-With lazy loading **on**, the container stores a proxy (``DeferredInitializer``)
+With lazy loading **on**, the container stores an internal initializer
for class/array-style definitions and resolves it when ``get()`` is first called.
-----------------------------------------
@@ -48,7 +48,7 @@ Default Rules
* - Array definition
- Yes
* - User closure plus Singleton/Scoped
- - Resolved on first ``get()`` (no ``DeferredInitializer`` wrapper)
+ - Resolved on first ``get()``
* - User closure plus Transient
- No caching (runs each ``get()``)
@@ -56,7 +56,7 @@ Default Rules
Why not all?
---------------
-User closures are not wrapped in ``DeferredInitializer``. They run when the
+User closures are not wrapped in an additional deferred object. They run when the
service is resolved and their reuse depends on lifetime (singleton/scoped cache
the resolved value; transient does not).
diff --git a/docs/di/options.rst b/docs/di/options.rst
index ef40718..e6d538c 100644
--- a/docs/di/options.rst
+++ b/docs/di/options.rst
@@ -27,20 +27,29 @@ Because this manager proxies container access, you can also call container APIs
1 · setOptions( injection , methodAttributes , … )
----------------------------------------------------
-====================== ========= ==========================================================
-Flag Default What it does
-====================== ========= ==========================================================
-``injection`` ``true`` Turn the **reflection autowiring engine** on / off.
- When *false* the container switches to ``GenericCall``
- and **every** dependency must be supplied via
- :ref:`di.registration` or an explicit direct factory.
-``methodAttributes`` ``false`` Honour ``#[Infuse]`` on **method parameters**.
-``propertyAttributes`` ``false`` Honour ``#[Infuse]`` on **class properties**.
-``defaultMethod`` ``null`` If you call ``getReturn(Foo::class)`` **without** a
- registered method, the container will execute this method
- on the freshly-built instance (e.g. ``'__invoke'`` or
- ``'handle'`` in CQRS/HTTP handlers).
-====================== ========= ==========================================================
+.. list-table::
+ :header-rows: 1
+ :widths: 25 15 60
+
+ * - Flag
+ - Default
+ - What it does
+ * - ``injection``
+ - ``true``
+ - Turn the **reflection autowiring engine** on or off. When *false*, the
+ container switches to ``GenericCall`` and **every** dependency must be
+ supplied via :ref:`di.registration` or an explicit direct factory.
+ * - ``methodAttributes``
+ - ``false``
+ - Honour ``#[Inject]`` on **method parameters**.
+ * - ``propertyAttributes``
+ - ``false``
+ - Honour ``#[Inject]`` on **class properties**.
+ * - ``defaultMethod``
+ - ``null``
+ - If you call ``getReturn(Foo::class)`` **without** a registered method,
+ the container executes this method on the freshly built instance (for
+ example, ``'__invoke'`` or ``'handle'`` in CQRS/HTTP handlers).
*Signature* (named arguments supported) ::
@@ -66,7 +75,7 @@ container and never reflects or autowires the factory parameters.
* - Helper
- Effect
* - ``enableLazyLoading(true)``
- - Store class bindings as ``DeferredInitializer`` until first ``get($id)``; reduces boot cost.
+ - Store class bindings as internal deferred entries until first ``get($id)``; reduces boot cost.
* - ``setEnvironment('prod')``
- Select the active environment; used with ``bindInterfaceForEnv()`` to swap implementations.
* - ``bindInterfaceForEnv($env, I::class, C::class)``
diff --git a/docs/di/overview.rst b/docs/di/overview.rst
index 1793b9b..4951d6c 100644
--- a/docs/di/overview.rst
+++ b/docs/di/overview.rst
@@ -13,7 +13,7 @@ Why another container?
* **Simple first** – one-liner definitions, no config files
* **Reflection-aware** – autowiring you can *switch off*
-* **Attribute powered** – ``#[Infuse]``, ``#[Autowire]``, ``#[Inject]``
+* **Attribute powered** – one canonical ``#[Inject]`` API
* **Fluent API** – four tiny managers that chain like one object
* **Performant** – static reflection cache, parameter-resolution planning cache,
optional PSR-6/16 cache,
@@ -112,7 +112,7 @@ needed.
#. **Already resolved?**
* Return immediately if found in the in-memory cache.
- * If the cache entry is a :php:class:`DeferredInitializer` *lazy* wrapper, execute it now and swap in the real object.
+ * If the cache entry is an internal deferred value, initialize it now and swap in the real object.
#. **FunctionReference lookup**
@@ -141,12 +141,14 @@ User closure vs. lazy
$c->definitions()->bind('heavy', fn () => new Expensive());
- is executed **immediately** – you asked for a closure.
+ executes when the definition is resolved. Singleton and scoped lifetimes
+ reuse the resolved value; transient definitions execute on every resolution.
-* **DeferredInitializer**
+* **Internal deferred entry**
For class strings/arrays *and* ``enableLazyLoading(true)``, InterMix stores a
- small wrapper and postpones construction until the first real ``get()``.
+ small internal initializer and postpones construction until the first real
+ ``get()``. This implementation type is not a public attribute or application API.
Concurrency note
~~~~~~~~~~~~~~~~
diff --git a/docs/di/quickstart.rst b/docs/di/quickstart.rst
index 97b578f..75bca54 100644
--- a/docs/di/quickstart.rst
+++ b/docs/di/quickstart.rst
@@ -36,8 +36,8 @@ Configure behaviour (optional)
$c1->options()->setOptions(
injection: true, // reflection autowiring engine
- methodAttributes: true, // honour #[Infuse] on parameters
- propertyAttributes: true, // honour #[Infuse] on properties
+ methodAttributes: true, // honour #[Inject] on parameters
+ propertyAttributes: true, // honour #[Inject] on properties
defaultMethod: 'handle' // fallback method name
);
@@ -116,7 +116,7 @@ Resolve
echo $c1->get('answer'); // 42
echo $c1->get('now')->format('c'); // 2025-06-18T12:34:56+00:00
-Autowire a class (constructor injection)::
+Resolve a class through constructor injection::
class Greeter
{
@@ -135,12 +135,12 @@ A taste of attributes
.. code-block:: php
- use Infocyph\InterMix\DI\Attribute\Infuse;
+ use Infocyph\InterMix\DI\Attribute\Inject;
class Mailer
{
- #[Infuse] private LoggerInterface $logger;
- public function __construct(#[Infuse('cfg.smtp')] string $dsn = 'smtp://localhost') {}
+ #[Inject] private LoggerInterface $logger;
+ public function __construct(#[Inject('cfg.smtp')] string $dsn = 'smtp://localhost') {}
}
$c1->definitions()
diff --git a/docs/di/registration.rst b/docs/di/registration.rst
index 3134ea3..233651e 100644
--- a/docs/di/registration.rst
+++ b/docs/di/registration.rst
@@ -49,7 +49,7 @@ Hints
^^^^^
* Un-listed parameters fall back to **autowiring** (if enabled) or
- ``#[Infuse]`` attributes.
+ ``#[Inject]`` attributes.
* The registration **overrides** anything the attribute would set for the
same parameter.
@@ -76,11 +76,11 @@ When you later do:
the container:
1. Builds ``EmailService``
-2. Injects the supplied parameters (plus Infuse fallbacks)
+2. Injects the supplied parameters (plus Inject fallbacks)
3. Executes ``setConfig()``
4. Stores/returns the **configured instance**
-**Tip** | You can omit ``$args`` to rely solely on ``#[Infuse]`` in the
+**Tip** | You can omit ``$args`` to rely solely on ``#[Inject]`` in the
method signature.
------------------------------------------------------------------
@@ -101,7 +101,7 @@ gymnastics.
Precedence (highest → lowest):
1. **registerProperty()**
-2. ``#[Infuse]`` on the property (if propertyAttributes = true)
+2. ``#[Inject]`` on the property (if propertyAttributes = true)
3. Do nothing (property remains untouched)
------------------------------------------------------------------
diff --git a/docs/di/scopes.rst b/docs/di/scopes.rst
index 2239aa0..3a9002d 100644
--- a/docs/di/scopes.rst
+++ b/docs/di/scopes.rst
@@ -15,9 +15,13 @@ Typical use-cases
* **HTTP request** ID – isolate per-request state or caches.
* **CLI job** / **queue worker** – reuse expensive objects during the job but
not across jobs.
-* **Fiber / coroutine** – give each fiber its own contextual dependencies.
* **Multi-tenant apps** – tag each tenant with their customer ID.
+Scopes are logical container scopes for synchronous request/job execution and
+explicitly isolated container instances. InterMix does not provide automatic
+Fiber- or coroutine-local storage; concurrent contexts must use isolated
+container instances or application-managed execution-context storage.
+
API
---
@@ -31,7 +35,7 @@ Switching scope *never* clears non-scoped singletons; only services bound with
``LifetimeEnum::Scoped`` are affected.
Example 🍰
----------
+--------------------
.. code-block:: php
@@ -94,7 +98,7 @@ The third argument to ``withinScope()``—and the second argument to
* do not modify definition metadata or lifetime caches.
Best practices 💡
-----------------
+--------------------
* **Keep scopes short-lived** – usually the lifetime of a single request or job.
* **Avoid cross-scope leakage** – pass *IDs* or *DTOs* between scopes, not the
diff --git a/docs/fence.rst b/docs/fence.rst
index cf47a31..43d1a2f 100644
--- a/docs/fence.rst
+++ b/docs/fence.rst
@@ -230,7 +230,7 @@ Applying requirements::
Best Practices
--------------
-* **Always call ``::instance()`` ** instead of ``new``.
+* **Always call ``::instance()``** instead of ``new``.
* If your class must remain a singleton, use ``Single``.
* If you need per‐key instances, use ``Multi``.
* If you want to cap how many objects can coexist, use ``Limit``.
diff --git a/docs/index.rst b/docs/index.rst
index 945ac18..f0e86d8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -23,4 +23,3 @@ Contents
remix
functions
benchmark
- di/attribute-deferred-initializer
diff --git a/docs/remix/macro-mix.rst b/docs/remix/macro-mix.rst
index 63f3257..7995db1 100644
--- a/docs/remix/macro-mix.rst
+++ b/docs/remix/macro-mix.rst
@@ -17,7 +17,7 @@ Advantages
``$this`` so you can continue chaining.
- **Config or annotation loading** — Bulk-load macros from arrays or
``@Macro("name")`` annotations.
-- **Thread safety** (optional) — Enable a lock if you care about concurrency.
+- **Optional mutation locking** — Serialize registry writes when required.
Basic Usage
===========
@@ -34,7 +34,7 @@ Include the trait in any class:
{
use MacroMix;
- // Optional: enable thread-safe macro registration
+ // Optional: serialize registry mutations
public const ENABLE_LOCK = true;
protected string $color = 'gold';
@@ -226,8 +226,8 @@ Results in:
Exception: Method App\House::nonexistent does not exist.
-Thread Safety
-=============
+Mutation Locking
+================
If you define the constant:
@@ -238,7 +238,14 @@ If you define the constant:
public const ENABLE_LOCK = true;
}
-Then:
+When ``ENABLE_LOCK`` is missing or false, mutations take the direct path. When
+it is true, ``macro()``, ``removeMacro()``, ``mix()``,
+``loadMacrosFromConfig()``, and ``loadMacrosFromAnnotations()`` use one
+class-specific portable lock. Bulk operations acquire the lock once for the
+complete registry update.
-- Write operations (``macro()``, ``removeMacro()``, ``loadMacrosFromConfig()``) acquire an exclusive file lock on the trait source.
-- Read operations (e.g. ``hasMacro()``, ``getMacros()``, macro calls) skip locking.
+``hasMacro()``, ``getMacros()``, ``__call()``, ``__callStatic()``, and normal
+macro execution remain lock-free, even when mutation locking is enabled.
+
+The lock serializes MacroMix registry mutations. It does not make process-local
+macro registries shared between independent PHP processes.
diff --git a/docs/serializer.rst b/docs/serializer.rst
index 75f4561..2324680 100644
--- a/docs/serializer.rst
+++ b/docs/serializer.rst
@@ -1,16 +1,52 @@
.. _serializer:
+=====================
+Closure Serialization
+=====================
+
+InterMix serializes only ``Closure`` objects, because ordinary PHP values already
+have native serialization facilities. Resource serialization is application or
+specialized-package responsibility.
+
+Unsigned Closures
+=================
+
+.. code-block:: php
+
+ use Infocyph\InterMix\Serializer\ClosureSerializer;
+
+ $payload = ClosureSerializer::serialize(
+ static fn (int $value): int => $value * 2,
+ );
+
+ if (ClosureSerializer::isSerialized($payload)) {
+ $closure = ClosureSerializer::unserialize($payload);
+ }
+
+Unsigned payloads use the versioned ``imxc1.`` envelope. Recognition is a
+constant-time prefix check; it does not inspect Opis internals.
+
+Signed Closures
===============
-Serializer API
-===============
-The **ValueSerializer** and **ResourceHandlers** classes let you
-serialize arbitrary PHP values—including closures and resources—
-using ``Opis Closure v4`` under the hood, plus a simple plugin system
-for wrapping/unwrapping resources.
+Executable closures transported through a queue, database, cache, IPC, or a
+remote system should be authenticated when the transport does not already
+provide equivalent integrity protection.
+
+.. code-block:: php
+
+ $serializer = ClosureSerializer::signed($_ENV['APP_KEY']);
+ $payload = $serializer->serialize(static fn (): string => 'work');
+ $closure = $serializer->unserialize($payload);
+
+Signed payloads use the ``imxcs1.`` envelope and HMAC-SHA-256. Signing keys are
+held by the serializer instance; InterMix has no process-global signing state.
+Unsigned serializers reject signed payloads, signed serializers reject unsigned
+payloads, and verification occurs before executable data is decoded.
-.. toctree::
- :maxdepth: 1
+Invocation Boundary
+===================
- serializer/value_serializer
- serializer/resource_handlers
+``Invoker`` accepts an InterMix unsigned Closure payload only as a cold string
+fallback. Native closures, invokable objects, functions, static methods, and
+class strings never perform Opis, payload decoding, or HMAC work.
diff --git a/docs/serializer/resource_handlers.rst b/docs/serializer/resource_handlers.rst
deleted file mode 100644
index 2e7f26b..0000000
--- a/docs/serializer/resource_handlers.rst
+++ /dev/null
@@ -1,181 +0,0 @@
-.. _serializer.resource_handlers:
-
-=====================
-ResourceHandlers
-=====================
-
-``ResourceHandlers`` is a **convenience base class** intended to help you register multiple
-resource handlers via a single static call (``registerDefaults()``).
-
-It does **not** itself define any handlers; instead, you must extend it and
-implement named ``registerXxx()`` methods. When you call
-``YourSubclass::registerDefaults()``, it automatically invokes every static
-method that begins with ``"register"``.
-
-Why?
-----
-
-When your application or library needs to support *many* resource types (e.g.
-streams, cURL handles, XML parsers, GD images, sockets, etc.), you can:
-
-1. Group them in one “bundle” class that extends ``ResourceHandlers``.
-2. Inside it, write one ``registerXxx()`` method per resource type:
- - e.g. ``registerStream()``, ``registerCurl()``, ``registerXmlParser()``, etc.
-3. Call ``YourSubclass::registerDefaults()`` to register all of them at once.
-
-This approach keeps your resource‐wrapper logic organized and self-documenting.
-
-Class API
----------
-
-.. php:function:: void ResourceHandlers::registerDefaults()
-
- Iterates over all public static methods on ``self::class`` whose names start
- with ``"register"``, except ``registerDefaults`` itself and invokes each one.
- In other words, every ``registerXxx()`` method in your subclass runs.
-
- Does nothing if no ``registerXxx()`` methods exist.
-
-You **cannot** instantiate ``ResourceHandlers`` (its constructor is private).
-It exists only to host static registration methods.
-
-Example: Registering a Stream Handler
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Suppose you want to support PHP streams:
-
-.. code-block:: php
-
- namespace App\Serializer;
-
- use Infocyph\InterMix\Serializer\ResourceHandlers;
- use Infocyph\InterMix\Serializer\ValueSerializer;
-
- class MyResourceHandlers extends ResourceHandlers
- {
- /**
- * Handles stream resources (both file‐ and network‐based).
- */
- public static function registerStream(): void
- {
- ValueSerializer::registerResourceHandler(
- 'stream',
- // ---------- wrapFn -----------------------------------
- function ($res): array {
- $meta = stream_get_meta_data($res);
- rewind($res);
- return [
- 'mode' => $meta['mode'],
- 'content' => stream_get_contents($res),
- ];
- },
- // -------- restoreFn ---------------------------------
- function (array $data) {
- $s = fopen('php://memory', $data['mode']);
- fwrite($s, $data['content']);
- rewind($s);
- return $s;
- }
- );
- }
- }
-
-Once you have that subclass, you can register the handler in two ways:
-
-1. **Call ``registerStream()`` directly**:
-
- .. code-block:: php
-
- MyResourceHandlers::registerStream();
-
-2. **Call ``registerDefaults()`` to pick up every ``registerXxx()``**:
-
- .. code-block:: php
-
- MyResourceHandlers::registerDefaults();
- // → automatically calls registerStream(), plus any other registerXxx()
-
- Then use ``ValueSerializer`` as usual:
-
- .. code-block:: php
-
- $fp = fopen('php://memory','r+');
- fwrite($fp,'hello'); rewind($fp);
-
- $blob = ValueSerializer::serialize($fp);
- $restored = ValueSerializer::unserialize($blob);
- echo stream_get_contents($restored); // "hello"
-
-Adding More Resource Types
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To support additional resources:
-
-1. **Extend** ``ResourceHandlers``.
-2. **Add** a new public static method named ``registerFoo()`` where “Foo” is any
- string you like (e.g. ``registerCurl()``, ``registerXmlParser()``, etc.).
-3. Inside it, call:
-
- .. code-block:: php
-
- ValueSerializer::registerResourceHandler(
- '', // e.g. 'curl'
- fn($res): array => /* wrap logic */,
- fn(array $data) => /* restore logic */
- );
-
-4. If you want all handlers applied at once, simply call:
-
- .. code-block:: php
-
- MyResourceHandlers::registerDefaults();
-
-Example: cURL Handler
-~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: php
-
- namespace App\Serializer;
-
- use Infocyph\InterMix\Serializer\ResourceHandlers;
- use Infocyph\InterMix\Serializer\ValueSerializer;
-
- class MyResourceHandlers extends ResourceHandlers
- {
- public static function registerCurl(): void
- {
- if (!extension_loaded('curl')) {
- return;
- }
- ValueSerializer::registerResourceHandler(
- 'curl',
- // wrapFn: store only the effective URL
- function ($ch): array {
- return ['url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)];
- },
- // restoreFn: open a new cURL handle with the same URL
- function (array $data) {
- return curl_init($data['url'] ?? '');
- }
- );
- }
- }
-
- // Usage:
- MyResourceHandlers::registerDefaults();
- // now you can ValueSerializer::serialize($my_curl_handle) safely.
-
-Empty‐State and Testing
-~~~~~~~~~~~~~~~~~~~~~~~
-
-- By default, **no resource handlers** are registered. If you call
- ``ValueSerializer::wrap($someResource)`` before any ``registerResourceHandler()``,
- you get an ``InvalidArgumentException``.
-
-- You can always start with a clean slate:
-
- .. code-block:: php
-
- use Infocyph\InterMix\Serializer\ValueSerializer;
-
- ValueSerializer::clearResourceHandlers(); // removes all handlers
diff --git a/docs/serializer/value_serializer.rst b/docs/serializer/value_serializer.rst
deleted file mode 100644
index 3e00308..0000000
--- a/docs/serializer/value_serializer.rst
+++ /dev/null
@@ -1,156 +0,0 @@
-.. _serializer.value_serializer:
-
-====================
-ValueSerializer
-====================
-
-``ValueSerializer`` is a thin wrapper around **Opis Closure v4** that adds
-first-class support for PHP **resources** and now includes
-**transport-friendly helpers**.
-
-Under the hood it relies on:
-
-1. Opis Closure’s ``serialize()`` / ``unserialize()`` for closures,
- anonymous classes & deep object graphs.
-2. A **plugin system** (“resource handlers”) that can *wrap* any PHP
- **resource** into plain data and *restore* it on the way back.
-3. A tiny **memo-cache** inside
- :php:meth:`Infocyph\\InterMix\\Serializer\\ValueSerializer::isSerializedClosure`
- to detect
- Opis payloads with **O(1)** string checks.
-
-Why?
-----
-
-Plain PHP ``serialize()`` chokes on closures ***and*** resources.
-**ValueSerializer** solves both:
-
-* **Closures & objects** → delegated to Opis Closure.
-* **Resources** → you register *wrap* / *restore* callbacks.
-
-Everything becomes a safe string, ready for caches, queues, cookies, etc.
-
-Public API
-----------
-
-.. php:function:: string ValueSerializer::serialize(mixed $value)
-
- Convert any PHP value into a binary string.
- Throws ``InvalidArgumentException`` if an un-handled resource is found.
-
-.. php:function:: mixed ValueSerializer::unserialize(string $blob)
-
- Reverse of ``serialize()`` – returns the original value.
-
-.. php:function:: string ValueSerializer::encode(mixed $value, bool $base64 = true)
-
- – Convenience wrapper around ``serialize()``.
- If ``$base64`` is *true* (default) the binary blob is passed through
- ``base64_encode()`` – perfect for URLs, JSON, headers, etc.
-
-.. php:function:: mixed ValueSerializer::decode(string $payload, bool $base64 = true)
-
- – Rebuild a value produced by ``encode()``.
- Decodes base64 (when enabled) and forwards to ``unserialize()``.
-
-.. php:function:: string ValueSerializer::encodeSigned(mixed $value, string $key, bool $base64 = true)
-
- Encode with an operation-scoped HMAC key without changing global serializer state.
-
-.. php:function:: mixed ValueSerializer::decodeSigned(string $payload, string $key, bool $base64 = true)
-
- Verify and decode with an operation-scoped HMAC key.
-
-.. php:function:: void ValueSerializer::setPayloadSigningKey(?string $key)
-
- Configure HMAC payload signing/verification for ``serialize()``/``unserialize()``
- and ``encode()``/``decode()``.
-
- - ``null`` disables signing checks (backward-compatible mode).
- - non-null enables signed payload mode (HMAC-SHA256 envelope required on decode).
-
-.. php:function:: bool ValueSerializer::isSerializedClosure(string $str)
-
- Cheap Opis payload detector used by *Invoker*.
- Internally uses a bounded process-local memo.
-
-.. php:function:: mixed ValueSerializer::wrap(mixed $value)
-
- Wrap resources **only** (no string conversion).
-
-.. php:function:: mixed ValueSerializer::unwrap(mixed $value)
-
- Reverse of ``wrap()``.
-
-.. php:function:: void ValueSerializer::registerResourceHandler(string $type, callable $wrapFn, callable $restoreFn)
-
- Register a new resource handler.
-
-.. php:function:: void ValueSerializer::clearResourceHandlers()
-
- Drop all previously registered handlers (handy in tests).
-
-Usage Examples
---------------
-
-Serialize / Unserialize
-~~~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: php
-
- use Infocyph\InterMix\Serializer\ValueSerializer;
-
- $adder = fn (int $x) => $x + 10;
- $stream = fopen('php://memory', 'r+'); // will need a handler
-
- // Register a simple stream handler (see docs below) …
- // ValueSerializer::registerResourceHandler('stream', $wrap, $restore);
-
- $blob = ValueSerializer::serialize([$adder, $stream]);
- $same = ValueSerializer::unserialize($blob);
-
-Encode / Decode (base64)
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: php
-
- $payload = ['id' => 42, 'cb' => fn() => 'hi'];
-
- $token = ValueSerializer::encode($payload); // base64 by default
- $clone = ValueSerializer::decode($token);
-
- echo ($clone['cb'])(); // "hi"
-
-Signed payload mode (recommended for untrusted transport)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: php
-
- ValueSerializer::setPayloadSigningKey($_ENV['INTERMIX_SIGNING_KEY']);
-
- $token = ValueSerializer::encode(['id' => 7]); // signed internally
- $same = ValueSerializer::decode($token); // signature verified
-
-Security note
-~~~~~~~~~~~~~
-
-If payloads can be modified by external/untrusted actors, enable
-``setPayloadSigningKey(...)`` before using ``decode()``/``unserialize()``.
-
-Manual wrap / unwrap
-~~~~~~~~~~~~~~~~~~~~
-
-See *wrap()* / *unwrap()* example in the original docs – unchanged.
-
-Registering a Resource Handler
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-*(Identical to previous version – shown here abbreviated)*
-
-.. code-block:: php
-
- ValueSerializer::registerResourceHandler(
- 'stream',
- fn ($res) => /* …wrap… */ ,
- fn ($data) => /* …restore… */
- );
diff --git a/docs/start.rst b/docs/start.rst
index 2a6928f..a6cd689 100644
--- a/docs/start.rst
+++ b/docs/start.rst
@@ -4,21 +4,22 @@
Getting Started
===============
-infocyph/InterMix is a PHP library to provide with some class oriented tools that we need frequently.
+InterMix is a lightweight, high-performance PHP dependency injection and runtime
+utility toolkit. DI and invocation are its primary capabilities; Closure
+serialization, Fence, and Remix provide focused supporting utilities.
What Is this?
##############
-Lots of class oriented functionality that we need like; Dependency Injection, Mixins, Class barrier etc.
-are simply put together. As my other projects, it focuses on being lightweight, updated, least complex
-and balanced within Speed & Scalability!.
+InterMix combines explicit dependency resolution, invocation, scoped lifetimes,
+compiled resolvers, and direct factories in one framework-agnostic package.
Requirements
############
infocyph/InterMix |version| requires the following:
-* PHP 8.4/+
+* PHP >= 8.4
Installation
############
diff --git a/src/DI/Attribute/AttributeRegistry.php b/src/DI/Attribute/AttributeRegistry.php
index c0792b6..565f72b 100644
--- a/src/DI/Attribute/AttributeRegistry.php
+++ b/src/DI/Attribute/AttributeRegistry.php
@@ -60,11 +60,11 @@ public function register(string $attributeFqcn, string $resolverFqcn): void
* Resolves the given attribute instance.
*
* Looks up the associated resolver in the map and calls its resolve method.
- * If the resolver is not found or returns null, the method returns null.
+ * If the resolver is not found, the method returns the unresolved sentinel.
*
* @param object $attributeInstance The attribute instance to resolve.
* @param Reflector $target The target of the attribute (e.g. a class, method, or property).
- * @return mixed The resolved value or null if not possible.
+ * @return mixed The resolved value or the unresolved sentinel.
*/
public function resolve(
object $attributeInstance,
@@ -72,7 +72,11 @@ public function resolve(
): mixed {
$resolver = $this->map[$attributeInstance::class] ?? null;
- return $resolver?->resolve($attributeInstance, $target, $this->container);
+ if ($resolver === null) {
+ return AttributeResolution::Unresolved;
+ }
+
+ return $resolver->resolve($attributeInstance, $target, $this->container);
}
/**
diff --git a/src/DI/Attribute/AttributeResolution.php b/src/DI/Attribute/AttributeResolution.php
new file mode 100644
index 0000000..26b8ab7
--- /dev/null
+++ b/src/DI/Attribute/AttributeResolution.php
@@ -0,0 +1,10 @@
+ 'debug')]
- */
-#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
-class Infuse
-{
- /** @var array */
- private array $data;
-
- private string|int|null $firstKey = null;
-
- /**
- * Constructs a new instance of the Infuse attribute.
- *
- * @param mixed ...$parameters The parameters declared in the attribute usage.
- * For example: #[Infuse('SomeType', ...otherData)]
- */
- public function __construct(mixed ...$parameters)
- {
- $this->data = [];
-
- if ($parameters !== []) {
- $this->firstKey = array_key_first($parameters);
- foreach ($parameters as $key => $value) {
- if (is_int($key)) {
- $this->data[] = $value;
- } else {
- $this->data[$key] = $value;
- }
- }
- }
- }
-
- /**
- * Retrieves data used for a method injection scenario.
- *
- * @param int|string|null $key If provided, returns just the sub-value from the array.
- */
- public function getMethodArguments(int|string|null $key = null): mixed
- {
- return $key !== null
- ? ($this->data[$key] ?? null)
- : $this->data;
- }
-
- /**
- * Retrieves data used for property or parameter injection.
- * The attribute stores the "firstKey" as a "type" and the corresponding value as "data".
- *
- * @param int|string|null $key If provided, returns just the sub-value from the array.
- */
- public function getParameterData(int|string|null $key = null): mixed
- {
- $firstKey = $this->firstKey;
-
- if (is_int($firstKey) && array_key_exists($firstKey, $this->data)) {
- $firstValue = $this->data[$firstKey];
- $firstKey = is_int($firstValue) || is_string($firstValue) ? $firstValue : null;
- if (is_int($firstKey) || is_string($firstKey)) {
- $this->data[$firstKey] = $this->firstKey;
- }
- }
-
- $returnable = [
- 'type' => $firstKey,
- 'data' => is_int($firstKey) || is_string($firstKey)
- ? ($this->data[$firstKey] ?? null)
- : null,
- ];
-
- return $key !== null ? ($returnable[$key] ?? null) : $returnable;
- }
-}
diff --git a/src/DI/Attribute/Inject.php b/src/DI/Attribute/Inject.php
index c453e14..7c0d522 100644
--- a/src/DI/Attribute/Inject.php
+++ b/src/DI/Attribute/Inject.php
@@ -6,15 +6,56 @@
use Attribute;
-/**
- * Exact alias of {@see Infuse}. Declared in its own file so that
- * Composer’s autoloader can always find the class requested by PHP’s
- * attribute reflection logic.
- *
- * @inheritDoc
- */
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
-final class Inject extends Infuse
+final class Inject
{
- /* inherits everything from Infuse */
+ /** @var array */
+ private array $data = [];
+
+ private string|int|null $firstKey = null;
+
+ public function __construct(mixed ...$parameters)
+ {
+ if ($parameters === []) {
+ return;
+ }
+
+ $this->firstKey = array_key_first($parameters);
+ foreach ($parameters as $key => $value) {
+ if (is_int($key)) {
+ $this->data[] = $value;
+ } else {
+ $this->data[$key] = $value;
+ }
+ }
+ }
+
+ public function getMethodArguments(int|string|null $key = null): mixed
+ {
+ return $key !== null
+ ? ($this->data[$key] ?? null)
+ : $this->data;
+ }
+
+ public function getParameterData(int|string|null $key = null): mixed
+ {
+ $firstKey = $this->firstKey;
+
+ if (is_int($firstKey) && array_key_exists($firstKey, $this->data)) {
+ $firstValue = $this->data[$firstKey];
+ $firstKey = is_int($firstValue) || is_string($firstValue) ? $firstValue : null;
+ if (is_int($firstKey) || is_string($firstKey)) {
+ $this->data[$firstKey] = $this->firstKey;
+ }
+ }
+
+ $returnable = [
+ 'type' => $firstKey,
+ 'data' => is_int($firstKey) || is_string($firstKey)
+ ? ($this->data[$firstKey] ?? null)
+ : null,
+ ];
+
+ return $key !== null ? ($returnable[$key] ?? null) : $returnable;
+ }
}
diff --git a/src/DI/Container.php b/src/DI/Container.php
index e3e2d86..29351ce 100644
--- a/src/DI/Container.php
+++ b/src/DI/Container.php
@@ -395,6 +395,7 @@ public function get(string $id): mixed
* Retrieves the class name of the current resolver being used by the repository.
*
* @return CompiledCall|InjectedCall|GenericCall The resolver currently active in the container.
+ * @internal
*/
public function getCurrentResolver(): CompiledCall|InjectedCall|GenericCall
{
@@ -450,21 +451,15 @@ public function getReturn(string $id): mixed
/**
* Checks if a definition ID exists in the container.
*
- * This method attempts to verify the existence of a given ID
- * within the container by delegating the check to the
- * InvocationManager. If an exception occurs during the check,
- * the method will return false.
+ * Existence checks are delegated to the repository-backed manager and do
+ * not resolve the entry or use exceptions for expected control flow.
*
* @param string $id The ID of the definition to check.
* @return bool True if the definition ID exists, false otherwise.
*/
public function has(string $id): bool
{
- try {
- return $this->invocationManager->has($id);
- } catch (Exception) {
- return false;
- }
+ return $this->invocationManager->has($id);
}
/**
@@ -591,6 +586,7 @@ public function options(): OptionsManager
* |array{kind:'function',function:string}
* @throws ContainerException If the callable spec is invalid.
* @throws InvalidArgumentException If no argument is provided.
+ * @internal
*/
public function parseCallable(string|array|Closure|callable $spec): array
{
@@ -713,6 +709,7 @@ public function setEnvironment(string $env): self
* The supplied resolver must satisfy the supported dynamic-call contract.
*
* @param class-string $resolverClass The fully qualified class name of the new resolver.
+ * @internal
*/
public function setResolverClass(string $resolverClass): void
{
diff --git a/src/DI/Attribute/DeferredInitializer.php b/src/DI/Internal/DeferredInitializer.php
similarity index 95%
rename from src/DI/Attribute/DeferredInitializer.php
rename to src/DI/Internal/DeferredInitializer.php
index f27aeca..68b3427 100644
--- a/src/DI/Attribute/DeferredInitializer.php
+++ b/src/DI/Internal/DeferredInitializer.php
@@ -2,12 +2,13 @@
declare(strict_types=1);
-namespace Infocyph\InterMix\DI\Attribute;
+namespace Infocyph\InterMix\DI\Internal;
use Closure;
use Infocyph\InterMix\DI\Container;
use Infocyph\InterMix\DI\Support\TraceLevelEnum;
+/** @internal */
final class DeferredInitializer
{
private bool $done = false;
diff --git a/src/DI/Invoker.php b/src/DI/Invoker.php
index a027f33..a2d255c 100644
--- a/src/DI/Invoker.php
+++ b/src/DI/Invoker.php
@@ -6,7 +6,7 @@
use Closure;
use Infocyph\InterMix\Exceptions\ContainerException;
-use Infocyph\InterMix\Serializer\ValueSerializer;
+use Infocyph\InterMix\Serializer\ClosureSerializer;
use InvalidArgumentException;
use ReflectionException;
@@ -63,7 +63,7 @@ public static function with(Container $container): self
* or an object instance. It ensures the target is invokable, either by creating
* an instance through dependency injection if a class name is provided, or by
* using the provided object directly. The resulting callable is cached to
- * optimize subsequent calls.
+ * speed up subsequent calls.
*
* @param string|object $target The class name or object to convert to a callable.
*
@@ -122,7 +122,7 @@ public function getContainer(): Container
* If the callable is a class with a method, the method is invoked.
* If the callable is a closure or a plain string/object, it is executed directly.
*
- * @param string|array{0: string, 1: string}|callable $target The callable to be executed.
+ * @param string|array{0: string|object, 1: string}|callable $target The callable to be executed.
* @param array $args Optional parameters to pass to the callable.
*
* @return mixed The result of executing the callable.
@@ -133,35 +133,23 @@ public function getContainer(): Container
*/
public function invoke(string|array|callable $target, array $args = []): mixed
{
- // Serialized closure fast-path
- if (is_string($target) && ValueSerializer::isSerializedClosure($target)) {
- return $this->routeCallable($target, $args);
+ if ($target instanceof Closure) {
+ return $this->viaClosure($target, $args);
}
- if (is_array($target) && is_callable($target)) {
- return $this->container
- ->getCurrentResolver()
- ->closureSettler($target, $args);
- }
-
- $desc = $this->container->parseCallable($target);
- return match ($desc['kind']) {
- // Closure / invokable / callable array → just call
- 'closure' => $this->routeCallable($desc['closure'], $args),
+ if (is_object($target)) {
+ return $this->viaClosure(Closure::fromCallable($target), $args);
+ }
- // Global function name → just call
- 'function' => $this->routeCallable($desc['function'], $args),
+ if (is_array($target)) {
+ return $this->invokeArray($target, $args);
+ }
- // Class only → register ctor args then resolve
- 'class' => $this->container
- ->registration()->registerClass($desc['class'], $args)
- ->invocation()->getReturn($desc['class']),
+ if (is_string($target)) {
+ return $this->invokeString($target, $args);
+ }
- // Class + method → register method args then resolve
- 'method' => $this->container
- ->registration()->registerMethod($desc['class'], $desc['method'], $args)
- ->invocation()->getReturn($desc['class']),
- };
+ throw new InvalidArgumentException('Unsupported callable formation.');
}
/**
@@ -227,70 +215,52 @@ public function resolve(string $id): mixed
}
/**
- * Serializes a given value into a string.
- *
- * This method wraps the ValueSerializer serialize function.
- *
- * @param mixed $v The value to be serialized, which may contain resources.
- *
- * @return string The serialized string representation of the value.
- *
- * @throws InvalidArgumentException If a resource type has no registered handler.
+ * @param array{0: string|object, 1: string} $target
+ * @param array $args
*/
- public function serialize(mixed $v): string
+ private function invokeArray(array $target, array $args): mixed
{
- return ValueSerializer::serialize($v);
- }
+ if (is_callable($target)) {
+ return $this->container
+ ->getCurrentResolver()
+ ->closureSettler($target, $args);
+ }
- /**
- * Unserializes a string into its original value.
- *
- * This method wraps the Opis Closure unserialize function and unwraps any
- * wrapped resources within the resulting value using registered resource
- * handlers.
- *
- * @param string $b The serialized string to be converted back to its original form.
- *
- * @return mixed The original value, with any resources restored.
- */
- public function unserialize(string $b): mixed
- {
- return ValueSerializer::unserialize($b);
+ $class = $target[0];
+ $method = $target[1];
+ if (is_string($class) && class_exists($class) && method_exists($class, $method)) {
+ return $this->container
+ ->registration()->registerMethod($class, $method, $args)
+ ->invocation()->getReturn($class);
+ }
+
+ throw new InvalidArgumentException('Unsupported callable formation.');
}
/**
- * Routes a callable to the appropriate execution path based on its type.
- *
- * Closures, functions, class targets, invokable objects, and serialized
- * closures retain their corresponding execution semantics.
- *
- * @param mixed $callable The callable to be routed and executed.
- * @param array $args The arguments to pass to the callable.
- * @return mixed The result of executing the callable.
- * @throws ContainerException|ReflectionException|\Psr\Cache\InvalidArgumentException
+ * @param array $args
*/
- private function routeCallable(mixed $callable, array $args): mixed
+ private function invokeString(string $target, array $args): mixed
{
- if (\is_string($callable) && ValueSerializer::isSerializedClosure($callable)) {
- $unserialized = ValueSerializer::unserialize($callable);
- if (!$unserialized instanceof \Closure) {
- throw new \InvalidArgumentException('Serialized closure payload did not produce a Closure.');
- }
- return $this->viaClosure($unserialized, $args);
+ $classMethod = str_contains($target, '::');
+ if (!$classMethod && function_exists($target)) {
+ return $this->viaClosure(Closure::fromCallable($target), $args);
+ }
+
+ if ($classMethod && is_callable($target)) {
+ return $this->viaClosure(Closure::fromCallable($target), $args);
+ }
+
+ if (class_exists($target)) {
+ return $this->make($target, $args);
+ }
+
+ if (ClosureSerializer::isSerialized($target)) {
+ return $this->viaClosure(ClosureSerializer::unserialize($target), $args);
}
- return match (true) {
- \is_string($callable) => match (true) {
- !\str_contains($callable, '::') && \function_exists($callable) => $this->viaClosure(\Closure::fromCallable($callable), $args),
- \str_contains($callable, '::') && \is_callable($callable) => $this->viaClosure(\Closure::fromCallable($callable), $args),
- \class_exists($callable) => $this->container->make($callable),
- default => throw new \InvalidArgumentException('Unsupported callable formation.'),
- },
- $callable instanceof \Closure => $this->viaClosure($callable, $args),
- \is_object($callable) && \is_callable($callable) => $this->viaClosure(\Closure::fromCallable($callable), $args),
- default => throw new \InvalidArgumentException('Unsupported callable formation.'),
- };
+ throw new InvalidArgumentException('Unsupported callable formation.');
}
/**
diff --git a/src/DI/Invoker/GenericCall.php b/src/DI/Invoker/GenericCall.php
index 20397e7..868bb46 100644
--- a/src/DI/Invoker/GenericCall.php
+++ b/src/DI/Invoker/GenericCall.php
@@ -6,7 +6,7 @@
use Infocyph\InterMix\DI\Resolver\Repository;
use Infocyph\InterMix\DI\Support\DirectFactory;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
+use Infocyph\InterMix\Internal\ReflectionResource;
use InvalidArgumentException;
use ReflectionException;
diff --git a/src/DI/Invoker/InjectedCall.php b/src/DI/Invoker/InjectedCall.php
index 5f99fc6..544eb80 100644
--- a/src/DI/Invoker/InjectedCall.php
+++ b/src/DI/Invoker/InjectedCall.php
@@ -10,8 +10,8 @@
use Infocyph\InterMix\DI\Resolver\ParameterResolver;
use Infocyph\InterMix\DI\Resolver\PropertyResolver;
use Infocyph\InterMix\DI\Resolver\Repository;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Psr\Cache\InvalidArgumentException;
use ReflectionException;
use WeakMap;
diff --git a/src/DI/Managers/InvocationManager.php b/src/DI/Managers/InvocationManager.php
index 1644d01..f8873c0 100644
--- a/src/DI/Managers/InvocationManager.php
+++ b/src/DI/Managers/InvocationManager.php
@@ -6,8 +6,8 @@
use ArrayAccess;
use Closure;
-use Infocyph\InterMix\DI\Attribute\DeferredInitializer;
use Infocyph\InterMix\DI\Container;
+use Infocyph\InterMix\DI\Internal\DeferredInitializer;
use Infocyph\InterMix\DI\Resolver\Repository;
use Infocyph\InterMix\DI\Support\LifetimeEnum;
use Infocyph\InterMix\DI\Support\TraceLevelEnum;
diff --git a/src/DI/Resolver/ClassResolver.php b/src/DI/Resolver/ClassResolver.php
index d1ba2a6..2e3b78c 100644
--- a/src/DI/Resolver/ClassResolver.php
+++ b/src/DI/Resolver/ClassResolver.php
@@ -4,10 +4,10 @@
namespace Infocyph\InterMix\DI\Resolver;
-use Infocyph\InterMix\DI\Attribute\IMStdClass;
-use Infocyph\InterMix\DI\Attribute\Infuse;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
+use Infocyph\InterMix\DI\Attribute\Inject;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Psr\Cache\InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
@@ -110,7 +110,7 @@ public function resolveClassInstance(ReflectionClass $class, ?string $failureMes
}
/**
- * Resolve an Infuse attribute by first extracting the "type" (class name, function name, definition ID, etc.)
+ * Resolve an Inject attribute by first extracting the "type" (class name, function name, definition ID, etc.)
* and then trying to resolve it in the following order:
* 1. If $type is in functionReference => let definitionResolver handle it
* 2. If $type is a global function name => reflect the function and use parameterResolver to handle injection or data
@@ -118,35 +118,35 @@ public function resolveClassInstance(ReflectionClass $class, ?string $failureMes
* (optional) environment-based override if it's an interface
* 4. Otherwise, we have no way to resolve it
*
- * @param Infuse $infuse The Infuse attribute to resolve
- * @return mixed The resolved value or null if not possible
+ * @param Inject $inject The Inject attribute to resolve
+ * @return mixed The resolved value or the unresolved sentinel
*
* @throws ContainerException
* @throws ReflectionException|InvalidArgumentException
*/
- public function resolveInfuse(Infuse $infuse): mixed
+ public function resolveInject(Inject $inject): mixed
{
- $typeData = $infuse->getParameterData();
+ $typeData = $inject->getParameterData();
if (!is_array($typeData)) {
- return new IMStdClass();
+ return AttributeResolution::Unresolved;
}
$type = $typeData['type'] ?? null;
$data = $typeData['data'] ?? [];
if (!is_string($type) || $type === '') {
- return new IMStdClass();
+ return AttributeResolution::Unresolved;
}
- $fromDefinition = $this->resolveInfuseFromDefinition($type);
- if ($fromDefinition !== null) {
+ $fromDefinition = $this->resolveInjectFromDefinition($type);
+ if ($fromDefinition !== AttributeResolution::Unresolved) {
return $fromDefinition;
}
- $fromFunction = $this->resolveInfuseFromFunction($type, (array) $data);
- if ($fromFunction !== null) {
+ $fromFunction = $this->resolveInjectFromFunction($type, (array) $data);
+ if ($fromFunction !== AttributeResolution::Unresolved) {
return $fromFunction;
}
- return $this->resolveInfuseFromClassOrInterface($type);
+ return $this->resolveInjectFromClassOrInterface($type);
}
/**
@@ -343,10 +343,10 @@ private function resolveConstructor(ReflectionClass $class): void
$this->repository->setResolvedResource($className, $resolvedResource);
}
- private function resolveInfuseFromClassOrInterface(string $type): mixed
+ private function resolveInjectFromClassOrInterface(string $type): mixed
{
if (!class_exists($type) && !interface_exists($type)) {
- return null;
+ return AttributeResolution::Unresolved;
}
if (interface_exists($type)) {
@@ -361,20 +361,20 @@ private function resolveInfuseFromClassOrInterface(string $type): mixed
);
}
- private function resolveInfuseFromDefinition(string $type): mixed
+ private function resolveInjectFromDefinition(string $type): mixed
{
return $this->repository->hasFunctionReference($type)
? $this->definitionResolver->resolve($type)
- : null;
+ : AttributeResolution::Unresolved;
}
/**
* @param array $data
*/
- private function resolveInfuseFromFunction(string $type, array $data): mixed
+ private function resolveInjectFromFunction(string $type, array $data): mixed
{
if (!function_exists($type)) {
- return null;
+ return AttributeResolution::Unresolved;
}
$reflectionFn = ReflectionResource::getFunctionReflection($type);
diff --git a/src/DI/Resolver/Concerns/ResolvesAssociativeParameters.php b/src/DI/Resolver/Concerns/ResolvesAssociativeParameters.php
index 022cb64..17feda3 100644
--- a/src/DI/Resolver/Concerns/ResolvesAssociativeParameters.php
+++ b/src/DI/Resolver/Concerns/ResolvesAssociativeParameters.php
@@ -4,8 +4,9 @@
namespace Infocyph\InterMix\DI\Resolver\Concerns;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Psr\Cache\InvalidArgumentException;
use ReflectionAttribute;
use ReflectionClass;
@@ -86,7 +87,7 @@ private function resolveAssociativeParameters(
$processed,
);
- if ($resolvedValue !== $this->stdClass) {
+ if ($resolvedValue !== AttributeResolution::Unresolved) {
$processed[$paramName] = $resolvedValue;
continue;
@@ -131,7 +132,7 @@ private function resolveIndividualAttribute(
string $attributeValue,
): mixed {
$definition = $this->resolveByDefinitionType($attributeValue, $param);
- if ($definition !== $this->stdClass) {
+ if ($definition !== AttributeResolution::Unresolved) {
return $definition;
}
@@ -141,11 +142,11 @@ private function resolveIndividualAttribute(
return $attributeValue(...$this->resolve($reflectionFn, [], 'constructor'));
}
- return $this->stdClass;
+ return AttributeResolution::Unresolved;
}
/**
- * @param array> $attributes
+ * @param array> $attributes
* @return array
*/
private function resolveMethodAttributes(array $attributes): array
@@ -191,7 +192,7 @@ private function tryResolveAssociative(
$paramName = $param->getName();
$definition = $this->resolveByDefinitionType($paramName, $param);
- if ($definition !== $this->stdClass) {
+ if ($definition !== AttributeResolution::Unresolved) {
return $definition;
}
@@ -201,7 +202,7 @@ private function tryResolveAssociative(
$this->ownerFor($reflector),
$classReflection,
);
- if ($contextual !== $this->stdClass) {
+ if ($contextual !== AttributeResolution::Unresolved) {
return $contextual;
}
@@ -222,11 +223,11 @@ private function tryResolveAssociative(
if (isset($parameterAttribute[$paramName])) {
$resolved = $this->resolveIndividualAttribute($param, $parameterAttribute[$paramName]);
- if ($resolved !== $this->stdClass) {
+ if ($resolved !== AttributeResolution::Unresolved) {
return $resolved;
}
}
- return $this->stdClass;
+ return AttributeResolution::Unresolved;
}
}
diff --git a/src/DI/Resolver/Concerns/ResolvesParameterAttributes.php b/src/DI/Resolver/Concerns/ResolvesParameterAttributes.php
index 9fa25d2..3c962d0 100644
--- a/src/DI/Resolver/Concerns/ResolvesParameterAttributes.php
+++ b/src/DI/Resolver/Concerns/ResolvesParameterAttributes.php
@@ -4,7 +4,7 @@
namespace Infocyph\InterMix\DI\Resolver\Concerns;
-use Infocyph\InterMix\DI\Attribute\IMStdClass;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
use Infocyph\InterMix\Exceptions\ContainerException;
use Psr\Cache\InvalidArgumentException;
use ReflectionException;
@@ -22,20 +22,20 @@ trait ResolvesParameterAttributes
private function resolveParameterAttribute(ReflectionParameter $param): array
{
$plan = $this->getParameterAttributePlan($param);
- $infuse = $plan['infuse'];
- $firstInfuse = $infuse[0] ?? null;
- if ($firstInfuse !== null && $firstInfuse->getArguments() !== []) {
- $resolved = $this->classResolver->resolveInfuse($firstInfuse->newInstance());
+ $inject = $plan['inject'];
+ $firstInject = $inject[0] ?? null;
+ if ($firstInject !== null && $firstInject->getArguments() !== []) {
+ $resolved = $this->classResolver->resolveInject($firstInject->newInstance());
return [
'isResolved' => true,
- 'inject' => !$resolved instanceof IMStdClass,
+ 'inject' => $resolved !== AttributeResolution::Unresolved,
'value' => $resolved,
];
}
$registry = $this->repository->attributeRegistry();
- $injectVal = null;
+ $injectVal = AttributeResolution::Unresolved;
$handled = false;
foreach ($plan['all'] as $raw) {
@@ -48,14 +48,14 @@ private function resolveParameterAttribute(ReflectionParameter $param): array
$handled = true;
$val = $registry->resolve($attrObj, $param);
- if ($injectVal === null && $val !== null && !$val instanceof IMStdClass) {
+ if ($injectVal === AttributeResolution::Unresolved && $val !== AttributeResolution::Unresolved) {
$injectVal = $val;
}
}
return [
'isResolved' => $handled,
- 'inject' => $injectVal !== null,
+ 'inject' => $injectVal !== AttributeResolution::Unresolved,
'value' => $injectVal,
];
}
diff --git a/src/DI/Resolver/DefinitionResolver.php b/src/DI/Resolver/DefinitionResolver.php
index a9aed5c..5e8e288 100644
--- a/src/DI/Resolver/DefinitionResolver.php
+++ b/src/DI/Resolver/DefinitionResolver.php
@@ -8,8 +8,8 @@
use Infocyph\InterMix\DI\Support\DirectFactory;
use Infocyph\InterMix\DI\Support\FactoryDefinition;
use Infocyph\InterMix\DI\Support\LifetimeEnum;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Psr\Cache\InvalidArgumentException;
use ReflectionException;
diff --git a/src/DI/Resolver/ParameterResolver.php b/src/DI/Resolver/ParameterResolver.php
index e3cb372..eeb48ae 100644
--- a/src/DI/Resolver/ParameterResolver.php
+++ b/src/DI/Resolver/ParameterResolver.php
@@ -4,14 +4,14 @@
namespace Infocyph\InterMix\DI\Resolver;
-use Infocyph\InterMix\DI\Attribute\IMStdClass;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
+use Infocyph\InterMix\DI\Attribute\Inject;
use Infocyph\InterMix\DI\Resolver\Concerns\ResolvesAssociativeParameters;
use Infocyph\InterMix\DI\Resolver\Concerns\ResolvesNumericAndVariadicParameters;
use Infocyph\InterMix\DI\Resolver\Concerns\ResolvesParameterAttributes;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
use Infocyph\InterMix\DI\Support\TraceLevelEnum;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Psr\Cache\InvalidArgumentException;
use ReflectionAttribute;
use ReflectionClass;
@@ -43,21 +43,19 @@ class ParameterResolver
use ResolvesNumericAndVariadicParameters;
use ResolvesParameterAttributes;
- private const int INFUSE_CACHE_LIMIT = 1024;
+ private const int INJECT_CACHE_LIMIT = 1024;
private const int PARAM_ATTRIBUTE_PLAN_CACHE_LIMIT = 4096;
private const int RESOLUTION_PLAN_CACHE_LIMIT = 2048;
- private readonly IMStdClass $stdClass;
-
private ClassResolver $classResolver;
- /** @var array>> */
- private array $infuseCache = [];
+ /** @var array>> */
+ private array $injectCache = [];
/** @var array>,
+ * inject: array>,
* all: array>
* }>
*/
@@ -74,9 +72,7 @@ class ParameterResolver
public function __construct(
private readonly Repository $repository,
private readonly DefinitionResolver $definitionResolver,
- ) {
- $this->stdClass = new IMStdClass();
- }
+ ) {}
/**
* @param array $suppliedParameters
@@ -181,7 +177,7 @@ public function resolveByDefinitionType(string $name, ReflectionParameter $param
}
}
- return $this->stdClass;
+ return AttributeResolution::Unresolved;
}
/**
@@ -190,12 +186,12 @@ public function resolveByDefinitionType(string $name, ReflectionParameter $param
public function resolveContextualDependency(string $consumer, ReflectionClass $dependency): mixed
{
if ($consumer === '') {
- return $this->stdClass;
+ return AttributeResolution::Unresolved;
}
$binding = $this->repository->getContextualBinding($consumer, $dependency->getName());
if ($binding === null) {
- return $this->stdClass;
+ return AttributeResolution::Unresolved;
}
if (is_callable($binding)) {
@@ -292,21 +288,21 @@ private function extractNamedTypeCandidates(ReflectionParameter $parameter): arr
}
/**
- * @return array>
+ * @return array>
*/
- private function getInfuseAttributes(ReflectionFunctionAbstract $reflector): array
+ private function getInjectAttributes(ReflectionFunctionAbstract $reflector): array
{
$key = $this->reflectorCacheKey($reflector);
- return $this->infuseCache[$key] ?? $this->rememberInfuse(
+ return $this->injectCache[$key] ?? $this->rememberInject(
$key,
- $reflector->getAttributes(Infuse::class),
+ $reflector->getAttributes(Inject::class),
);
}
/**
* @return array{
- * infuse: array>,
+ * inject: array>,
* all: array>
* }
*/
@@ -315,7 +311,7 @@ private function getParameterAttributePlan(ReflectionParameter $parameter): arra
$key = $this->makeParameterAttributePlanKey($parameter);
return $this->parameterAttributePlanCache[$key] ?? $this->rememberParameterAttributePlan($key, [
- 'infuse' => $parameter->getAttributes(Infuse::class),
+ 'inject' => $parameter->getAttributes(Inject::class),
'all' => $parameter->getAttributes(),
]);
}
@@ -340,7 +336,7 @@ private function getResolutionPlan(ReflectionFunctionAbstract $reflector, string
$attributeData = [];
if ($applyAttribute) {
- $attributeData = $this->resolveMethodAttributes($this->getInfuseAttributes($reflector));
+ $attributeData = $this->resolveMethodAttributes($this->getInjectAttributes($reflector));
}
return $this->rememberResolutionPlan($key, [
@@ -471,24 +467,24 @@ private function reflectorCacheKey(ReflectionFunctionAbstract $reflector): strin
}
/**
- * @param array> $value
- * @return array>
+ * @param array> $value
+ * @return array>
*/
- private function rememberInfuse(string $key, array $value): array
+ private function rememberInject(string $key, array $value): array
{
- $this->evictCacheKeyIfNeeded($this->infuseCache, $key, self::INFUSE_CACHE_LIMIT);
- $this->infuseCache[$key] = $value;
+ $this->evictCacheKeyIfNeeded($this->injectCache, $key, self::INJECT_CACHE_LIMIT);
+ $this->injectCache[$key] = $value;
return $value;
}
/**
* @param array{
- * infuse: array>,
+ * inject: array>,
* all: array>
* } $value
* @return array{
- * infuse: array>,
+ * inject: array>,
* all: array>
* }
*/
diff --git a/src/DI/Resolver/PropertyResolver.php b/src/DI/Resolver/PropertyResolver.php
index 2abb2aa..6d82148 100644
--- a/src/DI/Resolver/PropertyResolver.php
+++ b/src/DI/Resolver/PropertyResolver.php
@@ -4,11 +4,11 @@
namespace Infocyph\InterMix\DI\Resolver;
-use Infocyph\InterMix\DI\Attribute\IMStdClass;
-use Infocyph\InterMix\DI\Attribute\Infuse;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
+use Infocyph\InterMix\DI\Attribute\Inject;
use Infocyph\InterMix\DI\Support\TraceLevelEnum;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Psr\Cache\InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
@@ -125,14 +125,14 @@ private function applyPropertyValue(
}
/**
- * Attempt to resolve a single property value using the built-in #[Infuse] attribute.
+ * Attempt to resolve a single property value using the built-in #[Inject] attribute.
*
* @param ReflectionProperty $property The property to resolve a value for.
* @param object $classInstance The instance of the class to set the property on.
* @return array|null An array of two items: the instance and the resolved value. Or null if not possible to resolve.
* @throws ContainerException|ReflectionException|InvalidArgumentException
*/
- private function attemptBuiltInInfuse(
+ private function attemptBuiltInInject(
ReflectionProperty $property,
object $classInstance,
): ?array {
@@ -140,24 +140,24 @@ private function attemptBuiltInInfuse(
return null;
}
- $attrs = $property->getAttributes(Infuse::class);
+ $attrs = $property->getAttributes(Inject::class);
if (!$attrs) {
return null;
}
- /** @var Infuse $infuse */
- $infuse = $attrs[0]->newInstance();
+ /** @var Inject $inject */
+ $inject = $attrs[0]->newInstance();
- // (a) #[Infuse] – no args ➜ infer by type-hint
+ // (a) #[Inject] – no args ➜ infer by type-hint
if ($attrs[0]->getArguments() === []) {
$val = $this->resolveWithoutArgument($property, $property->getType());
return [$classInstance, $val];
}
- // (b) #[Infuse(...)] – has args ➜ delegate to ClassResolver
- $val = $this->classResolver->resolveInfuse($infuse);
- if ($val instanceof IMStdClass) {
+ // (b) #[Inject(...)] – has args ➜ delegate to ClassResolver
+ $val = $this->classResolver->resolveInject($inject);
+ if ($val === AttributeResolution::Unresolved) {
return null;
}
@@ -185,7 +185,7 @@ private function attemptCustomAttributes(
ReflectionProperty $property,
object $classInstance,
): ?array {
- $injectVal = null;
+ $injectVal = AttributeResolution::Unresolved;
$handled = false;
foreach ($property->getAttributes() as $raw) {
@@ -198,7 +198,7 @@ private function attemptCustomAttributes(
$handled = true;
$val = $this->repository->attributeRegistry()->resolve($attrObj, $property);
- if ($injectVal === null && $val !== null && !$val instanceof IMStdClass) {
+ if ($injectVal === AttributeResolution::Unresolved && $val !== AttributeResolution::Unresolved) {
$injectVal = $val;
}
}
@@ -207,7 +207,7 @@ private function attemptCustomAttributes(
return null;
}
- if ($injectVal === null) {
+ if ($injectVal === AttributeResolution::Unresolved) {
return [];
}
@@ -375,7 +375,7 @@ private function resolveValue(
return $attempt;
}
- $attempt = $this->attemptBuiltInInfuse($property, $classInstance);
+ $attempt = $this->attemptBuiltInInject($property, $classInstance);
if ($attempt !== null) {
return $attempt;
}
@@ -391,7 +391,7 @@ private function resolveValue(
/**
* Resolve a property without an argument.
*
- * If the property has a `#[Infuse]` attribute with no arguments, this method
+ * If the property has a `#[Inject]` attribute with no arguments, this method
* is called to resolve the value. It will throw a
* `ContainerException` if the property type is not a class or interface.
* If the type is an interface, it will check for an environment-based
@@ -410,7 +410,7 @@ private function resolveWithoutArgument(
): object {
if (!$parameterType instanceof ReflectionNamedType || $parameterType->isBuiltin()) {
throw new ContainerException(
- 'Malformed #[Infuse] or invalid property type on '
+ 'Malformed #[Inject] or invalid property type on '
. "{$property->getDeclaringClass()->getName()}::\${$property->getName()}",
);
}
@@ -443,7 +443,7 @@ private function shouldSkipProperty(ReflectionProperty $property, ?array $regist
{
return $property->isPromoted()
&& !isset(($registeredProps ?? [])[$property->getName()])
- && $property->getAttributes(Infuse::class) === [];
+ && $property->getAttributes(Inject::class) === [];
}
/**
diff --git a/src/DI/Resolver/Repository.php b/src/DI/Resolver/Repository.php
index 02e6fcc..e03371a 100644
--- a/src/DI/Resolver/Repository.php
+++ b/src/DI/Resolver/Repository.php
@@ -130,9 +130,7 @@ public function __construct(private readonly Container $container) {}
/**
* Stores a class resource, with a key that can be 'constructor', 'method', 'property'.
*
- * The given data is stored in the class resources array as
- * $classResource[$class][$key] = $data.
- *
+ * The given data is stored under its class and resource-type keys.
*
* This method ensures that the container is not locked before making modifications.
*
diff --git a/src/DI/Support/AutomaticClassCompiler.php b/src/DI/Support/AutomaticClassCompiler.php
index 297d84a..f596ef4 100644
--- a/src/DI/Support/AutomaticClassCompiler.php
+++ b/src/DI/Support/AutomaticClassCompiler.php
@@ -4,8 +4,9 @@
namespace Infocyph\InterMix\DI\Support;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
use Infocyph\InterMix\DI\Container;
+use Infocyph\InterMix\Internal\ReflectionResource;
use ReflectionClass;
use ReflectionIntersectionType;
use ReflectionNamedType;
@@ -250,7 +251,7 @@ private function normalizedDependencyType(ReflectionClass $class, string $type):
*/
private function propertyHasRegisteredAttribute(Container $container, ReflectionProperty $property): bool
{
- return array_any($property->getAttributes(), fn($attribute) => $attribute->getName() === Infuse::class
+ return array_any($property->getAttributes(), fn($attribute) => $attribute->getName() === Inject::class
|| $container->attributeRegistry()->has($attribute->getName()));
}
diff --git a/src/DI/Support/CompiledResolverGenerator.php b/src/DI/Support/CompiledResolverGenerator.php
index 5125c4e..6f71142 100644
--- a/src/DI/Support/CompiledResolverGenerator.php
+++ b/src/DI/Support/CompiledResolverGenerator.php
@@ -9,6 +9,7 @@
use Infocyph\InterMix\DI\Container;
use Infocyph\InterMix\Exceptions\ContainerException;
use Infocyph\InterMix\Internal\AtomicFileWriter;
+use Infocyph\InterMix\Internal\ReflectionResource;
use ReflectionClass;
use ReflectionException;
use ReflectionFunctionAbstract;
diff --git a/src/DI/Support/PreloadGenerator.php b/src/DI/Support/PreloadGenerator.php
index d33f2e8..ac1d778 100644
--- a/src/DI/Support/PreloadGenerator.php
+++ b/src/DI/Support/PreloadGenerator.php
@@ -6,6 +6,7 @@
use Infocyph\InterMix\DI\Container;
use Infocyph\InterMix\Internal\AtomicFileWriter;
+use Infocyph\InterMix\Internal\ReflectionResource;
use ReflectionException;
final class PreloadGenerator
diff --git a/src/Fence/Fence.php b/src/Fence/Fence.php
index 5795084..62c0fd4 100644
--- a/src/Fence/Fence.php
+++ b/src/Fence/Fence.php
@@ -28,9 +28,7 @@ trait Fence
private static ?int $limitOverride = null;
- /**
- * Resets the internal cache of instances. This is mostly useful for unit tests.
- */
+ /** Clear created instances without changing the runtime limit override. */
final public static function clearInstances(): void
{
self::$instances = [];
@@ -125,6 +123,13 @@ final public static function instance(
return $created;
}
+ /** Reset created instances and restore the class-declared limit. */
+ final public static function reset(): void
+ {
+ self::$instances = [];
+ self::$limitOverride = null;
+ }
+
/**
* Override the limit for this class at runtime.
*
diff --git a/src/DI/Support/ReflectionResource.php b/src/Internal/ReflectionResource.php
similarity index 99%
rename from src/DI/Support/ReflectionResource.php
rename to src/Internal/ReflectionResource.php
index e39af6c..37dba40 100644
--- a/src/DI/Support/ReflectionResource.php
+++ b/src/Internal/ReflectionResource.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Infocyph\InterMix\DI\Support;
+namespace Infocyph\InterMix\Internal;
use Closure;
use InvalidArgumentException;
diff --git a/src/Remix/MacroMix.php b/src/Remix/MacroMix.php
index ea1cf05..221a390 100644
--- a/src/Remix/MacroMix.php
+++ b/src/Remix/MacroMix.php
@@ -6,10 +6,11 @@
use Closure;
use Exception;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
+use Infocyph\InterMix\Internal\ReflectionResource;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
+use RuntimeException;
// Public trait consumers live in downstream projects and the excluded test suite.
// @phpstan-ignore trait.unused
@@ -23,10 +24,8 @@ trait MacroMix
*/
protected static array $macros = [];
- /**
- * @var resource|false|null
- */
- private static $lockHandle;
+ /** @var array */
+ private static array $lockHandles = [];
/**
* Handles dynamic calls to the object.
@@ -105,16 +104,18 @@ public static function loadMacrosFromAnnotations(string|object $class): void
{
$instance = is_object($class) ? $class : new $class();
$reflection = ReflectionResource::getClassReflection($instance);
+ $macros = [];
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$docComment = $method->getDocComment();
if ($docComment && preg_match('/@Macro\("(\w+)"\)/', $docComment, $matches)) {
$macroName = $matches[1];
- $macro = $method->isStatic()
+ $macros[$macroName] = $method->isStatic()
? fn(...$args) => $method->invoke(null, ...$args)
: fn(...$args) => $method->invoke($instance, ...$args);
- static::macro($macroName, $macro);
}
}
+
+ static::registerMany($macros);
}
/**
@@ -129,15 +130,7 @@ public static function loadMacrosFromAnnotations(string|object $class): void
*/
public static function loadMacrosFromConfig(array $config): void
{
- self::acquireLock();
-
- try {
- foreach ($config as $name => $macro) {
- static::macro($name, $macro);
- }
- } finally {
- self::releaseLock();
- }
+ static::registerMany($config);
}
/**
@@ -150,14 +143,13 @@ public static function loadMacrosFromConfig(array $config): void
*/
public static function macro(string $name, callable|object $macro): void
{
- static::$macros[$name] = $macro;
- if ($macro instanceof Closure) {
- static::$macroIsStaticClosure[$name] = new ReflectionFunction($macro)->isStatic();
+ if (!self::isLockEnabled()) {
+ static::registerMacroUnlocked($name, $macro);
return;
}
- unset(static::$macroIsStaticClosure[$name]);
+ self::mutate(static fn() => static::registerMacroUnlocked($name, $macro));
}
/**
@@ -180,19 +172,22 @@ public static function mix(object|string $mixin, bool $replace = true): void
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED,
);
+ $macros = [];
foreach ($methods as $method) {
$name = $method->name;
- if (!$replace && static::hasMacro($name)) {
- continue;
- }
-
- $macro = $method->isStatic()
+ $macros[$name] = $method->isStatic()
? fn(...$args) => $method->invoke(null, ...$args)
: fn(...$args) => $method->invoke($instance, ...$args);
+ }
+
+ if (!self::isLockEnabled()) {
+ static::registerMixinUnlocked($macros, $replace);
- static::macro($name, $macro);
+ return;
}
+
+ self::mutate(static fn() => static::registerMixinUnlocked($macros, $replace));
}
/**
@@ -203,45 +198,52 @@ public static function mix(object|string $mixin, bool $replace = true): void
* @param string $name The name of the macro to remove.
*/
public static function removeMacro(string $name): void
- {
- unset(static::$macros[$name]);
- unset(static::$macroIsStaticClosure[$name]);
- }
-
- /**
- * Acquires a lock to ensure thread-safe operations.
- *
- * This method checks if locking is enabled and acquires an exclusive lock
- * on the current file. It initializes the lock handle if it is not already set.
- * If the lock handle is valid, it uses `flock` to apply an exclusive lock.
- */
- private static function acquireLock(): void
{
if (!self::isLockEnabled()) {
+ static::removeMacroUnlocked($name);
+
return;
}
- if (is_null(self::$lockHandle)) {
- self::$lockHandle = fopen(__FILE__, 'r');
+ self::mutate(static fn() => static::removeMacroUnlocked($name));
+ }
+
+ private static function isLockEnabled(): bool
+ {
+ $class = static::class;
+
+ return defined("$class::ENABLE_LOCK") && (bool) constant("$class::ENABLE_LOCK");
+ }
+
+ /** @return resource */
+ private static function lockHandle()
+ {
+ $class = static::class;
+ if (isset(self::$lockHandles[$class])) {
+ return self::$lockHandles[$class];
}
- if (self::$lockHandle !== false) {
- flock(self::$lockHandle, LOCK_EX);
+ $path = sys_get_temp_dir() . '/intermix-macro-' . hash('xxh3', $class) . '.lock';
+ $handle = fopen($path, 'c');
+ if ($handle === false) {
+ throw new RuntimeException("Unable to open the MacroMix mutation lock for $class.");
}
+
+ return self::$lockHandles[$class] = $handle;
}
- /**
- * Checks if the locking mechanism is enabled.
- *
- * Determines whether the locking feature is enabled by checking the
- * 'ENABLE_LOCK' constant in the class. If the constant is defined
- * and true, locking is enabled; otherwise, it is disabled.
- *
- * @return bool True if locking is enabled, false otherwise.
- */
- private static function isLockEnabled(): bool
+ private static function mutate(Closure $operation): mixed
{
- return defined('static::ENABLE_LOCK') && (bool) static::ENABLE_LOCK;
+ $handle = self::lockHandle();
+ if (!flock($handle, LOCK_EX)) {
+ throw new RuntimeException('Unable to acquire the MacroMix mutation lock.');
+ }
+
+ try {
+ return $operation();
+ } finally {
+ flock($handle, LOCK_UN);
+ }
}
/**
@@ -297,25 +299,55 @@ private static function process(?object $bind, string $method, array $parameters
return $result ?? $bind ?? static::class;
}
+ private static function registerMacroUnlocked(string $name, callable|object $macro): void
+ {
+ static::$macros[$name] = $macro;
+ if ($macro instanceof Closure) {
+ static::$macroIsStaticClosure[$name] = new ReflectionFunction($macro)->isStatic();
+
+ return;
+ }
+
+ unset(static::$macroIsStaticClosure[$name]);
+ }
+
/**
- * Releases the lock to allow other processes to access the resource.
- *
- * This method checks if locking is enabled and releases the exclusive lock
- * on the current file by using `flock` to remove the lock. It then closes
- * the lock handle and sets it to null to indicate that the lock is no longer
- * held. If locking is not enabled or the lock handle is not set, the method
- * returns without taking any action.
+ * @param array $macros
*/
- private static function releaseLock(): void
+ private static function registerMany(array $macros): void
{
- if (!self::isLockEnabled() || is_null(self::$lockHandle)) {
+ if (!self::isLockEnabled()) {
+ foreach ($macros as $name => $macro) {
+ static::registerMacroUnlocked($name, $macro);
+ }
+
return;
}
- if (self::$lockHandle !== false) {
- flock(self::$lockHandle, LOCK_UN);
- fclose(self::$lockHandle);
- self::$lockHandle = null;
+ self::mutate(static function () use ($macros): void {
+ foreach ($macros as $name => $macro) {
+ static::registerMacroUnlocked($name, $macro);
+ }
+ });
+ }
+
+ /**
+ * @param array $macros
+ * @param bool $replace Whether existing macro names may be replaced.
+ */
+ private static function registerMixinUnlocked(array $macros, bool $replace): void
+ {
+ foreach ($macros as $name => $macro) {
+ if (!$replace && isset(static::$macros[$name])) {
+ continue;
+ }
+
+ static::registerMacroUnlocked($name, $macro);
}
}
+
+ private static function removeMacroUnlocked(string $name): void
+ {
+ unset(static::$macros[$name], static::$macroIsStaticClosure[$name]);
+ }
}
diff --git a/src/Serializer/ClosureSerializer.php b/src/Serializer/ClosureSerializer.php
new file mode 100644
index 0000000..ad4be54
--- /dev/null
+++ b/src/Serializer/ClosureSerializer.php
@@ -0,0 +1,55 @@
+key === '') {
+ throw new InvalidArgumentException('Closure signing key cannot be empty.');
+ }
+ }
+
+ public function serialize(Closure $closure): string
+ {
+ $serialized = opis_serialize($closure);
+ $signature = hash_hmac(self::HMAC_ALGORITHM, $serialized, $this->key, true);
+
+ return self::PREFIX . base64_encode($signature) . '.' . base64_encode($serialized);
+ }
+
+ public function unserialize(string $payload): Closure
+ {
+ if (!str_starts_with($payload, self::PREFIX)) {
+ throw new InvalidArgumentException('Signed InterMix Closure payload expected.');
+ }
+
+ $envelope = substr($payload, strlen(self::PREFIX));
+ $separator = strpos($envelope, '.');
+ if ($separator === false) {
+ throw new InvalidArgumentException('Invalid signed Closure payload format.');
+ }
+
+ $signature = base64_decode(substr($envelope, 0, $separator), true);
+ $serialized = base64_decode(substr($envelope, $separator + 1), true);
+ if ($signature === false || $serialized === false || $serialized === '') {
+ throw new InvalidArgumentException('Invalid signed Closure payload encoding.');
+ }
+
+ $expected = hash_hmac(self::HMAC_ALGORITHM, $serialized, $this->key, true);
+ if (!hash_equals($expected, $signature)) {
+ throw new InvalidArgumentException('Signed Closure payload verification failed.');
+ }
+
+ try {
+ $closure = opis_unserialize($serialized);
+ } catch (Throwable $throwable) {
+ throw new InvalidArgumentException('Invalid signed Closure payload.', previous: $throwable);
+ }
+
+ if (!$closure instanceof Closure) {
+ throw new InvalidArgumentException('Signed payload did not contain a Closure.');
+ }
+
+ return $closure;
+ }
+}
diff --git a/src/Serializer/SignedValueSerializer.php b/src/Serializer/SignedValueSerializer.php
deleted file mode 100644
index 53a5738..0000000
--- a/src/Serializer/SignedValueSerializer.php
+++ /dev/null
@@ -1,27 +0,0 @@
-key === '') {
- throw new InvalidArgumentException('Payload signing key cannot be empty.');
- }
- }
-
- public function decode(string $payload, bool $base64 = true): mixed
- {
- return ValueSerializer::decodeSigned($payload, $this->key, $base64);
- }
-
- public function encode(mixed $value, bool $base64 = true): string
- {
- return ValueSerializer::encodeSigned($value, $this->key, $base64);
- }
-}
diff --git a/src/Serializer/ValueSerializer.php b/src/Serializer/ValueSerializer.php
deleted file mode 100644
index 5fd8cc1..0000000
--- a/src/Serializer/ValueSerializer.php
+++ /dev/null
@@ -1,404 +0,0 @@
- */
- private static array $resourceHandlers = [];
-
- /** @var array */
- private static array $serializedClosureMemo = [];
-
- /**
- * Clear all registered resource handlers.
- *
- * Use this method to reset the state of ValueSerializer in test cases,
- * or when you want to ensure that no resource handlers are registered.
- */
- public static function clearResourceHandlers(): void
- {
- self::$resourceHandlers = [];
- self::$serializedClosureMemo = [];
- }
-
- public static function currentPayloadSigningKey(): ?string
- {
- return self::$payloadSigningKey;
- }
-
- /**
- * Decode a payload produced by {@see encode()}.
- *
- * @param string $payload The encoded string
- * @param bool $base64 True ⇒ expect base64; false ⇒ raw
- * @return mixed Original value
- * @throws InvalidArgumentException Forwarded from ::unserialize()
- */
- public static function decode(string $payload, bool $base64 = true): mixed
- {
- return self::decodeWithKey($payload, $base64, self::$payloadSigningKey);
- }
-
- public static function decodeSigned(string $payload, string $key, bool $base64 = true): mixed
- {
- self::validateSigningKey($key);
-
- return self::decodeWithKey($payload, $base64, $key);
- }
-
- /**
- * Encode any value into a transport-safe (optionally base64) string.
- * @param mixed $value Any PHP value
- * @param bool $base64 True ⇒ wrap with base64; false ⇒ raw
- * @return string Encoded payload
- * @throws InvalidArgumentException Forwarded from ::serialize()
- */
- public static function encode(mixed $value, bool $base64 = true): string
- {
- return self::encodeWithKey($value, $base64, self::$payloadSigningKey);
- }
-
- public static function encodeSigned(mixed $value, string $key, bool $base64 = true): string
- {
- self::validateSigningKey($key);
-
- return self::encodeWithKey($value, $base64, $key);
- }
-
- /**
- * Determines if a given string is a serialized Opis closure.
- *
- * This method checks if the provided string represents a serialized
- * Opis closure by looking for specific patterns associated with
- * Opis closures.
- *
- * @param string $str The string to check.
- *
- * @return bool True if the string is a serialized Opis closure, false otherwise.
- */
- public static function isSerializedClosure(string $str): bool
- {
- if (self::$payloadSigningKey !== null && str_starts_with($str, self::SIGNED_PAYLOAD_PREFIX)) {
- try {
- $str = self::extractSignedBlob($str, self::$payloadSigningKey);
- } catch (InvalidArgumentException) {
- return false;
- }
- }
-
- if (isset(self::$serializedClosureMemo[$str])) {
- return self::$serializedClosureMemo[$str];
- }
-
- if (!str_contains($str, 'Opis\\Closure')) {
- return self::rememberSerializedClosureMemo($str, false);
- }
-
- return self::rememberSerializedClosureMemo($str, (bool) preg_match(
- '/^(?:C:\d+:"Opis\\\\Closure\\\\SerializableClosure|O:\d+:"Opis\\\\Closure\\\\Box"|O:\d+:"Opis\\\\Closure\\\\Serializable")/',
- $str,
- ));
- }
-
- /**
- * Registers a handler for a specific resource type.
- *
- * The two callables provided are:
- * 1. `wrapFn`: takes a resource of type `$type` and returns an array
- * (or other serializable value) that represents the resource.
- * 2. `restoreFn`: takes the array (or other serializable value) returned
- * by `wrapFn` and returns a resource of type `$type`.
- *
- * @param string $type The type of resource this handler is for.
- * @param callable $wrapFn The callable that wraps the resource.
- * @param callable $restoreFn The callable that restores the resource.
- */
- public static function registerResourceHandler(
- string $type,
- callable $wrapFn,
- callable $restoreFn,
- ): void {
- self::$resourceHandlers[$type] = [
- 'wrap' => $wrapFn,
- 'restore' => $restoreFn,
- ];
- }
-
- /**
- * Serializes a given value into a string.
- *
- * This method takes a value, wraps any resources it contains using registered
- * resource handlers, and serializes it into a string using Opis Closure's
- * serialize function.
- *
- * @param mixed $value The value to be serialized, which may contain resources.
- *
- * @return string The serialized string representation of the value.
- *
- * @throws InvalidArgumentException If a resource type has no registered handler.
- */
- public static function serialize(mixed $value): string
- {
- return self::serializeWithKey($value, self::$payloadSigningKey);
- }
-
- /**
- * Set or clear the payload signing key used for serialize/unserialize.
- *
- * When a key is set:
- * - serialize()/encode() output is signed (HMAC-SHA256 envelope).
- * - unserialize()/decode() require and verify the signature.
- *
- * @throws InvalidArgumentException if an empty key is provided
- */
- public static function setPayloadSigningKey(?string $key): void
- {
- if ($key !== null) {
- self::validateSigningKey($key);
- }
-
- self::$payloadSigningKey = $key;
- self::$serializedClosureMemo = [];
- }
-
- public static function signed(string $key): SignedValueSerializer
- {
- return new SignedValueSerializer($key);
- }
-
- /**
- * Unserializes a given string into its original value.
- *
- * This method takes a serialized string and converts it back into its
- * original value. It first unserializes the string using Opis Closure's
- * unserialize function, then recursively unwraps any wrapped resources
- * within the resulting value using registered resource handlers.
- *
- * @param string $blob The serialized string to be converted back to its original form.
- *
- * @return mixed The original value, with any resources restored.
- */
- public static function unserialize(string $blob): mixed
- {
- return self::unserializeWithKey($blob, self::$payloadSigningKey);
- }
-
- /**
- * Reverse {@see wrap} by recursively unwrapping values that were wrapped by
- * {@see wrap}. This method is similar to {@see unserialize}, but it does not
- * involve serialisation.
- *
- * @param mixed $resource A value that may contain wrapped resources.
- *
- * @return mixed The same value with any wrapped resources restored.
- */
- public static function unwrap(mixed $resource): mixed
- {
- return self::unwrapRecursive($resource);
- }
-
- /**
- * Wraps resources within a given value.
- *
- * This method acts as a public interface to recursively wrap
- * resources found within the provided value using registered
- * resource handlers.
- *
- * @param mixed $value The value to be wrapped, which may contain resources.
- *
- * @return mixed The value with any resources wrapped, or the original value if no resources are found.
- */
- public static function wrap(mixed $value): mixed
- {
- return self::wrapRecursive($value);
- }
-
- private static function decodeWithKey(string $payload, bool $base64, ?string $key): mixed
- {
- $blob = $base64 ? base64_decode($payload, true) : $payload;
-
- if ($blob === false) {
- throw new InvalidArgumentException('Invalid base64 payload supplied to ValueSerializer::decode().');
- }
-
- return self::unserializeWithKey($blob, $key);
- }
-
- private static function encodeWithKey(mixed $value, bool $base64, ?string $key): string
- {
- $blob = self::serializeWithKey($value, $key);
-
- return $base64 ? base64_encode($blob) : $blob;
- }
-
- private static function extractSignedBlob(string $payload, string $key): string
- {
- if (!str_starts_with($payload, self::SIGNED_PAYLOAD_PREFIX)) {
- throw new InvalidArgumentException('Signed payload expected but signature envelope is missing.');
- }
-
- $raw = substr($payload, strlen(self::SIGNED_PAYLOAD_PREFIX));
-
- $separatorPosition = strpos($raw, '.');
- if ($separatorPosition === false) {
- throw new InvalidArgumentException('Invalid signed payload format.');
- }
-
- $signature = substr($raw, 0, $separatorPosition);
- $encodedBlob = substr($raw, $separatorPosition + 1);
- $blob = base64_decode($encodedBlob, true);
- if ($blob === false) {
- throw new InvalidArgumentException('Invalid signed payload body.');
- }
-
- $expected = hash_hmac(self::PAYLOAD_HMAC_ALGO, $blob, $key);
- if (!hash_equals($expected, $signature)) {
- throw new InvalidArgumentException('Signed payload verification failed.');
- }
-
- return $blob;
- }
-
- private static function rememberSerializedClosureMemo(string $key, bool $value): bool
- {
- if (strlen($key) > self::SERIALIZED_CLOSURE_MEMO_MAX_KEY_BYTES) {
- return $value;
- }
-
- if (!isset(self::$serializedClosureMemo[$key])
- && count(self::$serializedClosureMemo) >= self::SERIALIZED_CLOSURE_MEMO_LIMIT) {
- unset(self::$serializedClosureMemo[array_key_first(self::$serializedClosureMemo)]);
- }
-
- self::$serializedClosureMemo[$key] = $value;
-
- return $value;
- }
-
- private static function serializeWithKey(mixed $value, ?string $key): string
- {
- $blob = match (true) {
- is_scalar($value), $value === null => serialize($value),
- default => oc_serialize(self::wrapRecursive($value)),
- };
-
- return $key === null
- ? $blob
- : self::signBlob($blob, $key);
- }
-
- private static function signBlob(string $blob, string $key): string
- {
- $signature = hash_hmac(self::PAYLOAD_HMAC_ALGO, $blob, $key);
-
- return self::SIGNED_PAYLOAD_PREFIX . $signature . '.' . base64_encode($blob);
- }
-
- private static function unserializeWithKey(string $blob, ?string $key): mixed
- {
- if ($key !== null) {
- $blob = self::extractSignedBlob($blob, $key);
- }
-
- if (!ValueSerializer::isSerializedClosure($blob) && str_starts_with($blob, 's:')) {
- return unserialize($blob, ['allowed_classes' => true]);
- }
-
- return self::unwrapRecursive(oc_unserialize($blob));
- }
-
- /**
- * Reverse {@see wrapRecursive} by recursively unwrapping values
- * that were wrapped by {@see wrapRecursive}.
- *
- * @param mixed $resource A value that may contain wrapped resources.
- *
- * @return mixed The same value with any wrapped resources restored.
- */
- private static function unwrapRecursive(mixed $resource): mixed
- {
- if (
- is_array($resource)
- && ($resource['__wrapped_resource'] ?? null) === true
- && is_string($resource['type'] ?? null)
- && isset(self::$resourceHandlers[$resource['type']])
- ) {
- return (self::$resourceHandlers[$resource['type']]['restore'])($resource['data']);
- }
-
- if (is_array($resource)) {
- foreach ($resource as $key => $item) {
- $resource[$key] = self::unwrapRecursive($item);
- }
- }
-
- return $resource;
- }
-
- private static function validateSigningKey(string $key): void
- {
- if ($key === '') {
- throw new InvalidArgumentException('Payload signing key cannot be empty.');
- }
- }
-
- /**
- * Recursively wraps resources within a given value.
- *
- * This method checks if the provided value is a resource. If so,
- * it retrieves the appropriate handler for the resource type and
- * uses it to wrap the resource. The wrapped resource is returned
- * as an associative array containing a flag, the resource type,
- * and the wrapped data.
- *
- * If the value is an array, the method recursively processes each
- * element in the array.
- *
- * @param mixed $resource The value to be wrapped, which may contain resources.
- *
- * @return mixed The value with any resources wrapped, or the original value if no resources are found.
- *
- * @throws InvalidArgumentException If no handler is registered for a resource type.
- */
- private static function wrapRecursive(mixed $resource): mixed
- {
- if (is_resource($resource)) {
- $type = get_resource_type($resource);
- $arr = self::$resourceHandlers[$type] ?? null;
- if (!$arr) {
- throw new InvalidArgumentException("No handler for resource type '$type'");
- }
-
- return [
- '__wrapped_resource' => true,
- 'type' => $type,
- 'data' => ($arr['wrap'])($resource),
- ];
- }
-
- if (is_array($resource)) {
- foreach ($resource as $key => $value) {
- $resource[$key] = self::wrapRecursive($value);
- }
- }
-
- return $resource;
- }
-}
diff --git a/tests/ArchTest.php b/tests/ArchTest.php
index 19fc508..77b8db7 100644
--- a/tests/ArchTest.php
+++ b/tests/ArchTest.php
@@ -9,3 +9,32 @@
test('No echo statements', function () {
expect(['echo', 'print'])->each->not()->toBeUsed();
});
+
+test('feature modules respect InterMix dependency boundaries', function () {
+ $sourceRoot = dirname(__DIR__) . '/src';
+ $rules = [
+ 'Fence' => ['DI\\', 'Serializer\\', 'Remix\\'],
+ 'Serializer' => ['DI\\', 'Fence\\', 'Remix\\'],
+ 'Remix' => ['DI\\', 'Fence\\', 'Serializer\\'],
+ 'Internal' => ['DI\\', 'Fence\\', 'Remix\\', 'Serializer\\'],
+ ];
+
+ foreach ($rules as $module => $forbiddenModules) {
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator("$sourceRoot/$module"),
+ );
+
+ foreach ($files as $file) {
+ if (!$file->isFile() || $file->getExtension() !== 'php') {
+ continue;
+ }
+
+ $source = file_get_contents($file->getPathname());
+ expect($source)->toBeString();
+
+ foreach ($forbiddenModules as $forbiddenModule) {
+ expect($source)->not->toContain("Infocyph\\InterMix\\$forbiddenModule");
+ }
+ }
+ }
+});
diff --git a/tests/Container/AdditionalContainerTest.php b/tests/Container/AdditionalContainerTest.php
index a30a2d6..0c715b1 100644
--- a/tests/Container/AdditionalContainerTest.php
+++ b/tests/Container/AdditionalContainerTest.php
@@ -192,12 +192,12 @@
/*
|--------------------------------------------------------------------------
-| 9) Infuse attribute referencing an unknown definition
+| 9) Inject attribute referencing an unknown definition
|--------------------------------------------------------------------------
*/
-class InfuseUnknownParam
+class InjectUnknownParam
{
- #[\Infocyph\InterMix\DI\Attribute\Infuse('unknown.ref')]
+ #[\Infocyph\InterMix\DI\Attribute\Inject('unknown.ref')]
public function doSomething(BasicClass $basic, string $another): array
{
return [
@@ -207,12 +207,12 @@ public function doSomething(BasicClass $basic, string $another): array
}
}
-test('Infuse attribute with unknown reference', function () {
- $container = container(null, 'infuse_unknown')
+test('Inject attribute with unknown reference', function () {
+ $container = container(null, 'inject_unknown')
->options()
->setOptions(true, true)
->end();
- $container->call(InfuseUnknownParam::class, 'doSomething');
+ $container->call(InjectUnknownParam::class, 'doSomething');
})->throws(ContainerException::class);
test('helper default aliases are readable and isolated', function () {
diff --git a/tests/Container/AdditionalTest.php b/tests/Container/AdditionalTest.php
index 9028738..c5f709b 100644
--- a/tests/Container/AdditionalTest.php
+++ b/tests/Container/AdditionalTest.php
@@ -6,7 +6,7 @@
use Infocyph\InterMix\DI\Support\DebugTracer;
use Infocyph\InterMix\DI\Support\LifetimeEnum;
use Infocyph\InterMix\DI\Support\PreloadGenerator;
-use Infocyph\InterMix\DI\Support\ReflectionResource;
+use Infocyph\InterMix\Internal\ReflectionResource;
use Infocyph\InterMix\DI\Support\TraceLevelEnum;
use Infocyph\InterMix\Exceptions\ContainerException;
use Infocyph\InterMix\Tests\Fixture\DemoProvider;
diff --git a/tests/Container/AttributeResolutionTest.php b/tests/Container/AttributeResolutionTest.php
index 523d009..0c2b492 100644
--- a/tests/Container/AttributeResolutionTest.php
+++ b/tests/Container/AttributeResolutionTest.php
@@ -3,7 +3,8 @@
declare(strict_types=1);
use Infocyph\InterMix\DI\Container;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\AttributeResolverInterface;
+use Infocyph\InterMix\DI\Attribute\Inject;
use Infocyph\InterMix\Tests\Fixture\ExampleAttr;
use Infocyph\InterMix\Tests\Fixture\ExampleAttrResolver;
use Infocyph\InterMix\Tests\Fixture\LogicOnlyAttr;
@@ -16,6 +17,23 @@
uses()->group('di', 'attribute');
+#[Attribute(Attribute::TARGET_PROPERTY)]
+class NullInjection {}
+
+class NullInjectionResolver implements AttributeResolverInterface
+{
+ public function resolve(object $attributeInstance, Reflector $target, Container $container): mixed
+ {
+ return null;
+ }
+}
+
+class NullableAttributeTarget
+{
+ #[NullInjection]
+ public ?string $value;
+}
+
beforeEach(function () {
$this->container = new Container();
});
@@ -37,7 +55,6 @@
$c->definitions()->bind('name', 'hello');
$instance = $c->get(MixedAttributeExample::class);
- // Skip $std as it resolves to IMStdClass by design
expect($instance->name)
->toBe('hello')
->and($instance->custom)->toBe('TEST');
@@ -63,7 +80,7 @@
MethodAttrResolver::class,
);
- /* 3️⃣ Bind a container definition that Infuse will pick up */
+ /* 3️⃣ Bind a container definition that Inject will pick up */
$c->definitions()->bind('api_key', 'XYZ123');
/* 4️⃣ Pre-register the method we want the container to invoke */
@@ -103,8 +120,20 @@
expect($instance)->toBeInstanceOf(LogicOnlyTarget::class);
});
-it('returns the first positional Infuse method argument when requested explicitly', function () {
- $infuse = new Infuse('first', 'second');
+it('returns the first positional Inject method argument when requested explicitly', function () {
+ $inject = new Inject('first', 'second');
+
+ expect($inject->getMethodArguments(0))->toBe('first');
+});
+
+it('treats null as a resolved custom attribute value', function () {
+ $this->container->attributeRegistry()->register(
+ NullInjection::class,
+ NullInjectionResolver::class,
+ );
+ $this->container->options()->setOptions(propertyAttributes: true);
+
+ $target = $this->container->get(NullableAttributeTarget::class);
- expect($infuse->getMethodArguments(0))->toBe('first');
+ expect($target->value)->toBeNull();
});
diff --git a/tests/Container/CompiledResolverTest.php b/tests/Container/CompiledResolverTest.php
index 07692f1..40da974 100644
--- a/tests/Container/CompiledResolverTest.php
+++ b/tests/Container/CompiledResolverTest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
use Infocyph\InterMix\DI\Container;
use Infocyph\InterMix\DI\Invoker\CompiledCall;
use Infocyph\InterMix\DI\Invoker\InjectedCall;
@@ -56,7 +56,7 @@ final class CompiledResolverAttributedConstructor
{
public CompiledResolverDependency $annotated;
- public function __construct(#[Infuse] CompiledResolverDependency $annotated)
+ public function __construct(#[Inject] CompiledResolverDependency $annotated)
{
$this->annotated = $annotated;
}
@@ -64,7 +64,7 @@ public function __construct(#[Infuse] CompiledResolverDependency $annotated)
final class CompiledResolverAttributedProperty
{
- #[Infuse(CompiledResolverDependency::class)]
+ #[Inject(CompiledResolverDependency::class)]
public mixed $injected = null;
}
diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php
index 5e667d7..bca244d 100644
--- a/tests/Container/ContainerTest.php
+++ b/tests/Container/ContainerTest.php
@@ -232,7 +232,7 @@
// 5) Attribute-based Method Injection (ClassA->resolveIt())
// -------------------------------------------------------------------------
-test('Method injection with attribute Infuse (ClassA->resolveIt)', function () {
+test('Method injection with attribute Inject (ClassA->resolveIt)', function () {
$container = container(null, 'classA_method_test')
->options()
->setOptions(true, true)
diff --git a/tests/Container/InvokerTest.php b/tests/Container/InvokerTest.php
index 558f5ee..04991e3 100644
--- a/tests/Container/InvokerTest.php
+++ b/tests/Container/InvokerTest.php
@@ -8,6 +8,7 @@
use Infocyph\InterMix\DI\Container;
use Infocyph\InterMix\DI\Invoker;
use Infocyph\InterMix\Exceptions\ContainerException;
+use Infocyph\InterMix\Serializer\ClosureSerializer;
/* -----------------------------------------------------------------
| Fixtures
@@ -122,16 +123,26 @@ public static function json(): array
->toBe('UTC');
});
-it('throws a ContainerException on unsupported target', function () {
+it('throws an InvalidArgumentException on unsupported target', function () {
$this->inv->invoke('not-a-callable');
-})->throws(ContainerException::class);
+})->throws(InvalidArgumentException::class);
/* Opis-packed closure ------------------------------------------------------ */
it('executes a serialized-closure string', function () {
- $packed = $this->inv->serialize(fn() => 'packed');
+ $packed = ClosureSerializer::serialize(fn() => 'packed');
expect($this->inv->invoke($packed))->toBe('packed');
});
+it('invokes a static class-method string', function () {
+ expect($this->inv->invoke(StaticController::class . '::health'))
+ ->toBe(['status' => 'ok']);
+});
+
+it('resolves a class-string', function () {
+ expect($this->inv->invoke(MyService::class))
+ ->toBeInstanceOf(MyService::class);
+});
+
/* -----------------------------------------------------------------
| 2. resolve()
|-----------------------------------------------------------------*/
@@ -168,15 +179,6 @@ public static function json(): array
/* -----------------------------------------------------------------
| 4. Serializer round-trip
|-----------------------------------------------------------------*/
-it('serialises and restores closures', function () {
- $packed = $this->inv->serialize(fn() => 42);
- $restored = $this->inv->unserialize($packed);
-
- expect($restored)
- ->toBeInstanceOf(Closure::class)
- ->and($restored())->toBe(42);
-});
-
/* -----------------------------------------------------------------
| 5. NEW — pure callable + DI-injected parameters
|-----------------------------------------------------------------*/
diff --git a/tests/Fence/LimitTest.php b/tests/Fence/LimitTest.php
index d2db401..abed51f 100644
--- a/tests/Fence/LimitTest.php
+++ b/tests/Fence/LimitTest.php
@@ -11,9 +11,7 @@ class LimitTraitTest
}
beforeEach(function () {
- // Ensure a clean slate before each test
- LimitTraitTest::setLimit(2);
- LimitTraitTest::clearInstances();
+ LimitTraitTest::reset();
});
test('it creates instances up to the defined limit', function () {
@@ -43,3 +41,19 @@ class LimitTraitTest
expect($three)->toBeInstanceOf(LimitTraitTest::class);
});
+
+test('clearInstances preserves an override while reset restores the declared limit', function () {
+ LimitTraitTest::setLimit(3);
+ LimitTraitTest::clearInstances();
+
+ LimitTraitTest::instance('one');
+ LimitTraitTest::instance('two');
+ LimitTraitTest::instance('three');
+
+ LimitTraitTest::reset();
+ LimitTraitTest::instance('one');
+ LimitTraitTest::instance('two');
+
+ expect(fn() => LimitTraitTest::instance('three'))
+ ->toThrow(LimitExceededException::class);
+});
diff --git a/tests/Fixture/ClassA.php b/tests/Fixture/ClassA.php
index 28e0067..79a467f 100644
--- a/tests/Fixture/ClassA.php
+++ b/tests/Fixture/ClassA.php
@@ -4,22 +4,22 @@
namespace Infocyph\InterMix\Tests\Fixture;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
class ClassA implements InterfaceA
{
/**
* Showcases method injection with:
* 1) "ClassB $classB" => typed param
- * 2) "string $parameterA" => with attribute Infuse(...) => if no param is given, use that
- * 3) "string $parameterB" => with #[Infuse('db.host')] => from definition
+ * 2) "string $parameterA" => with attribute Inject(...) => if no param is given, use that
+ * 3) "string $parameterB" => with #[Inject('db.host')] => from definition
* 4) variadic $parameterC => leftover parameters
*/
- #[Infuse(parameterA: 'gethostname')]
+ #[Inject(parameterA: 'gethostname')]
public function resolveIt(
ClassB $classB,
string $parameterA,
- #[Infuse('db.host')] string $parameterB,
+ #[Inject('db.host')] string $parameterB,
...$parameterC
): array {
return [
diff --git a/tests/Fixture/LogicOnlyAttrResolver.php b/tests/Fixture/LogicOnlyAttrResolver.php
index 78af69c..2031183 100644
--- a/tests/Fixture/LogicOnlyAttrResolver.php
+++ b/tests/Fixture/LogicOnlyAttrResolver.php
@@ -3,6 +3,7 @@
declare(strict_types=1);
namespace Infocyph\InterMix\Tests\Fixture;
use Infocyph\InterMix\DI\Attribute\AttributeResolverInterface;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
use Infocyph\InterMix\DI\Container;
use Reflector;
@@ -11,6 +12,6 @@ class LogicOnlyAttrResolver implements AttributeResolverInterface
public function resolve(object $attr, Reflector $target, Container $c): mixed
{
$c->logger()?->log($attr->level, "[Attr] $target handled");
- return null;
+ return AttributeResolution::Unresolved;
}
}
diff --git a/tests/Fixture/MethodAttrResolver.php b/tests/Fixture/MethodAttrResolver.php
index eae2cc3..9ecf54a 100644
--- a/tests/Fixture/MethodAttrResolver.php
+++ b/tests/Fixture/MethodAttrResolver.php
@@ -6,6 +6,7 @@
use Infocyph\InterMix\DI\Container;
use Infocyph\InterMix\DI\Attribute\AttributeResolverInterface;
+use Infocyph\InterMix\DI\Attribute\AttributeResolution;
use Reflector;
class MethodAttrResolver implements AttributeResolverInterface
@@ -14,7 +15,7 @@ public function resolve(
object $attributeInstance,
Reflector $target,
Container $container
- ): null {
+ ): AttributeResolution {
fwrite(
STDERR,
'[TEST] '
@@ -26,6 +27,6 @@ public function resolve(
. "\n"
);
- return null;
+ return AttributeResolution::Unresolved;
}
}
diff --git a/tests/Fixture/MixedAttributeExample.php b/tests/Fixture/MixedAttributeExample.php
index f1bd7e4..584b811 100644
--- a/tests/Fixture/MixedAttributeExample.php
+++ b/tests/Fixture/MixedAttributeExample.php
@@ -3,16 +3,15 @@
declare(strict_types=1);
namespace Infocyph\InterMix\Tests\Fixture;
-use Infocyph\InterMix\DI\Attribute\Autowire;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
use stdClass;
class MixedAttributeExample
{
- #[Autowire]
+ #[Inject]
public ?stdClass $std = null;
- #[Infuse('name')]
+ #[Inject('name')]
public string $name;
#[ExampleAttr('TEST')]
diff --git a/tests/Fixture/NotificationService.php b/tests/Fixture/NotificationService.php
index 4534f7b..bef9753 100644
--- a/tests/Fixture/NotificationService.php
+++ b/tests/Fixture/NotificationService.php
@@ -4,11 +4,11 @@
namespace Infocyph\InterMix\Tests\Fixture;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
class NotificationService
{
- #[Infuse(FileLogger::class)]
+ #[Inject(FileLogger::class)]
public LoggerInterface $logger;
public function notify(string $message): void
diff --git a/tests/Fixture/ParentPropertyClass.php b/tests/Fixture/ParentPropertyClass.php
index fe5e431..2a37d0b 100644
--- a/tests/Fixture/ParentPropertyClass.php
+++ b/tests/Fixture/ParentPropertyClass.php
@@ -4,11 +4,11 @@
namespace Infocyph\InterMix\Tests\Fixture;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
class ParentPropertyClass
{
- #[Infuse('db.port')]
+ #[Inject('db.port')]
private string $dbPort;
public function getDbPort(): string
diff --git a/tests/Fixture/PropertyClass.php b/tests/Fixture/PropertyClass.php
index cf80c88..720644a 100644
--- a/tests/Fixture/PropertyClass.php
+++ b/tests/Fixture/PropertyClass.php
@@ -4,7 +4,7 @@
namespace Infocyph\InterMix\Tests\Fixture;
-use Infocyph\InterMix\DI\Attribute\Infuse;
+use Infocyph\InterMix\DI\Attribute\Inject;
class PropertyClass extends ParentPropertyClass
{
@@ -12,16 +12,16 @@ class PropertyClass extends ParentPropertyClass
private string $nothing;
- #[Infuse]
+ #[Inject]
private ClassA $classA;
- #[Infuse('db.host')]
+ #[Inject('db.host')]
private string $something;
- #[Infuse(strtotime: 'last monday')]
+ #[Inject(strtotime: 'last monday')]
private int $yesterday;
- #[Infuse(strtotime: ['last monday', 1678786990])]
+ #[Inject(strtotime: ['last monday', 1678786990])]
private int $yesterdayFromADate;
public function __get(string $key): mixed
diff --git a/tests/Fixture/Resources.php b/tests/Fixture/Resources.php
deleted file mode 100644
index e811fa4..0000000
--- a/tests/Fixture/Resources.php
+++ /dev/null
@@ -1,14 +0,0 @@
-stream = \fopen('php://memory', 'rb');
- }
-}
diff --git a/tests/Regression/BugFixesTest.php b/tests/Regression/BugFixesTest.php
index 8b9a13f..8d241bb 100644
--- a/tests/Regression/BugFixesTest.php
+++ b/tests/Regression/BugFixesTest.php
@@ -6,32 +6,10 @@
use Infocyph\InterMix\DI\Support\LifetimeEnum;
use Infocyph\InterMix\DI\Support\PreloadGenerator;
use Infocyph\InterMix\DI\Support\TraceLevelEnum;
-use Infocyph\InterMix\Serializer\ResourceHandlers;
-use Infocyph\InterMix\Serializer\ValueSerializer;
use Infocyph\InterMix\Tests\Fixture\NamespacedClosureFactory;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
-class RegressionResourceHandlers extends ResourceHandlers
-{
- public static function registerStream(): void
- {
- ValueSerializer::registerResourceHandler(
- 'stream',
- function ($res): array {
- rewind($res);
- return ['content' => stream_get_contents($res)];
- },
- function (array $data) {
- $s = fopen('php://memory', 'r+');
- fwrite($s, $data['content']);
- rewind($s);
- return $s;
- }
- );
- }
-}
-
interface RegressionTokenSource
{
public function token(): string;
@@ -201,22 +179,6 @@ public function commit(): bool
->and($c->get('answer'))->toBe(42);
});
-it('registerDefaults discovers subclass register methods', function () {
- ValueSerializer::clearResourceHandlers();
- RegressionResourceHandlers::registerDefaults();
-
- $s = fopen('php://memory', 'r+');
- fwrite($s, 'ok');
- rewind($s);
-
- $blob = ValueSerializer::serialize($s);
- $rest = ValueSerializer::unserialize($blob);
-
- expect(is_resource($rest))->toBeTrue()
- ->and(get_resource_type($rest))->toBe('stream')
- ->and(stream_get_contents($rest))->toBe('ok');
-});
-
it('does not reuse non-constructor method parameter resolution values across calls', function () {
$c = Container::instance(uniqid('method_args_'));
$c->definitions()->bind(
diff --git a/tests/Remix/MacroMixTest.php b/tests/Remix/MacroMixTest.php
index 42a18bc..974310c 100644
--- a/tests/Remix/MacroMixTest.php
+++ b/tests/Remix/MacroMixTest.php
@@ -7,10 +7,19 @@
class MacroTestClass
{
use MacroMix;
+
public const ENABLE_LOCK = true;
+
public string $name = '';
}
+class UnlockedMacroTestClass
+{
+ use MacroMix;
+
+ public const ENABLE_LOCK = false;
+}
+
it('registers & calls macros', function () {
MacroTestClass::macro('sayHello', fn () => 'Hello, MacroMix!');
expect(MacroTestClass::sayHello())->toBe('Hello, MacroMix!');
@@ -115,3 +124,59 @@ public function shout($value)
MacroTestClass::class
));
});
+
+it('uses the direct mutation path when locking is disabled', function () {
+ UnlockedMacroTestClass::macro('direct', static fn(): string => 'ok');
+ UnlockedMacroTestClass::loadMacrosFromConfig([
+ 'configured' => static fn(): string => 'configured',
+ ]);
+
+ expect(UnlockedMacroTestClass::direct())->toBe('ok')
+ ->and(UnlockedMacroTestClass::configured())->toBe('configured');
+
+ UnlockedMacroTestClass::removeMacro('direct');
+ expect(UnlockedMacroTestClass::hasMacro('direct'))->toBeFalse();
+});
+
+it('protects direct and bulk mutations when locking is enabled', function () {
+ MacroTestClass::macro('lockedDirect', static fn(): int => 1);
+ MacroTestClass::loadMacrosFromConfig([
+ 'lockedA' => static fn(): int => 2,
+ 'lockedB' => static fn(): int => 3,
+ ]);
+
+ expect(MacroTestClass::lockedDirect())->toBe(1)
+ ->and(MacroTestClass::lockedA())->toBe(2)
+ ->and(MacroTestClass::lockedB())->toBe(3);
+
+ MacroTestClass::removeMacro('lockedDirect');
+ expect(MacroTestClass::hasMacro('lockedDirect'))->toBeFalse();
+});
+
+it('releases the mutation lock when an operation throws', function () {
+ $mutate = new ReflectionMethod(MacroTestClass::class, 'mutate');
+
+ expect(fn() => $mutate->invoke(null, static fn() => throw new RuntimeException('failed')))
+ ->toThrow(RuntimeException::class, 'failed');
+
+ $lockHandle = new ReflectionMethod(MacroTestClass::class, 'lockHandle');
+ $handle = $lockHandle->invoke(null);
+
+ expect(flock($handle, LOCK_EX | LOCK_NB))->toBeTrue();
+ flock($handle, LOCK_UN);
+});
+
+it('keeps reads and macro execution lock-free', function () {
+ MacroTestClass::macro('lockFreeRead', static fn(): string => 'read');
+ $lockHandle = new ReflectionMethod(MacroTestClass::class, 'lockHandle');
+ $handle = $lockHandle->invoke(null);
+ flock($handle, LOCK_EX);
+
+ try {
+ expect(MacroTestClass::hasMacro('lockFreeRead'))->toBeTrue()
+ ->and(MacroTestClass::getMacros())->toHaveKey('lockFreeRead')
+ ->and(MacroTestClass::lockFreeRead())->toBe('read');
+ } finally {
+ flock($handle, LOCK_UN);
+ }
+});
diff --git a/tests/Serializer/ClosureSerializerTest.php b/tests/Serializer/ClosureSerializerTest.php
new file mode 100644
index 0000000..99d9e21
--- /dev/null
+++ b/tests/Serializer/ClosureSerializerTest.php
@@ -0,0 +1,82 @@
+ 'ok'),
+ );
+
+ expect($closure())->toBe('ok');
+});
+
+it('round-trips captured scalars objects and nested data', function () {
+ $scalar = 7;
+ $object = (object) ['value' => 11];
+ $nested = ['items' => [['value' => 13]]];
+
+ $closure = ClosureSerializer::unserialize(ClosureSerializer::serialize(
+ static fn(): int => $scalar + $object->value + $nested['items'][0]['value'],
+ ));
+
+ expect($closure())->toBe(31);
+});
+
+it('recognizes only the unsigned InterMix envelope', function () {
+ $payload = ClosureSerializer::serialize(static fn(): null => null);
+
+ expect($payload)->toStartWith('imxc1.')
+ ->and(ClosureSerializer::isSerialized($payload))->toBeTrue()
+ ->and(ClosureSerializer::isSerialized('not-a-payload'))->toBeFalse()
+ ->and(ClosureSerializer::isSerialized('imxcs1.signature.payload'))->toBeFalse();
+});
+
+it('rejects missing malformed and non-Closure payloads', function (string $payload) {
+ expect(fn() => ClosureSerializer::unserialize($payload))
+ ->toThrow(InvalidArgumentException::class);
+})->with([
+ 'missing envelope' => 'not-a-payload',
+ 'empty body' => 'imxc1.',
+ 'invalid base64' => 'imxc1.*',
+ 'ordinary serialized value' => 'imxc1.' . base64_encode(serialize(['value'])),
+]);
+
+it('round-trips a signed Closure', function () {
+ $serializer = ClosureSerializer::signed('test-key');
+ $payload = $serializer->serialize(static fn(int $value): int => $value * 2);
+ $closure = $serializer->unserialize($payload);
+
+ expect($payload)->toStartWith('imxcs1.')
+ ->and($closure(6))->toBe(12);
+});
+
+it('rejects an empty signing key', function () {
+ expect(fn() => ClosureSerializer::signed(''))
+ ->toThrow(InvalidArgumentException::class);
+});
+
+it('rejects a wrong signing key and tampering', function () {
+ $serializer = ClosureSerializer::signed('correct-key');
+ $payload = $serializer->serialize(static fn(): string => 'safe');
+ $parts = explode('.', $payload, 3);
+ $parts[2][0] = $parts[2][0] === 'A' ? 'B' : 'A';
+ $tampered = implode('.', $parts);
+
+ expect(fn() => ClosureSerializer::signed('wrong-key')->unserialize($payload))
+ ->toThrow(InvalidArgumentException::class)
+ ->and(fn() => $serializer->unserialize($tampered))
+ ->toThrow(InvalidArgumentException::class);
+});
+
+it('keeps signed and unsigned envelopes separate', function () {
+ $unsigned = ClosureSerializer::serialize(static fn(): null => null);
+ $signedSerializer = ClosureSerializer::signed('test-key');
+ $signed = $signedSerializer->serialize(static fn(): null => null);
+
+ expect(fn() => $signedSerializer->unserialize($unsigned))
+ ->toThrow(InvalidArgumentException::class)
+ ->and(fn() => ClosureSerializer::unserialize($signed))
+ ->toThrow(InvalidArgumentException::class);
+});
diff --git a/tests/Serializer/ResourceHandlersTest.php b/tests/Serializer/ResourceHandlersTest.php
deleted file mode 100644
index ae9396f..0000000
--- a/tests/Serializer/ResourceHandlersTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
- $meta['mode'],
- 'content' => stream_get_contents($res),
- ];
- },
- function (array $d) {
- $s = fopen('php://memory', $d['mode']);
- fwrite($s, $d['content']);
- rewind($s);
- return $s;
- }
- );
- }
-}
-
-it('stream handler round-trip via MyResourceHandlers', function () {
- MyResourceHandlers::registerStream();
-
- $s = fopen('php://memory', 'r+');
- fwrite($s, 'hello');
- rewind($s);
-
- $blob = ValueSerializer::serialize($s);
- $rest = ValueSerializer::unserialize($blob);
-
- expect(is_resource($rest))
- ->toBeTrue()
- ->and(get_resource_type($rest))->toBe('stream')
- ->and(stream_get_contents($rest))->toBe('hello');
-});
-
-it('registerDefaults invokes registerStream()', function () {
- MyResourceHandlers::registerDefaults();
-
- $s = fopen('php://memory', 'r+');
- fwrite($s, 'xyz');
- rewind($s);
-
- $blob = ValueSerializer::serialize($s);
- $rest = ValueSerializer::unserialize($blob);
-
- expect(get_resource_type($rest))
- ->toBe('stream')
- ->and(stream_get_contents($rest))->toBe('xyz');
-});
diff --git a/tests/Serializer/ValueSerializerTest.php b/tests/Serializer/ValueSerializerTest.php
deleted file mode 100644
index ab1c106..0000000
--- a/tests/Serializer/ValueSerializerTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
- 'x', 'b' => ['nested' => true]],
- ];
-
- foreach ($values as $v) {
- $blob = ValueSerializer::serialize($v);
- $out = ValueSerializer::unserialize($blob);
- expect($out)->toBe($v);
- }
-});
-
-it('round-trips closures', function () {
- $fn = fn (int $x): int => $x + 2;
- $blob = ValueSerializer::serialize($fn);
- $rest = ValueSerializer::unserialize($blob);
-
- expect(is_callable($rest))
- ->toBeTrue()
- ->and($rest(5))->toBe(7);
-});
-
-it('wraps and unwraps without full serialization', function () {
- $data = ['foo' => 'bar', 'baz' => [1, 2, 3]];
- $wrapped = ValueSerializer::wrap($data);
- expect($wrapped)->toBe($data);
-
- $unwrapped = ValueSerializer::unwrap($wrapped);
- expect($unwrapped)->toBe($data);
-});
-
-it('throws when wrapping a resource with no handler', function () {
- $s = fopen('php://memory', 'r+');
-
- expect(fn () => ValueSerializer::wrap($s))
- ->toThrow(InvalidArgumentException::class)
- ->and(fn () => ValueSerializer::serialize($s))
- ->toThrow(InvalidArgumentException::class);
-
- fclose($s);
-});
-
-it('verifies signed payloads when signing key is configured', function () {
- ValueSerializer::setPayloadSigningKey('test-signing-key');
-
- $payload = ['id' => 42, 'cb' => fn () => 'ok'];
- $blob = ValueSerializer::serialize($payload);
- $out = ValueSerializer::unserialize($blob);
-
- expect($out['id'])->toBe(42)
- ->and(($out['cb'])())->toBe('ok');
-});
-
-it('rejects tampered payloads when signing key is configured', function () {
- ValueSerializer::setPayloadSigningKey('test-signing-key');
-
- $blob = ValueSerializer::serialize(['safe' => true]);
- $tampered = substr_replace($blob, 'x', -1);
-
- expect(fn () => ValueSerializer::unserialize($tampered))
- ->toThrow(InvalidArgumentException::class);
-});
-
-it('rejects an empty signing key at the signed serializer boundary', function () {
- expect(fn () => ValueSerializer::signed(''))
- ->toThrow(InvalidArgumentException::class);
-});
-
-it('keeps signed serializer instances isolated from global key state', function () {
- ValueSerializer::setPayloadSigningKey('global-key');
- $signed = ValueSerializer::signed('scoped-key');
-
- $payload = $signed->encode(['scope' => 'local']);
-
- expect(ValueSerializer::currentPayloadSigningKey())->toBe('global-key')
- ->and($signed->decode($payload))->toBe(['scope' => 'local'])
- ->and(ValueSerializer::currentPayloadSigningKey())->toBe('global-key')
- ->and(fn () => ValueSerializer::decode($payload))
- ->toThrow(InvalidArgumentException::class);
-});
-
-it('requires the exact wrapped-resource marker before restoring', function () {
- ValueSerializer::registerResourceHandler(
- 'test-resource',
- static fn(mixed $value): mixed => $value,
- static fn(mixed $value): string => "restored:$value",
- );
-
- $lookalike = [
- '__wrapped_resource' => 1,
- 'type' => 'test-resource',
- 'data' => 'payload',
- ];
-
- expect(ValueSerializer::unwrap($lookalike))->toBe($lookalike);
-});