zf2: Introdução a Services e a ServiceManager

Este artigo é uma tradução mal feita do artigo Introducing Services and the ServiceManager da documentação do Zend Framework 2.4 feita por mim mesmo (Vasconcelos) para minhas consultas futuras, é claro que estou deixando disponível para qualquer pessoal que necessitar.

No capítulo anterior, aprendemos como criar um aplicativo simples “Hello World” no Zend Framework 2. Foi bom para o começo e fácil de entender, mas a aplicação em si não faz nada. Neste capítulo irá lhe apresentar o conceito de Services com a introdução de Zend\ServiceManager\ServiceManager.

O que é um Service?

Um Service é um objeto que executa a lógica complexa da aplicação. Isto é parte da aplicação que junta todas as pontas de fios soltas juntas e lhe dá um resultado fácil de entender.

O que nós estamos tentando fazer com o nosso Blog
Isto significa que precisamos ter um Service que irá nos dar os dados que nós precisamos. O Service vai pegar os dados em alguma fonte e quando escrevemos o Service nós não damos real atenção a esta fonte. O Service será escrito de acordo uma Interface que vamos definir e que no futuro temos que implementar para providenciar os dados.

Escrevendo o PostService?

Para escrever um Service uma boa prática é definir uma Interface primeiro.

Interfaces são uma boa maneira de garantir que outros programadores podem facilmente construir extensões para os nossos Services usando suas próprias implementações. Em outras palavras, eles podem escrever Services que têm os mesmos nomes, mas internamente fazer coisas completamente diferentes, tendo algum resultado especificado.

No nosso caso, precisamos criar um Postservice. Isso significa que primeiro vamos definir uma PostServiceInterface. A tarefa do nosso Service é fornecer os dados de nossos posts. Por hora vamos focar-nos somente na leitura das coisas. Vamos definir uma function que nos dará todas as mensagens e vamos definir uma function que nos dará um único post.

Vamos começar criando a Interface em /module/Blog/src/Blog/Service/PostServiceInterface.php


<?php
// Filename: /module/Blog/src/Blog/Service/PostServiceInterface.php
namespace Blog\Service;

use Blog\Model\PostInterface;

interface PostServiceInterface
{
    /**
    * Should return a set of all blog posts that we can iterate over. Single entries of the array are supposed to be
    * implementing \Blog\Model\PostInterface
    *
    * @return array|PostInterface[]
    */
    public function findAllPosts();

    /**
    * Should return a single blog post
    *
    * @param  int $id Identifier of the Post that should be returned
    * @return PostInterface
    */
    public function findPost($id);
}

Como você pode ver nós definimos duas funções. Supõe-se que a primeira findAllPosts() deve retornar todas as mensagens e a segunda findPost($id) que deve retornar o post correspondente ao identificador $id dado. O que há de novo aqui é o fato de que nós definimos um valor de retorno que não existe ainda. Nós fazemos a suposição de que os valores de retorno em geral são do tipo Blog\Model\PostInterface. Nós iremos definir esta classe em um momento posterior, agora primeiro vamos simplesmente criar a PostService.

Crie a classe PostService em /module/Blog/src/Blog/Service/PostService.php, certifique-se de implementar o PostServiceInterface e as funções requeridas (nós vamos preencher estas funções mais tarde). Em seguida você deve ter uma classe parecida com a seguinte


<?php
// Filename: /module/Blog/src/Blog/Service/PostService.php
namespace Blog\Service;

class PostService implements PostServiceInterface
{
    /**
    * {@inheritDoc}
    */
    public function findAllPosts()
    {
        // TODO: Implement findAllPosts() method.
    }

    /**
    * {@inheritDoc}
    */
    public function findPost($id)
    {
        // TODO: Implement findPost() method.
    }
}

Escrevendo os arquivos modelos requeridos

Dado que a PostService irá retornar Models, devemos criá-los, também.
Certifique-se de escrever uma Interface para o primeiro Model! Vamos criar /module/Blog/src/Blog/Model/PostInterface.php e /module/Blog/src/Blog/Model/Post.php. Primeiro o PostInterface:



