DEPENDENCY INJECTION FEATURE
Service Contracts
Depend on contracts, but keep ownership explicit — deterministic substitution instead of runtime magic.
Feature Guide
A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.
What this does
A service contract is module-owned and explicit: one module declares the capability, and its implementations advertise themselves with #[SatisfiesServiceContract].
How it works
The container registry resolves contracts at boot from attributes, not string lookups. For keyed factories, Semitexa uses closed-world backed enums so the allowed variants are declared in code and validated exhaustively.
Why it matters
This keeps substitution deterministic instead of magical. A reader can see who owns the capability, which implementations exist, and whether the selection space is complete without reverse-engineering runtime behavior.
Key concepts
- #[SatisfiesServiceContract]
- Marks a class as the implementation of a service contract interface.
- module-owned capability
- A contract lives with the module that owns the behavior and ships at least one valid implementation.
- closed-world factory
- A factory whose selectable implementations are exhaustively declared by a backed enum instead of open-ended strings.
Interface Binding
Handlers depend on contracts
The container resolves a contract at boot from explicit ownership and implementation metadata, not from hidden container lookups.
// Contract owned by the module:
interface MailerInterface { public function send(Mail $mail): void; }
// Explicit implementation:
#[SatisfiesServiceContract(of: MailerInterface::class)]
final class SmtpMailer implements MailerInterface { ... }
// Handler only knows the contract:
#[InjectAsReadonly]
protected MailerInterface $mailer;
The goal is not unlimited runtime swapping. The goal is a reviewable, deterministic binding graph.
Verified against Semitexa Ultimate 2026.09.19.1020
Service Contracts
A service contract is an interface one module owns, and any module may implement. Implementations advertise themselves — nothing registers them by hand, and nothing looks them up by string.
#[AsService]
#[SatisfiesServiceContract(of: CollectionQueryCompilerInterface::class)]
final class CollectionQueryCompiler implements CollectionQueryCompilerInterface
That is Semitexa\Orm\Query\CollectionQueryCompiler. Consumers keep depending on the interface:
#[InjectAsReadonly]
protected CollectionQueryCompilerInterface $compiler;
#[SatisfiesServiceContract] also makes the class container-managed by itself, so the #[AsService] above is a statement of role rather than a requirement.
Two attributes, one idea
| Attribute | Arguments | For |
|---|---|---|
#[SatisfiesServiceContract] |
of, factoryKey |
service capabilities |
#[SatisfiesRepositoryContract] |
of |
repository capabilities — see repository workflow |
The repository variant has no factoryKey: a repository contract has one active implementation, not a keyed set.
Seeing who won
When two modules implement the same contract, one is active. Do not guess:
bin/semitexa contracts:list
Contract (interface) Implementations (module → class) Active
FormCollabDraftStoreInterface semitexa-platform-site → ShowcaseFormCollabDraftStore ✓ ShowcaseFormCollabDraftStore
semitexa-platform-ui → FormCollabDraftDbRepository
The ✓ marks the active implementation and the second row shows the one it displaced. Contract resolution covers how that choice is made and how to override it.
When several implementations must coexist
If callers need to choose an implementation at call time rather than one being active, every implementation declares an enum-backed factoryKey and consumers inject a factory instead:
#[SatisfiesServiceContract(of: StorageInterface::class, factoryKey: StorageKey::S3)]
This is all-or-nothing: miss the key on one implementation and boot fails with a named error. See factory injection.
Why attributes rather than a config file
The declaration lives on the class that makes the promise, so a reader who has the implementation open can see what it claims to satisfy, and a reader who has the interface open can run one command to see every claimant. There is no registry file to drift out of step with the code, and no string id to typo.
How it works
The container registry resolves contracts at boot from attributes, not string lookups. For keyed factories, Semitexa uses closed-world backed enums so the allowed variants are declared in code and validated exhaustively.
Why it matters
This keeps substitution deterministic instead of magical. A reader can see who owns the capability, which implementations exist, and whether the selection space is complete without reverse-engineering runtime behavior.
Key concepts
- #[SatisfiesServiceContract]
- Marks a class as the implementation of a service contract interface.
- module-owned capability
- A contract lives with the module that owns the behavior and ships at least one valid implementation.
- closed-world factory
- A factory whose selectable implementations are exhaustively declared by a backed enum instead of open-ended strings.