Injeção de dependências


1Visão geral

1.1Princípio

A injeção de dependências permite desacoplar a lógica entre objetos, com base no princípio de inversão de controle.

O Temma cria automaticamente um componente (chamado de “loader”) para gerenciar a injeção de dependências dos objetos de negócio. Esse componente é a espinha dorsal na qual todos os objetos da sua aplicação podem se apoiar para acessar uns aos outros.
Ele está disponível nos controladores através do atributo privado $_loader.

O loader pode instanciar automaticamente os objetos solicitados a ele, mas também pode conter valores (escalares ou objetos) que lhe são fornecidos explicitamente.

O comportamento padrão do loader é sempre fornecer a mesma instância para um determinado tipo de objeto.
Quando um objeto é solicitado ao loader:

  • Se o loader ainda não conhece esse objeto, ele o instancia, armazena a instância em seu cache e a retorna.
  • Se o loader já conhece esse objeto (seja porque ele foi fornecido, seja porque o loader já o instanciou), o loader retorna a instância.

1.2Uso

O loader pode ser usado de duas formas diferentes: como um service locator, ou por meio de autowiring.

Service locator

Este é o uso típico em controladores. O loader é usado para solicitar os objetos necessários. Nos seus controladores, você usa o loader para obter as instâncias dos objetos de que precisa.

Exemplo de uso:

// em um controlador => service locator
class MyController extends \Temma\Web\Controller {
    public function __invoke() {
        // usando um objeto de negócio por meio do loader
        $this['data'] = $this->_loader->MyObject->process();
    }
}
  • Linha 5: O loader é usado para obter uma instância do objeto MyObject, no qual o método process() é chamado. Se o loader já tiver essa instância em cache, ele a retorna; caso contrário, ele instancia o objeto antes de retorná-lo.
Autowiring

Seus objetos (exceto os controladores) recebem suas dependências como parâmetros de seus construtores. Se necessário, o loader vai instanciar os objetos esperados antes de fornecê-los ao construtor.

// em um objeto de negócio => autowiring
class MyObject {
    // Injeção de dependências via autowiring.
    // O loader instancia primeiro os objetos que devem ser passados ao construtor.
    public function __construct(
        private UserDao $userDao,
        private BucketService $bucketService
    ) {
    }

    // usando as dependências em outros métodos
    public function process() : array {
        $users = $this->userDao->getList();
        $buckets = $this->bucketService->getFromUsers($users);
        return $buckets;
    }
}
  • Linhas 5 a 8: O construtor do objeto espera dois parâmetros, dos tipos UserDao e BucketService. Quando o objeto MyObject é instanciado automaticamente pelo loader, ele fornece ao construtor os parâmetros esperados, instanciando-os primeiro se necessário.
    Aqui, a promoção de construtor é usada para armazenar esses parâmetros em propriedades privadas do objeto.
  • Linhas 13 e 14: As propriedades privadas são usadas.

2Dados disponíveis

Por padrão, o componente contém os seguintes elementos:

As conexões com fontes de dados são acessíveis de duas formas diferentes:

  • $loader->dataSources é um Registro que contém os objetos de conexão (veja a documentação de controladores).
    Por exemplo, uma conexão MySQL chamada db ficará acessível usando $loader->dataSources->db.
  • Se o nome da fonte de dados ainda não estiver presente no componente (como session, config etc.), ela fica diretamente acessível a partir do componente.
    Por exemplo, uma conexão MySQL chamada db ficará acessível usando $loader->db.

3Acesso alternativo

No exemplo anterior, o loader (no modo service locator) foi usado partindo do princípio de que os objetos gerenciados pelo componente estão no namespace raiz (ou seja, não estão em um namespace explícito), e que seus arquivos de origem estão no diretório lib/ do projeto.

Mas às vezes você vai querer usar objetos que estão em namespaces mais profundos. Nesse caso, os objetos devem ser acessados usando uma sintaxe de array associativo, em vez de uma sintaxe orientada a objetos.

Por exemplo, se você quiser usar o método add() do objeto \Math\Base\Calculator, deve escrever:

