omegaalfa / container
Small, typed and production-ready PSR-11 dependency injection container for PHP 8.4.
Requires
- php: ^8.4
- psr/container: ^2.0
Requires (Dev)
- omegaalfa/lazy-object: ^1.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- squizlabs/php_codesniffer: ^3.13
Suggests
- omegaalfa/lazy-object: Enables native PHP 8.4 lazy services when the package becomes available.
Provides
- psr/container-implementation: 1.0.0
This package is not auto-updated.
Last update: 2026-08-03 01:09:04 UTC
README
🧩 Omegaalfa Container
Dependency Injection PSR-11 explícito e tipado para PHP 8.4
O problema que este pacote resolve
Em uma aplicação pequena, montar objetos manualmente é simples:
$config = new Config(...); $logger = new Logger(...); $database = new Database($config, $logger); $repository = new UserRepository($database); $service = new UserService($repository, $logger); $controller = new UserController($service);
Conforme a aplicação cresce, essa montagem se repete. A ordem fica difícil de controlar, trocar implementações exige mudanças em vários arquivos, algumas instâncias precisam ser compartilhadas e outras precisam ser novas. Testes também passam a conhecer detalhes de infraestrutura.
Com o container, as regras ficam centralizadas:
use Omegaalfa\Container\ContainerBuilder; use Psr\Container\ContainerInterface; $builder = new ContainerBuilder(); $builder->set(Config::class, new Config(...)); $builder->singleton(Logger::class, static fn(): Logger => new Logger()); $builder->singleton( Database::class, static fn(ContainerInterface $container): Database => new Database( $container->get(Config::class), $container->get(Logger::class), ), ); $builder->autowire(UserRepository::class); $builder->autowire(UserService::class); $builder->autowire(UserController::class); $container = $builder->build(); $controller = $container->get(UserController::class);
Em vez de cada parte da aplicação decidir como criar suas dependências, essa responsabilidade fica concentrada na configuração do container.
O que é injeção de dependências?
Sem injeção, uma classe cria internamente aquilo de que precisa:
final class UserService { public function __construct() { $database = new Database(); $this->repository = new UserRepository($database); } }
Com injeção, a classe recebe a dependência:
final class UserService { public function __construct(private UserRepository $repository) { } }
Dependency Injection é uma técnica de projeto. O container é uma ferramenta que ajuda a aplicá-la; eles não são a mesma coisa.
ContainerBuilder e Container: qual é a diferença?
O builder funciona como a receita; o container é a estrutura pronta.
| Objeto | Responsabilidade |
|---|---|
ContainerBuilder |
Registrar e configurar serviços |
Container |
Resolver e devolver serviços durante a execução |
build() |
Criar um snapshot da configuração atual |
Snapshot é uma cópia naquele momento. Alterar o builder depois de build() não altera containers já construídos. Cada container mantém seus próprios singletons e proxies lazy.
Exemplo completo para iniciantes
O arquivo examples/beginner.php demonstra set(), singleton(), factory(), autowire(), alias(), lazy(), get(), has(), make() e call().
composer install php examples/beginner.php
Important
O builder configura; o container resolve. Autowiring é explícito, interfaces não são adivinhadas e escalares nunca são inventados.
📑 Índice
- Recursos e instalação
- Início rápido
- Ciclos de vida
- Values, singletons, factories e aliases
- Autowiring
make()ecall()- Lazy services
- Referência da API pública
- Exceções e PSR-11
- Workers, segurança e limitações
- Desenvolvimento
✨ Recursos
- 📦 values prontos: escalares, objetos e closures;
- ♻️ singletons lazy por container;
- 🏭 factories transient;
- 🔗 aliases que preservam o ciclo de vida;
- 🧠 autowiring explícito por construtor e cache de Reflection;
- 🧰
make()para criação transient ecall()para invocação; - 💤 proxies nativos via
omegaalfa/lazy-object; - 🔄 ciclos detectados com caminho completo;
- ✅
Psr\Container\ContainerInterface.
📋 Instalação
Requer PHP ^8.4, Composer 2 e psr/container ^2.0.
composer require omegaalfa/container
Para lazy services:
composer require omegaalfa/lazy-object
Checkout local:
composer install php exemplo.php
🚀 Início rápido
<?php declare(strict_types=1); use Omegaalfa\Container\ContainerBuilder; use Psr\Container\ContainerInterface; $builder = new ContainerBuilder(); $builder->set('app.name', 'Minha aplicação'); $builder->set(Config::class, new Config('production')); $builder->singleton( Logger::class, static fn (ContainerInterface $_container): Logger => new Logger(), ); $builder->alias(LoggerInterface::class, Logger::class); $builder->autowire(UserRepository::class); $builder->autowire(UserService::class); $container = $builder->build(); $service = $container->get(UserService::class);
O arquivo exemplo.php é executável e cobre todos os recursos principais:
php exemplo.php
🔁 Ciclos de vida
| Registro | Execução | Cache | Identidade |
|---|---|---|---|
set() |
valor já pronto | definição | sempre a mesma |
singleton() |
primeiro get() |
resultado | a mesma por container |
factory() |
todo get() |
nenhum | normalmente diferente |
autowire() |
todo get() |
nenhum | diferente |
lazy() |
proxy no primeiro get(); real no acesso |
proxy | mesmo proxy |
alias() |
segue destino | segue destino | segue destino |
make() |
toda chamada | nenhum | diferente |
Containers de chamadas distintas a build() não compartilham singletons, proxies ou estado de resolução.
📦 Values, singletons, factories e aliases
Value
set() devolve exatamente o valor registrado. Closure registrada assim não é executada.
$builder->set('app.name', 'Omegaalfa'); $builder->set('limit', 100); $builder->set(LoggerInterface::class, new ConsoleLogger()); $builder->set('callback', static fn (): string => 'valor');
Singleton
A factory executa uma vez, somente no primeiro get().
$builder->singleton( DatabaseConnection::class, static fn (ContainerInterface $container): DatabaseConnection => new DatabaseConnection($container->get(Config::class)->dsn), );
Falhas não armazenam resultados parciais. A exceção original é preservada e outra resolução tenta novamente.
Factory transient
Executa em cada resolução e não armazena resultado.
$builder->factory( RequestContext::class, static fn (ContainerInterface $_container): RequestContext => new RequestContext(), );
Alias
$builder->singleton(RedisCache::class, static fn (): RedisCache => new RedisCache()); $builder->alias(CacheInterface::class, RedisCache::class);
O alias preserva o ciclo de vida do destino. Self-alias é rejeitado. Destino ausente ou cadeia circular faz has() retornar false.
Note
O container pode ser usado em factories na composition root. Evite injetá-lo nos serviços como Service Locator.
🧠 Autowiring
$builder->autowire(UserRepository::class); $builder->autowire(UserService::class);
Política:
- resolve tipos de objeto registrados;
- interfaces exigem registro ou alias explícito;
- usa defaults quando não há resolução;
- nullable recebe
nullsem candidato; - escalares obrigatórios e parâmetros sem tipo não são inventados;
- union exige exatamente um candidato;
- intersection é rejeitada;
- classes abstratas, inexistentes e construtores não públicos são rejeitados;
- variádicos sem valores explícitos ficam vazios;
- nenhuma propriedade é injetada após a construção.
final readonly class UserService { public function __construct( public UserRepository $repository, public int $limit = 25, ) {} }
🧰 make() e call()
make() cria objeto transient, resolve dependências e aceita parâmetros por nome:
$controller = $container->make( UserController::class, ['request' => $request, 'limit' => 50], );
Não modifica eventual singleton registrado. Parâmetro desconhecido ou obrigatório ausente gera UnresolvableParameterException.
call() resolve closures, funções e métodos:
$result = $container->call( [$controller, 'handle'], ['request' => $request], );
$result = $container->call( static fn (LoggerInterface $logger, string $message): string => $handler->handle($logger, $message), ['message' => 'Processar pedido'], );
O retorno não é transformado. Use somente callables confiáveis.
💤 Lazy services
lazy() chama Omegaalfa\LazyObject\LazyObject::proxy(); não há proxy próprio.
$builder->lazy( ReportGenerator::class, static fn (ContainerInterface $container): ReportGenerator => new ReportGenerator($container->get(DatabaseConnection::class)), ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE, ); $container = $builder->build(); $report = $container->get(ReportGenerator::class); // proxy echo $report->status; // inicializa a instância real
Comportamento:
- registro,
build()ehas()não executam a factory; - o primeiro
get()cria um proxy ainda não inicializado; - o proxy é singleton por container;
- a factory captura o container, nunca o builder;
- opções nativas são encaminhadas sem alteração;
- pacote ausente gera
LazyServiceException, sem fallback eager; - erros da biblioteca e do PHP são preservados;
- classes sem propriedades podem não permanecer lazy.
Lazy loading adia criação; não substitui Dependency Injection.
📚 Referência da API pública
ContainerBuilder
| Assinatura | Descrição |
|---|---|
set(string $id, mixed $value, bool $override = false): self |
Registra valor pronto. |
singleton(string $id, Closure $factory, bool $override = false): self |
Factory compartilhada. |
factory(string $id, Closure $factory, bool $override = false): self |
Factory transient. |
alias(string $id, string $target, bool $override = false): self |
Identificador alternativo. |
autowire(string $class, bool $override = false): self |
Construção explícita por Reflection. |
lazy(string $class, Closure $factory, int $options = 0, bool $override = false): self |
Proxy lazy singleton. |
addDefinitions(array $definitions, bool $override = false): self |
Objetos de definição; API de baixo nível. |
build(): Container |
Snapshot independente das definições. |
Identificador vazio é rejeitado. Duplicatas exigem override: true. Containers já construídos não mudam quando o builder é alterado.
Container
| Assinatura | Descrição |
|---|---|
get(string $id): mixed |
Resolve entrada conforme o ciclo de vida. |
has(string $id): bool |
Verifica sem construir ou executar factory. |
make(string $class, array $parameters = []): object |
Cria instância transient. |
call(callable $callable, array $parameters = []): mixed |
Injeta argumentos e invoca callable. |
has() === true indica definição conhecida, não que a construção futura será infalível. Prefira ContainerBuilder::build() ao construtor direto de Container.
🚨 Exceções
| Exceção | Situação |
|---|---|
ContainerException |
base PSR-11 |
NotFoundException |
entrada ausente; implementa NotFoundExceptionInterface |
CircularDependencyException |
ciclo em alias, factory ou autowiring |
InvalidDefinitionException |
id vazio, duplicata ou definição inválida |
AutowiringException |
classe não construível |
AmbiguousTypeException |
union ambígua |
UnresolvableParameterException |
parâmetro não resolvível |
LazyServiceException |
pacote lazy ausente |
Circular dependency detected: A -> B -> C -> A
Exceções de factories, construtores e callables consumidores são preservadas.
🔌 PSR-11
Container implementa Psr\Container\ContainerInterface. A PSR-11 padroniza apenas get() e has(); registros são extensões do pacote.
function boot(Psr\Container\ContainerInterface $container): void { if ($container->has(Application::class)) { $application = $container->get(Application::class); } }
O Composer fornece psr/container-implementation: 1.0.0, conforme a PSR-11. Isso é independente de psr/container ^2.0.
🧵 Workers, segurança e limitações
Não existe estado estático global. Singletons vivem enquanto o container; em workers persistentes, reusar o container também mantém singletons e proxies. Não há escopo automático por request, Fiber, coroutine ou tenant.
Identificadores, classes, factories e callables são configuração executável confiável. Não encaminhe entrada HTTP diretamente a get(), make() ou call() e não carregue definições remotas.
Ainda não existem bindings contextuais, atributos, property injection, autowiring implícito por get(), lazy ghosts, lazy transient, compilação ou escopos complexos. Consulte ROADMAP.md.
🧪 Desenvolvimento
composer install
composer test
composer analyse
composer lint
composer check
XDEBUG_MODE=coverage composer coverage
composer benchmark
php exemplo.php
O benchmark usa hrtime(true), warm-up e várias rodadas. Resultados dependem de hardware, OPcache, JIT e carga.
📄 Licença
Distribuído sob a licença MIT.