Container IoC of the Simpla Framework with implementation of Dependence Injection

v1.3.0 2018-03-22 16:58 UTC

This package is not auto-updated.

Last update: 2024-04-14 02:47:45 UTC


README

Container de Serviços

O conceito de Container de Serviços está intrissecamente relacionado com objetos e, logicamente à Programação Orientada a Objetos (POO). De fato, todo serviço pode ser um objeto mas, nem todo objeto será um serviço. Uma aplicação modernas temos diversos objetos ou conjunto de objetos, cada um com uma função específica. Assim, temos objetos que nos ajudam a enviar um e-mail, acessar e registrar informações em bancos de dedos, etc. Mas, o que é um serviço?

Serviço

A documentação do Symfony define um serviço de uma forma bastante clara:

Um Serviço é qualquer objeto PHP que realiza algum tipo de tarefa “global”. É um nome genérico proposital, usado em ciência da computação, para descrever um objeto que é criado para uma finalidade específica (por exemplo, entregar e-mails). Cada serviço é usado em qualquer parte da sua aplicação sempre que precisar da funcionalidade específica que ele fornece. Você não precisa fazer nada de especial para construir um serviço: basta escrever uma classe PHP com algum código que realiza uma tarefa específica. Parabéns, você acabou de criar um serviço!

Este conceito está presente na arquitetura orientada a serviços onde cada "serviço" pode ser acessado e utilizado facilmente, sem interferir no funcionamento de outro serviço ou da aplicação.

Service Container

Um Container de Serviço é um objeto PHP responsável pelo gerenciamento e o instanciamento de serviços (objetos ou outros elementos).

Diversos Frameworks, como Laravel, Zend, Symfony, Silex e Slim também implementam Services Containers.

O Simpla utiliza como base o Pimple, Service Container desenvolvido e mantido pela Symfony e utilizado pelo Silex, sendo que é utilizada a versão que implementa a PSR11 .

Estruturando um Projeto para Service Container

Neste exemplo iremos criar uma estrutura padrão para utilização de um service container. Essa é a mesma estrutura utilizada pelo Simpla Framework, porém vamos nos ater ao exemplo abaixo.

  • A seguir definimos em "Core" o local onde serão armazenados todos os serviços criados.

  • Em "Http/Controllers" teremos uma classe que poderá utilizar aquele serviço.

  • Em "Providers" teremos os Provedores de Serviço.

  • Em "bootstrap" teremos um inicializador automático de serviços.

      /__app
      |    |__Core
      |    |   |__ [Services.php]
      |    |__Http
      |    |   |__Controllers
      |    |       |__ [UsingServices.php]
      |    |__Providers
      |
      |__bootstrap
      |    |__providers.php
      |
      |__index.php
    

Criando um Serviço Orientado a Interface

Um serviço nada mais é que um objeto, e objeto é definido em uma classe. Porém, antes de construírmos uma classe podemos definir uma inteface para o objeto.

Neste sentido vamos definir serviço de calculadora simples conforme a seguir:

    namespace App\Classes;

    interface CalculatorInterface
    {
        public function sum($a, $b);
        public function subtract($a, $b);    
        public function multiply($a, $b);    
        public function divide($a, $b);    
    }

Interfaces são como contratos de comportamentos obrigatórios disponíveis ao ambiente externo, onde são declarados métodos e constantes públicas, além disso as interfaces desempenham papel importante no desacoplamento de código em uma aplicação orientada a objeto.

Interfaces definem quais métodos devem ser implementados de forma obrigatória pela classe que à implementar.

A utilidade desse prática é um menor acoplamento do código. Quando sua aplicação necessitar de efetuar um cálculo de soma ela não ficará refém da "CalculadoraDoFulano", podemos substituir pela "CalculadorMelhorDoCiclano" sem a necessidade de trocar em toda a aplicação a implementação da soma por que en "CalculadoraDoFulano", "sum" foi implementado como sum($a, $b = null) e "CalculadoraMelhorDoCiclano" implementou add($a, $b).

