O Accelero trabalha FORTE com injeção de dependência.
Usamos um container central ( SinFramework\DependencyContainer) que implementa MAIS-OU-MENOS um PSR-11 (ele não estoura NotFoundExceptionInterface).
Esse container é simples de usar e tem auto-wiring, o que facilita bastante a vida do programador.
REGISTRO DE DEPENDÊNCIAS
Durante o bootstrap do sistema são registradas as dependências necessárias. Todos os arquivos existentes em private/config/injections são registrados, com a proteção de "subdiretórios-são-carregados-apenas-se-houver-um-módulo-ativo-com-o-mesmo-nome".
Se a mesma dependência for registrada duas vezes, vale a última.
Todos os arquivos devem retornar um array key/value, onde a chave é o "nome da coisa registrada" e o valor é "a implementação".
É possível (e MUITO recomendado) fazer lazy-load das dependências retornando Closures durante o registro (exemplos mais abaixo).
É possível (mas tome cuidado) registrar singletons. Detalhes mais abaixo.
ESTRUTURA BÁSICA DO REGISTRO
Alguns exemplos de registro de dependências:
//private/config/injections/my_injections.php
return [
MyInterface::class => fn() : MyInterface => new MyConcreteImplementation(),
'magicNumber' => 7,
];
BUSCANDO UMA DEPENDÊNCIA
Se for necessário buscar uma dependência qualquer no meio do código (basicamente um Service Locator), basta fazer assim:
$injection = Accelero::getInjection(MyInterface::class); //MyConcreteImplementation
Se o que se está pedindo não estiver registrado no container, o retorno é sempre NULL:
$injection = Accelero::getInjection('coisaNaoRegistrada'); //null
AUTO-WIRING
O container é especialmente útil pra construir automaticamente objetos complexos com injeção de dependência no construtor. Exemplo:
//private/config/injections/my_injections.php
return [
LoggerInterface::class => fn() => new Logger(.......),
];
//MyClass.php
class MyClass {
public function __construct(private readonly LoggerInterface $logger) {}
}
//teste.php
$injection = Accelero::getInjection(MyClass::class); //funciona mesmo sem jamais termos registrado MyClass
Quando se "pede" um objeto MyClass pro container, ele verifica automaticamente que o construtor exige um LoggerInterface. Daí o container busca um LoggerInterface no registro... e se o construtor do LoggerInterface pedisse alguma coisa, ele ia atrás dessa "alguma coisa", recursivamente, até conseguir resolver todas as dependências.
Se alguma coisa não for resolvível automaticamente, retorna-se NULL.
REGISTRO RECURSIVO DE DEPENDÊNCIAS
Em geral, é boa prática nunca usar new nos registros de dependência de interfaces, mas sempre "pedir a implementação que se deseja pro container". Isso facilita futuras refatorações e evita que você tenha que se preocupar com a construção do objeto na maior parte dos casos. Exemplo:
//private/config/injections/my_injections.php
return [
MyInterface::class => fn() : MyInterface => new MyConcreteImplementation(), //RUIM
MyInterface::class => fn() : MyInterface => Accelero::getInjection(MyConcreteImplementation::class), //BOM
];
LAZY-LOADING
Quando o retorno de algo registrado no container é uma Closure, essa Closure é executada automaticamente quando se pede aquilo pro container (e somente quando aquilo é solicitado). Exemplo:
//private/config/injections/my_injections.php
class Teste {
public static function print() : void {
echo 'Hello';
}
}
return [
'mykey' => Teste::print(), //VAI EXECUTAR O MÉTODO IMEDIATAMENTE NA HORA DO REGISTRO, MESMO SEM NINGUÉM "PEDIR"
'mykey2' => fn() => Teste::print(), //SÓ VAI EXECUTAR QUANDO ALGUÉM "PEDIR" ESSE mykey2
];
Em geral, é melhor SEMPRE retornar Closures ao registrar dependências, pois isso garante que tudo será lazy-loaded (menor uso de memória, mais rápido).
CACHE
Você pode configurar certas classes para usarem "cache no injetor de dependência". Às vezes "resolver a dependência" é caro, e é uma boa ideia guardar o resultado em cache por um tempo pra evitar ter que resolver novamente na sequência. Pra isso, basta incluir na definição da própria classe um attribute passando o TTL desejado em segundos:
#[Cacheable(ttl: 600)]
class MyClass {}
Como essa definição acontece na própria classe, NÃO É POSSÍVEL usar esse recurso para classes externas (pois não devemos mexer no source alheio). Nada impede de implementar algo manualmente durante o registro de dependência, no entanto:
//private/config/injections/my_injections.php
return [
MyClass::class => function() : MyClass {
$cache = Accelero::getInjection(CacheInterface::class);
$object = $cache->get(MyClass::class);
if (!$object) {
$object = new MyClass();
$cache->set(MyClass::class, $object, ttl: 600);
}
return $object;
}
];
SINGLETONS
Se você não fizer nada especial, todas as vezes que pedir um objeto para o container receberá uma nova instância:
class MyClass {}
var_dump(Accelero::getInjection(MyClass::class) === Accelero::getInjection(MyClass::class)); //false
Mas você pode dizer pro container que certas coisas devem ser registradas como singleton (uma única instância compartilhada). Existem duas formas de fazer isso.
Direto na definição da classe
#[Singleton]
class MyClass {}
var_dump(Accelero::getInjection(MyClass::class) === Accelero::getInjection(MyClass::class)); //true
Lista de singletons no próprio injetor de dependência
A classe Singleton injetada no container é um array de classes que devem ser tratadas como singleton:
//private/config/injections/singletons.php
return [
Singleton::class => [
MyClass::class,
CacheInterface::class,
...
]
];
var_dump(Accelero::getInjection(MyClass::class) === Accelero::getInjection(MyClass::class)); //true
DEV
É muito útil fazer um bypass de certas dependências durante o desenvolvimento. Cache, banco de dados, etc...
Como existe a regra "dependências registradas com o mesmo nome, vale a última", é possível registrar coisas "por cima do padrão" sem ter que mexer nos arquivos "originais".
As dependências são registradas na seguinte ordem (e a última sempre é "que vale"):
private/config/injections
plugins/**/injections
client/config/injections
dev/injections
A lógica/justificativa é a seguinte:
- primeiro se carregam as injeções padrão
- depois se carregam as injeções padrão DOS PLUGINS HABILITADOS
- depois se carregam as injeções "do cliente" (customizações)
- depois se carregam as injeções "do desenvolvedor" (só deve existir em ambiente dev ou em emergências)