$res = $this->_loader['\Math\Base\Calculator']->add(3, 4);

Também é possível usar o método get():

$res = $this->_loader->get('\Math\Base\Calculator')->add(3, 4);

Mais adiante você verá como simplificar essa sintaxe usando aliases e prefixos.


4Armazenamento

Os dados armazenados no loader são identificados por uma chave (na maioria das vezes, o tipo do objeto). Se um caractere de barra invertida (\) estiver presente no início da chave, ele é removido.

Esse processamento existe para que as três notações a seguir sejam equivalentes:

$res = $loader['\Math\Base\Calculator']->add(3, 4);
$res = $loader['Math\Base\Calculator']->add(3, 4);
$res = $loader[\Math\Base\Calculator::class]->add(3, 4);

Caso especial: o próprio loader satisfaz qualquer requisição para sua própria classe, ou para uma de suas classes-pai (até \Temma\Base\Loader). O atalho TµLoader é aceito como sinônimo de \Temma\Base\Loader.
As notações a seguir, portanto, retornam todas a própria instância do loader:

$l = $loader['\Temma\Base\Loader'];
$l = $loader['Temma\Base\Loader'];
$l = $loader[\Temma\Base\Loader::class];
$l = $loader['TµLoader'];

5Configuração do loader

5.1Pré-carregamento

É possível configurar o loader para fornecer a ele valores que ele não precisará instanciar sozinho (ou que ele não conseguiria instanciar).

Assim, no arquivo etc/temma.php, você pode definir valores fixos que ficarão acessíveis em toda a aplicação:

<?php

return [
    'x-loader' => [
        'preload' => [
            // adiciona uma instância de ZipArchive
            'zipManager' => new ZipArchive(),

            // adiciona uma string obtida do ambiente
            'appPassword' => getenv('APP_PASSWORD'),

            // adiciona um número que ficará acessível globalmente
            'maxArticles' => 100,
        ]
    ]
];

Esses valores poderão ser usados diretamente pelo loader.
Assim, será possível escrever:

print("Número máximo de artigos: " . $this->_loader->maxArticles);

Um valor registrado dessa forma também poderá ser usado pelo loader no contexto de autowiring.

Você encontrará mais informações sobre a adição explícita de dados em a seção dedicada.


5.2Aliases

É possível definir aliases de nomenclatura, que serão usados quando um objeto for chamado através do loader.

Por exemplo, com o seguinte arquivo etc/temma.php:

<?php

return [
    'x-loader' => [
        'aliases' => [
            'UserService' => '\MyApp\User\UserService',
            'TµEmail'     => '\Temma\Utils\Email',
        ]
    ]
];

Você poderá escrever o seguinte código:

$list = $this->_loader->UserService->getList();
// é equivalente a
$list = $this->_loader['\MyApp\User\UserService']->getList();

$this->_loader->TµEmail->textMail($from, $to, $title, $message);
// é equivalente a
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);

Você encontrará mais informações sobre aliases em a seção dedicada.


5.3Prefixos

Também é possível definir prefixos de nomenclatura, que resumem namespaces (ou partes de namespaces). Esses prefixos podem então ser usados no início do nome de um objeto gerenciado pelo loader.

Exemplo de um arquivo etc/temma.php:

<?php

return [
    'x-loader' => [
        'prefixes' => [
            'global' => '\MyApp',
            'App'    => '\OtherApp\Extension\OtherApp',
            '€x'     => '\Europa\Source\Service',
        ]
    ]
];

Você poderá então escrever o seguinte código:

$object = $this->_loader->globalArticles;
// equivalente a
$object = $this->_loader['\MyApp\Articles'];

$object = $this->_loader->AppWidget;
// equivalente a
$object = $this->_loader['\OtherApp\Extension\OtherApp\Widget'];

$object = $this->_loader->€xUser;
// equivalente a
$object = $this->_loader['\Europa\Source\Service\User'];

Você encontrará mais informações sobre prefixos na seção dedicada, mais abaixo.


6Autowiring: desacoplamento

6.1Princípio do autowiring