Assim, vamos implementar a Interface:

    namespace App\Classes;

    class Calculator implements CalculatorInterface
    {
        public function sum($a, $b)
        {
            return $a + $b;
        }

        public function subtract($a, $b)
        {
            return $a - $b;
        }

        public function multiply($a, $b)
        {
            return $a * $b;
        }

        public function divide($a, $b)
        {
            return $a / $b;
        }
    }

Adicionando o Serviço no Container

Uma vez definido o serviço podemos usá-lo simplimente com $calc = new Calculator(). Porém, às vezes isso pode não ser funcional do ponto de vista do gerenciamento da aplicação. Quantos objetos instanciados existem na aplicação? Devemos definir os serviços como Singleton?

Para simplificar o gerencimento dos objetos podemos fazer o uso do Container de Serviço utilizando as opções a seguir:

Make

Vamos carregar em nosso arquivo index.php o Container do Simpla e "injetar" o container do Pimple, conforme a seguir:

    use Simpla\Container\Container;
    use Xtreamwayz\Pimple\Container as Pimple;


    /* @var $app Simpla\Container\Container */
    $app = Container::instance(new Pimple);

O Container é um singleton que garante que haja uma única instancia dele em toda a aplicação, assim, temos garantido que só temos um controller em toda a aplicação.

Opcionalmente podemos definir $app como sendo um objeto global, para ser acessado em toda a aplicação:

    global $app;

Podemos adicionar um serviço com o método make:

    $app->make("calc", function(){
         return new \App\Core\Calculator();
    });

Se por algum motivo o objeto já foi definido podemos simplemente incluí-lo, ao invés de uma closure:

    $calc = new \App\Classes\Calculator();
    $app->make("calc", $calc);

    // ou diretamente

    $app->make("calc", new \App\Classes\Calculator());

Podemos adicionar um serviço também pela informação da assinatura da classe:

    $app->make("calc", \App\Classes\Calculator::class);

Se não quisermos definir um nome para o serviço, podemos simplesmente informar a assinatura da classe como único parâmetro.

    $app->make(\App\Classes\Calculator::class);

A desvantagem nesta opção é a necessidade de informar \App\Classes\Calculator::class como nome do serviço, o que pode ser um pouco cansativo.

Array

Por implementar a interface ArrayAccess o Simpla Container permite que possamos adicionar um serviço da mesmo forma que adicionamos valores em um array:

    $app['calc1'] = new \App\Classes\Calculator();

    // Ou ainda

    $app['calc2'] = function(){
        return new \App\Classes\Calculator();    
    };

Singleton

Utilizando o método singleton garantimos que apenas uma única instância (objeto) de uma classe exista.

    $app->singleton("calc", function(){
        return new Calculator();
    });

    $calc1 = $app["calc"];
    $calc2 = $app["calc"];
    $calc3 = $app["calc"];

    // $calc1 = $calc2 = $calc3

Uma característica do Pimple, que se aplica nesta situação é a adição de serviço como array.

    $app['calc'] = new \App\Classes\Calculator();

    $calc1 = $app["calc"];
    $calc2 = $app["calc"];
    $calc3 = $app["calc"];

    // $calc1 = $calc2 = $calc3

Recuperando um Serviço

Os serviços podem ser recuperados pelo método get.

    $calc = $app->get("calc");
    $calc=>sum(34,54);
    
    //Usando diretamente

    $app->get("calc")->subtract(32,12);

Tembém podemos recuperar um serviço com se este fosse um array:

    $calc = $app['calc'];
    $calc=>sum(34,54);
    

Adicionando Dados aos Container