<?php
// Filename: /module/Blog/src/Blog/Model/PostInterface.php
namespace Blog\Model;

interface PostInterface
{
  /**
  * Will return the ID of the blog post
  *
  * @return int
  */
  public function getId();

  /**
  * Will return the TITLE of the blog post
  *
  * @return string
  */
  public function getTitle();

  /**
  * Will return the TEXT of the blog post
  *
  * @return string
  */
  public function getText();
}

Observe que nós apenas criamos funções getters aqui. Isto é porque agora não nos incomoda como se pega os dados dentro da classe Post. Toda nossa preocupação é de como seremos capazes de acessar as propriedades por meio dessas funções getters.

E agora nós vamos criar um arquivo Model apropriado associado com a Interface. Certifique-se de criar as propriedades de classe necessários e completar as funções getters definidas por nossa PostInterface com algum conteúdo útil. Mesmo que nossa Interface não se preocupe com funções setters nós iremos escrevê-las como nós preencheremos nossa classe através destes dados. Em seguida, você deve ter uma classe que se parece com o seguinte:


<?php
// Filename: /module/Blog/src/Blog/Model/Post.php
namespace Blog\Model;

class Post implements PostInterface
{
    /**
    * @var int
    */
    protected $id;

    /**
    * @var string
    */
    protected $title;

    /**
    * @var string
    */
    protected $text;

    /**
    * {@inheritDoc}
    */
    public function getId()
    {
        return $this->id;
    }

    /**
    * @param int $id
    */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
    * {@inheritDoc}
    */
    public function getTitle()
    {
        return $this->title;
    }

    /**
    * @param string $title
    */
    public function setTitle($title)
    {
        $this->title = $title;
    }

    /**
    * {@inheritDoc}
    */
    public function getText()
    {
        return $this->text;
    }

    /**
    * @param string $text
    */
    public function setText($text)
    {
        $this->text = $text;
    }
}

Dando vida à nossa Postservice

Agora que temos nossos arquivos Models no lugar podemos realmente trazer vida para nossa classe Postservice. Para manter a camada Service fácil de compreender por enquanto nós iremos retornar apenas algum conteúdo fortemente codificado diretamente da nossa classe Postservice. Crie uma propriedade dentro de PostService Chamada $data e faça desta um array do tipo de nosso Model. Edite PostService parecida com isto:


<?php
// Filename: /module/Blog/src/Blog/Service/PostService.php
namespace Blog\Service;

class PostService implements PostServiceInterface
{
    protected $data = array(
        array(
        'id'    => 1,
        'title' => 'Hello World #1',
        'text'  => 'This is our first blog post!'
        ),
        array(
        'id'     => 2,
        'title' => 'Hello World #2',
        'text'  => 'This is our second blog post!'
        ),
        array(
        'id'     => 3,
        'title' => 'Hello World #3',
        'text'  => 'This is our third blog post!'
        ),
        array(
        'id'     => 4,
        'title' => 'Hello World #4',
        'text'  => 'This is our fourth blog post!'
        ),
        array(
        'id'     => 5,
        'title' => 'Hello World #5',
        'text'  => 'This is our fifth blog post!'
        )
    );

    /**
    * {@inheritDoc}
    */
    public function findAllPosts()
    {
        // TODO: Implement findAllPosts() method.
    }

    /**
    * {@inheritDoc}
    */
    public function findPost($id)
    {
        // TODO: Implement findPost() method.
    }
}

Depois disto temos alguns dados, vamos modificar nosso find*() para retornar o
modelo apropriado


<?php
namespace Blog\Service;
// Filename: /module/Blog/src/Blog/Service/PostService.php
 use Blog\Model\Post;
class PostService implements PostServiceInterface
{
    protected $data = array(
        array(
        'id'    => 1,
        'title' => 'Hello World #1',
        'text'  => 'This is our first blog post!'
        ),
        array(
        'id'     => 2,
        'title' => 'Hello World #2',
        'text'  => 'This is our second blog post!'
        ),
        array(
        'id'     => 3,
        'title' => 'Hello World #3',
        'text'  => 'This is our third blog post!'
        ),
        array(
        'id'     => 4,
        'title' => 'Hello World #4',
        'text'  => 'This is our fourth blog post!'
        ),
        array(
        'id'     => 5,
        'title' => 'Hello World #5',
        'text'  => 'This is our fifth blog post!'
        )
    );