Autowiring é a capacidade do loader de detectar as dependências que um objeto espera em seu construtor.

Assim, qualquer objeto pode ser chamado através do loader. Na primeira chamada, o loader vai instanciar o objeto, passando suas dependências como parâmetros. Se o loader ainda não contiver instâncias dessas dependências, ele as criará na hora.

Aqui está um exemplo de dois objetos, sendo um dependência do outro:

/**
 * Objeto usado para calcular hashes a partir de identificadores.
 */
class Hasher {
    /**
     * Método que retorna um hash a partir de uma string.
     * @param string  $text  Texto de entrada.
     * @return string  Hash calculado.
     */
    public function hash(string $text) : string {
        return hash('sha256', $text);
    }
}

/**
 * Objeto que gerencia os usuários no banco de dados.
 */
class UserDao {
    /**
     * Construtor.
     * @param \Temma\Datasources\Redis  $ndb     Conexão com o banco de dados.
     * @param Hasher                    $hasher  Objeto de hash.
     */
    public function __construct(
        private \Temma\Datasources\Redis $ndb,
        private Hasher $hasher,
    ) {
    }

    /**
     * Retorna as informações de um usuário.
     * @param  int  $id  Identificador do usuário.
     * @return array  Array associativo.
     */
    public function get(int $id) : array {
        $user = $this->ndb["user-$id"];
        $user['hash'] = $this->hasher->hash($user['email']);
        return $user;
    }
}
  • Linhas 4 a 13: Objeto utilitário Hasher, que não tem dependências.
  • Linhas 18 a 40: Objeto UserDao.
    • Linhas 24 a 28: Construtor, que espera duas dependências.
    • Linha 36: Uso da conexão com o banco de dados Redis.
    • Linha 37: Uso do objeto Hasher.

E aqui está o código do controlador que chama o objeto UserDao:

class Account extends \Temma\Web\Controller {
    public function show(int $id) {
        $this['user'] = $this->_loader->UserDao->get($id);
    }
}
  • Linha 3: O loader é usado para chamar o objeto UserDao. Uma instância é criada na hora e, para criá-la, o loader usa a conexão Redis já existente e cria uma instância do objeto Hasher.

6.2Gerenciamento de dependências

Quando o loader instancia automaticamente um objeto, ele percorre os parâmetros esperados pelo construtor.

Para um determinado parâmetro (por exemplo, \App\UserManager $userManager), há três casos possíveis:

  • Se o loader já contiver uma entrada com o nome do tipo (por exemplo, \App\UserManager), ela é usada.
  • Se houver uma entrada com o nome do parâmetro (por exemplo, $userManager), ela é usada.
  • Se o tipo do parâmetro (por exemplo, \App\UserManager) for instanciável, o loader cria uma instância e a usa.

Uma dependência não precisa necessariamente ser um objeto instanciável. Por exemplo, se o loader contiver uma string name, e um construtor tiver um parâmetro string $name, o loader usará esse valor.

Parâmetros tipados como \Temma\Base\Loader (ou com a classe do loader atual, ou uma de suas classes-pai) são um caso especial:

  • O loader atual é sempre injetado. Assim, qualquer objeto pode receber o componente de injeção de dependências, bastando declará-lo em seu construtor.
  • O loader nunca cria uma nova instância de loader. Se o parâmetro for tipado com uma classe de loader que não corresponda ao loader atual, uma exceção \Temma\Exceptions\Loader é lançada (ou o valor null é injetado, se o parâmetro for anulável).

7Service locator: otimização de desempenho

Embora o autowiring seja muito simples e prático de usar, ele tem a desvantagem de ser custoso em termos de desempenho.

Para uma aplicação em que o desempenho é crítico, você pode preferir a abordagem service locator.
Nesse caso, um objeto não receberá suas dependências como parâmetros do construtor. Em vez disso, ele receberá o loader, que usará diretamente para acessar os objetos de que precisa.

Para isso, ele deve implementar a interface \Temma\Base\Loadable, que exige que seu construtor receba apenas um único parâmetro, do tipo \Temma\Base\Loader.

Observe que implementar essa interface não é estritamente necessário para receber o loader: como visto acima, o autowiring injeta o loader atual em qualquer parâmetro de construtor tipado como \Temma\Base\Loader. A vantagem da interface \Temma\Base\Loadable é que ela evita o uso de reflexão: o loader instancia diretamente o objeto, passando a ele sua própria instância, o que é melhor em termos de desempenho.

Aqui está um exemplo de um objeto que usa o loader para acessar suas dependências:

class SpecialLogger implements \Temma\Base\Loadable {
    /** Construtor. */
    public function __construct(private \Temma\Base\Loader $loader) {
    }

    /**
     * Método que adiciona linhas ao arquivo 'var/list.txt'.
     * @param string  $text  Texto a ser escrito.
     */
    public function write(string $text) : void {
        $dest = $this->loader->config->varPath . '/list.txt';
        file_put_contents($dest, "$text\n", FILE_APPEND);
    }

    /**
     * Método que usa o objeto OtherObject.
     */
    public function process() : void {
        $value = $this->loader->OtherObject->doCalculation();
        $this->write($value);
    }
}
  • Linha 1: O objeto implementa a interface \Temma\Base\Loadable.
  • Linha 3: Construtor do objeto, que recebe como parâmetro uma instância do componente de injeção de dependências. Ela é armazenada como propriedade privada.
  • Linha 11: No método write(), $this->loader->config é usado para acessar o objeto de configuração (e sua propriedade varPath).
  • Linha 19: No método process(), $this->loader->OtherObject é chamado. Se o loader já tinha uma instância de OtherObject, ela é usada; caso contrário, uma nova instância é criada e retornada.
    OtherObject pode usar autowiring ou implementar a interface \Temma\Base\Loadable.

Para que esse objeto seja carregável pelo loader, ele deve estar acessível através dos caminhos de inclusão do projeto. Por padrão, isso significa que ele deve ser registrado em um arquivo chamado SpecialLogger.php, colocado no diretório lib/ do projeto.

A partir desse momento, o objeto fica disponível diretamente, como se fosse uma propriedade do loader. Apenas uma instância do objeto será criada (na primeira chamada).

Aqui está um exemplo de controlador que usa o objeto SpecialLogger através do loader:

class Homepage extends \Temma\Web\Controller {
    /** Ação raiz. */
    public function __invoke() {
        // escreve no arquivo
        $this->_loader->SpecialLogger->write('Funciona');
    }
}
  • Linha 5: O loader é usado para acessar o objeto SpecialLogger e, em seguida, seu método write().

Agora imagine que criamos outro objeto de negócio, carregável através do loader, que usa o objeto SpecialLogger.

class Calculator implements \Temma\Base\Loadable {
    /** Construtor. */
    public function __construct(private \Temma\Base\Loader $loader) {
    }

    /**
     * Função que realiza uma adição.
     * @param int  $i  Primeiro operando.
     * @param int  $j  Segundo operando.
     * @return int  Resultado da adição.
     */
    public function add(int $i, int $j) : int {
        $result = $i + $j;
        $this->loader->SpecialLogger->write("Calculado: $result");
        return ($result);
    }
}
  • Linha 3: Construtor do objeto, que armazena o loader como propriedade privada.
  • Linha 14: O loader é usado para chamar o objeto SpecialLogger.

Isso ilustra como os objetos de negócio podem se chamar uns aos outros, com o componente gerenciando o acesso e a instanciação.


8Adicionando dados ao loader

8.1Adições explícitas

Você pode criar manualmente um objeto e adicioná-lo ao loader, especificando o nome que deseja dar a ele:

// cria a instância do objeto
$calculator = new \Math\Base\Calculator($this->_loader);

// adiciona a instância ao componente
// (as três sintaxes são equivalentes)

// - sintaxe orientada a objetos
$this->_loader->calculator = $calculator;

// - sintaxe de array associativo
$this->_loader['calculator'] = $calculator;

// - usando o método set()
$this->_loader->set('calculator', $calculator);