O Container também pode conter qualquer outro tipo de informação que não seja um objeto. Isso é extremamente útil para incluir informações que possam ser utilizadas globalmente na aplicação.

    $app['tz.br.spo'] = "America/Sao_Paulo";
    $app['tz'] = new DateTimeZone($app['tz.br.spo']);

    $app->make("today", new DateTime("now", $app['tz']));

Chamada de Serviços (injeção de dependência)

O método call permite chamar um método de um serviço pré-definido com a sintaxe "service@method", passando um array com todos os parâmetros.

    $show = $app->call("today@format", ['d/m/Y H:i:s']); // 02/10/2017 21:11:25
    

Desta forma estamos injetando uma dependência em um método de um serviço.

Adicionando Interface

Podemos definir um serviço com o nome de sua interface.

    $app->make(App\Classes\CalculatorInterface::class, \App\Classes\Calculator::class);

    var_dump($app->get());

    $calcs = $app[App\Classes\CalculatorInterface::class];

Para simplificar podemos estabelecer uma tag para aquela interface, como se fosse um nome mesmo.

    $app->tagged("ICalc", App\Classes\CalculatorInterface::class);

    $app->make("ICalc", \App\Classes\Calculator::class);

    // também podemos definir como array ou singleton

    $app["ICalc"] = new \App\Classes\Calculator;
    
    $app->singleton("ICalc", \App\Classes\Calculator::class);

Adicionando Closures

Podemos adicionar closures no container como serviços.

 
    $app["sum"] = $app->closure(function ($a, $b) {
        return $a + $b;
    });

    $rand = $app["sum"];
    
    var_dump($rand(4,12)); // 16

Obtendo a função de criação de serviço

Quando você acessa um objeto, o Pimple chama automaticamente a função anônima que você definiu, o que cria o objeto de serviço para você. Se você quiser obter acesso bruto a esta função, você pode usar o método raw():

 
    $calc = $app->raw('calc');

    var_dump($calc()->sum(5,65)); // 70

Desta forma $calc obteve uma função anônima contendo a implementação do serviço calc. Ao chamarmos $calc como uma função, o serviço é criado (é criado o objeto).

Modificando Serviços após Definição

Em alguns casos você pode querer modificar uma definição de serviço depois de ter sido definido. Você pode usar o método extend() para definir código adicional para ser executado em seu serviço logo após ele é criado:

    class Car
    {
        private $placa;
        private $ano; 

        function getPlaca()
        {
            return $this->placa;
        }

        function getAno()
        {
            return $this->ano;
        } 

        function setPlaca($placa)
        {
            $this->placa = $placa;
        }

        function setAno($ano)
        {
            $this->ano = $ano;
        }  
    }

    $app['car'] = function(){
        return new Car();
    };

    $app['car'] = $app->extend('car', function($car){
        $car->setPlaca("HNT-2299");
        $car->setAno(2010);

        return $car;
    });


    var_dump($app['car']);

    /*
        object(Car)#37 (2) {
            ["placa":"Car":private]=>
                string(8) "HNT-2299"
            ["ano":"Car":private]=>
                int(2010)
        }
    */

O primeiro argumento é o nome do serviço para estender, a segunda uma função que obtém acesso à instância do objeto e do recipiente.

Service Provider

Em um tradução direta um Service provider é um Provedor de Serviço, ou seja: Provedor: O que provê algo. Serviço: Ato ou efeito de servir. Provedor de serviço: O que provê um serviço.

O Service Provider complementa o uso do Container pois, serve como um inicializador de um serviço, provendo a este todos os módulos necessários para que seja iniciado corretamente. Em um contexto de Orientação a Objetos é como se eu configurasse todos os parametros que um construtor de uma classe necessita para ser iniciada.

namespace App\Providers;
 
use Simpla\Contracts\ServiceProviderInterface;
use Simpla\Contracts\ContainerInterface;