    /**
    * {@inheritDoc}
    */
    public function findAllPosts()
    {
        $allPosts = array();

        foreach ($this->data as $index => $post) {
            $allPosts[] = $this->findPost($index);
        }

        return $allPosts;
    }

    /**
    * {@inheritDoc}
    */
    public function findPost($id)
    {
        $postData = $this->data[$id];

        $model = new Post();
        $model->setId($postData['id']);
        $model->setTitle($postData['title']);
        $model->setText($postData['text']);

        return $model;

    }
}


Como você pode ver, ambas as nossas funções agora têm valores de retorno adequados. Por favor, note que a partir de um ponto de vista técnico, a implementação atual está longe de ser perfeita. Vamos melhorar muito este Service no futuro, mas por hora temos um Service trabalhando que é capaz de nos fornecer alguns dados do modo que está definido pelo nosso PostServiceInterface.

Trazendo o Service para o Controller

Agora que temos o nosso Postservice escrito, queremos ter acesso a este serviço em nossos Controllers. Para esta tarefa, vamos pisar em um novo tema chamado “Dependency Injection” (Injeção de dependência), abreviação “DI”.

Quando estamos falando de injeção de dependência que estamos falando de uma maneira de obter dependências em nossas classes. A forma mais comum, “Constructor Injection” (Injeção de Construtor), é usado para todas as dependências que são exigidas por uma classe a qualquer momento.

No nosso caso, precisamos ter em nossa ListController de Blog-Module alguma forma interagir com a nossa Postservice. Isto significa que classe PostService é uma dependência da classe ListController. Sem a PostService nossa ListController não será capaz de funcionar adequadamente. Para garantir de que o nosso ListController obtenha sempre a dependência apropriada, vamos primeiro definir a dependência dentro de ListControllers a função construtora __construct(). Vá em frente e modifique o ListController como este:


<?php
namespace Blog\Controller;

// Filename: /module/Blog/src/Blog/Controller/ListController.php

use Blog\Service\PostServiceInterface;
use Zend\Mvc\Controller\AbstractActionController;

class ListController extends AbstractActionController
{
    /**
    * @var \Blog\Service\PostServiceInterface
    */
    protected $postService;

    public function __construct(PostServiceInterface $postService)
    {
        $this->postService = $postService;
    }
}

Como você pode ver a função __construct() agora tem um argumento requerido. Nós não estamos mais aptos a chamar essa classe sem passá-la uma instância de uma classe que corresponde a nossa definição em PostServiceInterface.
Se você fosse voltar para o seu browser e recarregar a url do seu projeto com localhost:8080/blog, você verá a seguinte mensagem de erro:

( ! ) Catchable fatal error: Argument 1 passed to Blog\Controller\ListController::__construct()
must be an instance of Blog\Service\PostServiceInterface, none given,
called in {libraryPath}\Zend\ServiceManager\AbstractPluginManager.php on line {lineNumber}
and defined in \module\Blog\src\Blog\Controller\ListController.php on line {lineNumber}

E essa mensagem de erro é esperada. Isto lhe diz exatamente que o nosso ListController espera que seja passada uma implementação do PostServiceInterface. Então, como podemos ter certeza de que o nosso ListController receberá essa implementação? Para resolver isso, nós precisamos dizer à aplicação como criar instâncias de Blog\Controller\ListController. Se você lembra de quando nós criamos o controller, nós adicionamos uma entrada para o array invokables na configuração do módulo:


<?php
// Filename: /module/Blog/config/module.config.php
return array(
    'view_manager' => array( /** ViewManager Config */ ),
    'controllers'  => array(
        'invokables' => array(
            'Blog\Controller\List' => 'Blog\Controller\ListController'
        )
    ),
    'router' => array( /** Router Config */ )
);