Depois, é possível recuperar esse objeto a partir do loader (usado como service locator):

// agora que a instância está registrada no componente,
// ela pode ser usada em qualquer lugar onde o componente esteja acessível
$res = $this->_loader->calculator->add(3, 4);

Isso também funciona com autowiring (que aqui se baseia no nome calculator em vez do tipo):

class MyObject {
    // construtor: recebe a dependência previamente adicionada ao loader
    public function __construct(private \Math\Base\Calculator $calculator) {
    }

    public function process(int $i, int $j) : int {
        // usa o objeto privado
        return $this->calculator->add($i, $j);
    }
}

Você pode adicionar qualquer tipo de elemento ao loader (não apenas objetos):

$this->_loader->anInteger = 3;
$this->_loader->anArray = ['a', 'b', 'c'];
$this->_loader->anObject = new \Toto();

8.2Adições por callback

Também é possível atribuir uma função anônima. Essa função será executada na primeira vez que o loader for acessado; o valor que ela retornar será então armazenado pelo loader, e a função anônima nunca mais será chamada.

A função anônima recebe a instância do loader como parâmetro. Ela deve retornar o valor que substituirá a função anônima no loader, e que será retornado pelo loader em cada chamada subsequente para a mesma chave.

É possível passar o nome de uma função, uma função anônima, uma string de chamada estática (por exemplo, 'MyObject::myMethod'), ou um array de callback em um objeto instanciado (por exemplo, [$object, 'myMethod']).

Aqui está um exemplo de controlador:

class Homepage extends \Temma\Web\Controller {
    // inicialização
    public function __wakeup() {
        // adiciona a chave 'calc' associada a uma função anônima
        $this->_loader->calc = function($loader) {
            if ($this['param'] == 'base')
                return (new \Math\Base\Calculator($loader));
            return (new \Math\Other\Calculator($loader));
        };
    }

    // ação
    public function compute(string $type) {
        $this['param'] = $type;

        // a função anônima é executada para gerar a chave 'calc'
        $this['res'] = $this->_loader->calc->add(3, 4);

        // o valor de 'calc' é recuperado diretamente
        $this['zzz'] = $this->_loader->calc->add(5, 6);
    }

    // outra ação
    public function compute2() {
        $this['res'] = $this->_loader->calc->add(3, 4);
    }
}
  • Linha 3: O método __wakeup() é usado para inicializar o controlador.
  • Linha 5: A chave calc é criada no loader, e recebe uma função anônima.
  • Linhas 6 a 8: A variável $this se refere ao próprio controlador. A variável de template param é usada para determinar qual implementação será retornada.
  • Linha 14: A variável de template param recebe o valor recebido no parâmetro $type.
  • Linha 17: O loader é usado sem se preocupar com qual implementação é selecionada.
  • Linha 20: O loader é usado para recuperar a mesma instância da chamada anterior.
  • Linha 25: O loader é usado. Aqui, o objeto \Math\Other\Calculator sempre será usado.

8.3Adições por callback dinâmico

Com as adições por callback (vistas na seção anterior), a função anônima é executada apenas uma vez, e o valor que ela retorna é armazenado pelo loader para substituir a função anônima.

Mas, às vezes, você quer que a função anônima seja executada dinamicamente toda vez que o loader for acessado, sem que o valor retornado seja armazenado.
Nesse caso, você deve usar o método dynamic() do loader, fornecendo a ele a chave e uma função anônima:

// adiciona um valor dinâmico ao loader
$loader->dynamic('randomValue', function() {
    return mt_rand(0, 255);
});

// exibe o valor várias vezes; ele será gerado novamente a cada vez
print($loader->randomValue . "\n");
print($loader->randomValue . "\n");
print($loader->randomValue . "\n");
print($loader->randomValue . "\n");

8.4Adições por builder

Um builder é uma função responsável por gerenciar as instanciações, registrada usando o método setBuilder() do loader.
Essa função recebe dois parâmetros: o primeiro é a instância do loader; o segundo é o nome do objeto que está sendo acessado.

Aqui está um exemplo:

// definição do builder
$this->_loader->setBuilder(function($loader, $key) {
    // verifica se o nome do objeto solicitado termina
    // com "Service" ou com "Dao"
    if (str_ends_with($key, 'Service')) {
        $className = '\MyApp\Service\\' . substr($key, 0, -7);
        return new $className($loader);
    } else if (str_ends_with($key, 'Dao')) {
        $className = '\MyApp\Dao\\' . substr($key, 0, -3);
        return new $className($loader);
    }
});

// esta chamada vai usar o objeto \MyApp\Service\Mail
$this->_loader->MailService->send();

// esta chamada vai usar o objeto \MyApp\Dao\User
$this->_loader->UserDao->remove($userId);

8.5Adições de DAO

Os objetos DAO podem ser instanciados em controladores usando seu método _loadDao(). Fora dos controladores, o componente de injeção de dependências pode ser usado para criar instâncias de objetos DAO que você desenvolveu.

Por exemplo, se você criou um objeto UserDao (no arquivo lib/UserDao.php), você pode usá-lo desta forma:

$user = $this->_loader->UserDao->getFromEmail($email);

9Gerenciamento de aliases

9.1Definição de alias

Como visto acima, é possível definir aliases de nomenclatura no arquivo de configuração.

Você também pode declarar aliases diretamente no loader usando o método alias():

// definição de um alias
$this->_loader->alias('UserService', '\MyApp\User\UserService');

// uso do alias
$list = $this->_loader->UserService->getList();
// é equivalente a
$list = $this->_loader['\MyApp\User\UserService']->getList();

Você também pode declarar vários aliases passando um array associativo:

// definição de um array de aliases
$this->_loader->alias([
    'UserService' => '\MyApp\User\UserService',
    'TµEmail'     => '\Temma\Utils\Email',
]);

// uso do alias
$list = $this->_loader->UserService->getList();
$this->_loader->TµEmail->textMail($from, $to, $title, $message);

// é equivalente a
$list = $this->_loader['\MyApp\User\UserService']->getList();
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);

É possível remover um alias definido anteriormente fornecendo o valor null para o mesmo nome de alias no método alias().


9.2Uso de aliases para testes

Ao executar testes automatizados em uma aplicação Temma, você pode precisar substituir um objeto por um mock, um “objeto stub” que simula o comportamento do objeto original.

Com os aliases de nomenclatura, fica muito fácil criar um arquivo etc/temma.test.php (se o ambiente de execução se chamar test), que redefine os objetos carregados para um determinado nome.

Exemplo de um arquivo etc/temma.php:

<?php

return [
    'x-loader' => [
        'aliases' => [
            'UserService' => '\MyApp\User\UserService'
        ]
    ]
];

Exemplo de um arquivo etc/temma.test.php:

<?php

return [
    'x-loader' => [
        'aliases' => [
            'UserService'        => '\MyMock\UserService',
            '\Temma\Utils\Email' => '\MyMock\Email',
        ]
    ]
];

Seu código de aplicação pode conter:

// esta linha de código:
$list = $this->_loader->UserService->getList();
// em condições normais, é equivalente a:
$list = $this->_loader['\MyApp\User\UserService']->getList();
// no ambiente de teste, é equivalente a:
$list = $this->_loader['\MyMock\UserService']->getList();

// esta linha de código:
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
// no ambiente de teste, é equivalente a:
$this->_loader['\MyMock\Email']->textMail($from, $to, $title, $message);

10Gerenciamento de prefixos

10.1Visão geral dos prefixos

Além dos aliases, também é possível definir prefixos de nomenclatura, que resumem namespaces. Esses prefixos podem então ser usados no início do nome de um objeto gerenciado pelo loader.

Atenção: Evite definir muitos prefixos, pois todos eles são verificados toda vez que um objeto é solicitado pela primeira vez. Isso pode ter impacto no desempenho.


10.2Definição de prefixo

Você pode declarar um prefixo usando o método prefix():

// definição de um prefixo
$this->_loader->prefix('global', '\MyApp');

// uso do prefixo
$object = $this->_loader->globalArticles;
// é equivalente a
$object = $this->_loader['\MyApp\Articles'];

// segundo prefixo
$this->_loader->prefix('App', '\OtherApp\Extension\OtherApp');

// uso do prefixo
$object = $this->_loader->AppWidget;
// é equivalente a
$object = $this->_loader['\OtherApp\Extension\OtherApp\Widget'];

// terceiro prefixo
$this->_loader->prefix('€x', '\Europa\Source\Service');

// uso do prefixo
$object = $this->_loader->€xUser;
// é equivalente a
$object = $this->_loader['\Europa\Source\Service\User'];

Você também pode declarar vários prefixos passando um array associativo:

// definição de um array de prefixos
$this->_loader->prefix([
    'global' => '\MyApp',
    'App'    => '\OtherApp\Extension\OtherApp',
    '€x'     => '\Europa\Source\Service',
]);

É possível remover um prefixo definido anteriormente fornecendo o valor null para o mesmo prefixo.


10.3Gerenciamento de prefixos com callbacks

Ao definir um prefixo (seja pelo método prefix() ou no arquivo de configuração), é possível atribuir a ele um valor do tipo callable. Nesse caso, quando o elemento é solicitado ao loader, a função referenciada é executada (recebendo como parâmetros a instância do loader e o nome do objeto solicitado sem seu prefixo), e seu valor de retorno é usado como o valor associado ao nome solicitado.

Exemplo de código usando uma função:

// definição de uma função que retorna um objeto de acordo com a configuração
function sinclairManager(\Temma\Base\Loader $loader, string $name) {
    if ($name == 'Spectrum')
        return new \Sinclair\Computers\ZxSpectrum();
    if ($name == '81')
        return new \Sinclair\Computers\Zx81();
    if ($name == 'QL')
        return new \Sinclair\Computers\QuantumLeap();
    return (null);
}

// registro do prefixo
$this->_loader->setPrefix('Zx', 'sinclairManager');

// uso
$computer = $this->_loader->ZxSpectrum;
// é equivalente a
$computer = $this->_loader['\Sinclair\Computers\ZxSpectrum'];

// outro uso
$computer = $this->_loader->Zx81;
// é equivalente a
$computer = $this->_loader['\Sinclair\Computers\Zx81'];

// outro uso
$computer = $this->_loader->ZxQL;
// é equivalente a
$computer = $this->_loader['\Sinclair\Computers\QuantumLeap'];

É possível passar o nome de uma função, uma função anônima, uma string de chamada estática (por exemplo, 'MyObject::myMethod'), ou um array de callback em um objeto instanciado (por exemplo, [$object, 'myMethod']).


11Substituição do loader via configuração

Em vez de chamar explicitamente o método setBuilder(), é possível especificar um objeto loader no arquivo de configuração etc/temma.php (veja a documentação de configuração). Esse objeto deve estender a classe \Temma\Base\Loader e conter um método protegido builder(). Esse método deve receber como parâmetro o nome do objeto a ser instanciado, e retornar uma instância dele.

Aqui está um exemplo. Primeiro, a configuração do Temma no arquivo etc/temma.php:

<?php

return [
    'application' => [
        'loader' => 'MyLoader'
    ]
];

Depois, no arquivo lib/MyLoader.php:

class MyLoader extends \Temma\Base\Loader {
    protected function builder(string $key) {
        if ($key == 'User')
            return new \MyApp\Dao\User($this);
        else if ($key == 'HttpClient')
            return new \Utils\Http\Client($this);
        throw new Exception("Unknown object '$key'.");
    }
}

Assim, torna-se possível escrever:

// usa \MyApp\Dao\User
$user = $this->_loader->User->get($userId);

// usa \Utils\Http\Client
$this->_loader->HttpClient->post($url, $data);

Observe que um loader personalizado também satisfaz requisições para sua própria classe ou para a classe \Temma\Base\Loader, retornando sua própria instância. Isso também funciona com autowiring: um objeto pode tipar um de seus parâmetros de construtor como MyLoader para receber o loader atual, corretamente tipado.