class TodayServiceProvider implements ServiceProviderInterface
{ 
		/**
		 * Register the services 
		 *
		 * @return void
		 */	
    	public function register(ContainerInterface $serviceContainer)
    	{ 
    		$tz = new \DateTimeZone("America/Sao_Paulo");
    		
        	$serviceContainer->make("today", function(){
	        	   return  new DateTime("now");
        	});         
    	} 
}
 

O método Register

Perceba que nosso Service Provider foi criado com o nome de TodayServiceProvider e conta com o método de registro (register), onde definimos nosso serviço.

Para definir o serviço temos como parâmetro o nosso Container definido com a interface ContainerInterface, assim podemos utilizar o container e realizar o registro do serviço.

Para entender melhor, vamos adicionar em nossa estrutura um provedor:

    /__app
    |    |__Core
    |    |   |__ Calculator.php
    |    |__Http
    |    |   |__Controllers
    |    |       |__ Home.php
    |    |__Providers
    |           |__ CalculatorServiceProvider.php
    |__bootstrap
    |    |__providers.php
    |    |__bootstrap.php
    |
    |__index.php

A forma de trabalho do service provider no Simpla é muito similar a do Laravel.

Um outro exemplo de provedor de serviço pode ser visto a seguir:

// CalculatorServiceProvider.php
namespace App\Providers;
 
use Simpla\Contracts\ServiceProviderInterface;
use Simpla\Contracts\ContainerInterface;

class CalculatorServiceProvider implements ServiceProviderInterface
{ 
		/**
		 * Register the services 
		 *
		 * @return void
		 */	
    	public function register(ContainerInterface $serviceContainer)
    	{ 
        	$serviceContainer->make("calc", function(){
	        	   return  new \App\Classes\Calculator();
        	});         
    	} 
}
 

Este provider foi criado mas, só pode ser utilizado quando o adicionarmos ao nosso Container. Para isso utilizamos o método register do container.

	$app->register(new \App\Providers\CalculatorServiceProvider());
	
	$calc = $app['calc'];
	
	$calc->sum(13.5,432.3) // 445.8

Isso pode ser dispendioso se pensarmos que teremos de adicionar todos os Services Providers no container, mas veremos o quão prático torna-se essa metodologia quando utilizamos um Bootstrap.

O método Boot

Podemos ainda, adicionar um método boot no Service Provider. Este método é chamado depois que todos os serviços foram registrados, permitindo acessar todos os serviços que foram inicializados antes deste.

O método boot deve ser utilizado para inicializar um serviço. Nesta opção podemos interagir com o serviço, inicializando dependências deste e quaisquer eventos que devam ser executados antes do serviço ser criado/chamado.

// CalculatorServiceProvider.php
namespace App\Providers;
 
use Simpla\Contracts\ServiceProviderInterface;
use Simpla\Contracts\ContainerInterface;

class TestServiceProvider implements ServiceProviderInterface
{ 
		/**
		 * Bootstrap the services 
		 *
		 * @return void
		 */	
    	public function boot(ContainerInterface $app)
    	{
    		echo "Isso é uma mensagem de inicialização";
    		
    		//Imprimindo serviços disponíveis
    		print_r($app->get());
    	}
		/**
		 * Register the services 
		 *
		 * @return void
		 */	
    	public function register(ContainerInterface $serviceContainer)
    	{ 
        	// code    
    	} 
}

Bootstrap

Para que possamos criar nossos serviços de uma forma mais automatizada podemos fazer uso de um inicializador. Assim podemos utilizar uma arquivo contendo todos os providers que desejamos chamar.

    /__app
    |    |__Core
    |    |   |__ [Services.php]
    |    |__Http
    |    |   |__Controllers
    |    |       |__ [UsingServices.php]
    |    |__Providers
    |
    |__bootstrap
    |    |__providers.php
    |    |__bootstrap.php
    |
    |__index.php

Adotando a estrutura acima, podemos definir em um array no arquivo providers.php todos os serviços que desejamos chamar:

<?php

    $providers = [
    			App\Providers\CalculatorServiceProvider::class,
                       App\Providers\CarServiceProvider::class,
                       App\Providers\HelloServiceProvider::class
                   ];
                   
	 $aliases = [
	     'calc' => App\Facades\CalculatorFacade::class,
	     'hello' => App\Facades\HelloFacade::class,
	     'car' => \App\Facades\CarServiceProvider::class
	 ];

Também devemos adicionar neste arquivo os 'aliases' para cada provider, o que permite a utilização das **facades **, se existirem. Se não existir facade devemos adicionar em 'aliases' a assinatura da classe ou provider, como no exemplo acima em \App\Facades\CarServiceProvider::class.

Saiba mais sobre facades aqui.

Podemos ainda definir no arquivo bootstrap.php o nosso container:

	require __DIR__.'/../bootstrap/providers.php';

	use Simpla\Container\Container;
	use Xtreamwayz\Pimple\Container as Pimple;
	 
	global $app;

	/* @var $app Simpla\Container\Container */
	$app = Container::instance(new Pimple);

	$app->createAlias($aliases);
	$app->registerProviders($providers); 

Podemos carregar nosso arquivo bootstrap.php em uma index.php e utilizar todos os serviços disponíveis.

Opção Defer

A opção defer (deferred/adiado) faz com que o Service Provider só seja registrado no momento em que se precisa dele. Ou seja, o container somente registra o serviço quando ele é chamado.

Esta opção é extremamente útil pois, evita que serviços desnecessários sejam carregados automaticamente em uma aplicação.

Você pode obter a lista dos serviços 'deferidos' com o comando getDeferredServices() do container.

Para definir que um serviço será carregado de forma adiada devemos definir a propriedade $defer como true e retornando em um metodo provider() um array com o nome dos serviços.

class CalculatorServiceProvider implements ServiceProviderInterface
{
	    protected $defer = true;
	  
	    public function register(ContainerInterface $serviceContainer)
	    {
			$calc = new \App\Classes\Calculator();

			$serviceContainer->make("calc", $calc);
			$serviceContainer->make("calcSum",  
				$serviceContainer->closure(function ($a, $b) {
				    return $a + $b;
				})
			);
	    }

	    public static function providers()
	    {
			return ['calc','callSum'];
	    }
}

Facades

As Facades do Simpla são bem parecidas com as Facades do framework Laravel.

As Facades neste contexto permitem que tenhamos acesso aos serviços de forma simplificada, como se estes fossem classes com métodos estáticos. Isso não quer dizer que os serviços sejam estáticos de verdade, a facade cria uma nova forma de acessar um serviço, uma "fachada".

Como as facades funcionam

Para que as facades funcionem precisamos defini-las e adiciona-las em nosso arquivo de bootstrap.

<?php

    $providers = [
   			App\Providers\CalculatorServiceProvider::class,
                       App\Providers\CarServiceProvider::class,
                       App\Providers\HelloServiceProvider::class
                   ];
                   
	 $aliases = [
	     'calc' => App\Facades\CalculatorFacade::class,
	     'hello' => App\Facades\HelloFacade::class,
	     'car' => \App\Facades\CarServiceProvider::class
	 ];

a variável $aliases é responsável por armazenar o apelido para nossas facades.

Para cada serviço devemos criar um arquivo de facade que, em nossa estrutura foi adicionado em App\Facades, conforme definido no exemplo a seguir:

// CalculatorFacade.php

namespace App\Facades;                  
 
class CalculatorFacade extends Simpla\Container\Facade
{
    public static function createService()
    {
        return 'calc';
    }
}

Desta forma podemos chamar o serviço Calculator como:

	use App\Facades\CalculatorFacade;

	calc::sum(43,21); // 63

O nome definido no método createService() e no arquivo de bootstrap devem coincidir, caso contrário um NotFoundException será lançada.