Um invokable é uma classe que pode ser construída sem nenhum argumento. Já que nosso Blog\Controller\ListController agora tem necessidade um argumento, nós precisamos mudar isto. O ControllerManager, que é responsável por instanciar controllers, também suporta a utilização de factories (fábricas).
Uma factory é uma classe que cria instâncias de outra classe. Vamos agora criar uma para o nosso ListController. Vamos modificar nossas configurações como estas:


 <?php
// Filename: /module/Blog/config/module.config.php
return array(
    'view_manager' => array( /** ViewManager Config */ ),
    'controllers'  => array(
        'factories' => array(
            'Blog\Controller\List' => 'Blog\Factory\ListControllerFactory'
        )
    ),
    'router' => array( /** Router Config */ )
);

Como você pode ver não temos mais a chave invokables, em vez disso, agora temos a chave factories. Além disso, o nome Blog\Controller\List teve seu valor alterado para não coincidir diretamente com a classe Blog\Controller\ListController em vez de chamar uma classe chamada Blog\Factory\ListControllerFactory. Se você atualizar seu navegador, você verá uma mensagem de erro diferente:

An error occurred
An error occurred during execution; please try again later.

Additional information:
Zend\ServiceManager\Exception\ServiceNotCreatedException

File:
{libraryPath}\Zend\ServiceManager\AbstractPluginManager.php:{lineNumber}

Message:
While attempting to create blogcontrollerlist(alias: Blog\Controller\List) an invalid factory was registered for this instance type.

Esta mensagem deve ser muito fácil de entender. O Zend\Mvc\Controller\ControllerManager está acessando Blog\Controller\List, que internamente é salvo como blogcontrollerlist.
Enquanto ele faz isso ele percebe que uma classe de fábrica é supostamente chamada por este nome de controller. No entanto, ele não encontra esta classe de fábrica desta forma para o Gerenciador é uma fábrica inválida. Em palavras mais simples: o Manager (gerente) não encontrar a classe de fábrica (Factory class) então este é provavelmente o lugar onde reside o nosso erro. E claro, ainda temos que escrever a fábrica, então vamos em frente e fazer isto. A implementação desta classe permite que o ServiceManager saiba que a CreateService() é supostamente chamada. E CreateService() realmente espera que seja passada uma instância da ServiceLocatorInterface assim o ServiceManager sempre irá injetar isso usando Injeção de Dependência como nós aprendemos snteriormente. Vamos implementar nossa classe Factory:

Escrevendo uma classe Factory

Classe Factory dentro do Zend Framework 2 sempre necessita de implementação do Zend\ServiceManager\FactoryInterface


<?php
// Filename: /module/Blog/src/Blog/Factory/ListControllerFactory.php
namespace Blog\Factory;

use Blog\Controller\ListController;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class ListControllerFactory implements FactoryInterface
{
    /**
    * Create service
    *
    * @param ServiceLocatorInterface $serviceLocator
    *
    * @return mixed
    */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $realServiceLocator = $serviceLocator->getServiceLocator();
        $postService        = $realServiceLocator->get('Blog\Service\PostServiceInterface');

        return new ListController($postService);
    }
}

Agora isto parece complicado! Vamos começar olhando em $realServiceLocator. Ao usar uma classe Factory que será chamada de ControllerManager ela vai na verdade injetar-se como o $serviceLocator. No entanto, precisamos do real ServiceManager para chegar a nossa classe de Service. Isto é porque nós chamamos a função getServiceLocator que nos dará o real ServiceManager.

Depois nós temos que configurar o $realServiceLocator para tentar obter um Service chamado Blog\Service\PostServiceInterface. Presumimos que este nome que estamos acessando retorne um Service que corresponde a PostServiceInterface.
Este Service é então passado para o ListController que irá diretamente ser retornado.

Note no entanto que ainda temos de registrar um Service chamado Blog\Service\PostServiceInterface. Não é nenhum acontecimento mágico que faz isso para nós apenas porque damos o nome Service de uma Interface. Atualize seu browser e você verá esta mensagem de erro:

An error occurred
An error occurred during execution; please try again later.

Additional information:
Zend\ServiceManager\Exception\ServiceNotFoundException

File:
{libraryPath}\Zend\ServiceManager\ServiceManager.php:{lineNumber}

Message:
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for Blog\Service\PostServiceInterface

Exatamente o que nós esperamos. Em qualquer lugar de nossa aplicação – atualmente nossa classe Factory – um Service chamado Blog\Service\PostServiceInterface é requerida mas o ServiceManager não sabe sobre este serviço ainda. Assim isto não é capaz criar uma instância para o nome requisitado.

Registrando Service.

Registar um Serviço é tão simples quanto registrar um controlador. Tudo o que precisamos fazer é modificar o nosso module.config.php e adicionar uma nova chave chamada service_manager que em seguida tem invokables e factories, também, da mesma forma que temos isto dentro do nosso array controller. Cheque o novo arquivo de configuração:


<?php
// Filename: /module/Blog/config/module.config.php
return array(
    'service_manager' => array(
        'invokables' => array(
            'Blog\Service\PostServiceInterface' => 'Blog\Service\PostService'
        )
    ),
    'view_manager' => array( /** View Manager Config */ ),
    'controllers'  => array( /** Controller Config */ ),
    'router'       => array( /** Router Config */ )
);

Como você pode ver agora temos acrescentado um novo serviço que atende pelo nome Blog\Service\PostServiceInterface e aponta para a nossa própria implementação que é Blog\Service\PostService. Desde que o nosso Service não tem dependências nós estamos aptos a adicionar este Service através o array invokables. tente atualizar o seu browser. Você não verá mais mensagens de erro, mas sim exatamente a página que criamos no capítulo anterior do Tutorial

Usando o Service em nosso Controller

Vamos agora usar o PostService dentro da nossa ListController. Para isso nós precisamos substituir o indexAction() padrão para retornar os valores da nossa PostService para a view. Modifique a ListController para se parecer com isto:


<?php
// Filename: /module/Blog/src/Blog/Controller/ListController.php
namespace Blog\Controller;

use Blog\Service\PostServiceInterface;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class ListController extends AbstractActionController
{
    /**
    * @var \Blog\Service\PostServiceInterface
    */
    protected $postService;

    public function __construct(PostServiceInterface $postService)
    {
        $this->postService = $postService;
    }

    public function indexAction()
    {
        return new ViewModel(array(
            'posts' => $this->postService->findAllPosts()
        ));
    }
}

Primeiro por favor note que o nosso controller importou outra classe. Precisamos importar Zend\View\Model\ViewModel, que geralmente é o que seus controllers irão retornar. Ao retornar uma instância de um ViewModel você sempre pode especificar uma chamada de variável de View. Neste caso nós atribuímos uma variável chamada $posts com um valor qualquer retornado da função findAllPosts() da nossa PostService. No nosso caso isto é um array de nossa classe Blog\Model\Post. Atualizando o navegador não vai mudar nada ainda, porque nós evidentemente precisamos modificar nosso arquivo de view para ser capaz de exibir os dados que desejamos.

Nota


Você não obrigatoriamente precisa retornar uma instância de ViewModel. Quando você retornar um array php normal ele será convertido internamente em um ViewModel. Assim como abaixo:

return new ViewModel(array('foo' => 'bar'));
é igual a
return array('foo' => 'bar');

Até aqui nós simplesmente executamos um foreach sobre o nosso array $this-> posts. Já que cada entrada de nosso array é do tipo Blog\Model\Post nós podemos usar a respectivas funções getters para obter os dados que precisamos.

Resumo

E com isto o capítulo atual é finalizado. Nós aprendemos agora como interagir com o ServiceManager e também o que é Injeção de Dependência e tudo isto. Agora nós estamos aptos a passar variáveis de nossos Services para nossas views atraveś de um controller e sabemos como interagir com arrays dentro de um script de view.

No próximo capítulo, vamos primeiro dar uma olhada nas coisas que devemos fazer quando queremos obter dados de um banco de dados.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: