← Dependency Injection

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.

SatisfiesServiceContract SatisfiesRepositoryContract contracts:list factoryKey

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.

© Brian Kernighan: "Controlling complexity is the essence of computer programming."

Handler Application entry point
<?phpdeclare(strict_types=1);namespace App\Application\Handler\Container;use App\Application\Payload\Container\ServiceContractPayload;use App\Application\Resource\Page\MailerStatusResource;use App\Domain\Mail\MailerInterface;use Semitexa\Core\Attribute\AsPayloadHandler;use Semitexa\Core\Attribute\InjectAsReadonly;use Semitexa\Core\Contract\TypedHandlerInterface;#[AsPayloadHandler(payload: ServiceContractPayload::class, resource: MailerStatusResource::class)]final class ServiceContractHandler implements TypedHandlerInterface{    #[InjectAsReadonly]    protected MailerInterface $mailer;    public function handle(ServiceContractPayload $payload, MailerStatusResource $resource): MailerStatusResource    {        return $resource->withResolvedMailer($this->mailer::class);    }}

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.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal