[livro] zendframework 2 - elton luís minetto

Upload: deryk-sedlak

Post on 11-Oct-2015

93 views

Category:

Documents


2 download

TRANSCRIPT

  • Zend Framework 2 na prticaElton MinettoThis book is for sale at http://leanpub.com/zend-framework2-na-pratica

    This version was published on 2012-11-14

    This is a Leanpub book. Leanpub helps authors to self-publish in-progress ebooks. We call this idea LeanPublishing.To learn more about Lean Publishing, go to http://leanpub.com/manifesto.To learn more about Leanpub, go to http://leanpub.com.

    2012 Leanpub

  • Contedo

    Introduo 1

    Agradecimentos 2

    Instalando 3Requisitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3Instalando o framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3Instalando o PHPUnit/PHPQATools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

    Definindo o projeto 6Descrio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6Modelagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6Configurando o projeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8Configuraes dos testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

    Modelos 11Criando o teste para a entidade Post . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12Rodando os testes pela primeira vez . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16Criando a entidade Post . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16Rodando os testes novamente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19Criando o teste da entidade Comment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20Criando o cdigo da entidade Comment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

    Controladores 30Criando os testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30Criando o controlador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34Vises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34Layouts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36Executando os testes novamente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39Desafio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

    Paginador 40Adicionando o teste do paginador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40Adicionando o paginador no IndexController . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42Partials . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

  • CONTEDO

    Mdulos 46Configurando o novo mdulo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46Criando a entidade User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

    Servios 57Servio de autenticao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57ServiceManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63Rodando os testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

    Formulrios 66Formulrio de login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66Controlador de autenticao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67CRUD de posts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

    Eventos 82Incluindo a autenticao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

    Controle de Acesso 85Incluindo a autorizao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

    View Helper 91Criando um View Helper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

    Cache 94Introduo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94Configurando o Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94Usando o cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

    Tradues 97Traduzindo o projeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97Traduzindo formulrios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

    Requisies Assncronas 99Gerando uma API de comentrios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99Mostrando a view sem o layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

  • CONTEDO

    Doctrine 101Instalando o Doctrine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101Configurando o projeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102Criando uma entidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105Criando os testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107CRUD de Usurio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

    Concluso 123

  • IntroduoDeixe-me comear este livro explicando como funciona minha mente e minha forma de aprender. Sempreme considerei uma pessoa pragmtica e isso se reflete na forma como eu aprendo as coisas. Quando meempolgo ou preciso aprender uma nova tecnologia ou ferramenta eu coloco na minha mente uma meta.Com o Zend Framework 2 no foi diferente, apesar dos motivos terem sido. A meta foi a coincidnciade estar iniciando um grande projeto para um cliente exatamente na semana que o framework teve suaverso estvel lanada. Com um prazo de entrega j definido pelo contrato deu-se incio o desafio deaprender novos conceitos e uma nova forma de trabalhar com o cdigo.Tendo a meta definida eu inicio a codificao e os testes da nova tecnologia. Eu no comeo lendo toda ateoria antes de colocar a mo na massa. Eu preciso desse feedback imediato, de codificar algo pequenorapidamente, de ter algo funcionando que me d a vontade de continuar aprendendo. Conforme eu voume deparando com os desafios do desenvolvimento eu paro e a sim leio a teoria necessria para entenderexatamente o que estou fazendo.Pode no ser a melhor forma de aprender mas tem funcionado bem comigo, e baseando-se nos feedbacksda verso anterior deste e-book, parece funcionar para mais pessoas. Ento vou continuar com essaabordagem neste livro. Vamos traar uma meta inicial fcil de ser cumprida, iniciar o projeto e iraprofundando a teoria conforme formos nos deparando com os desafios de entregar o projeto. Por issovoc no vai encontrar no ndice um captulo inicial sobre teoria, explicando coisas legais como injeode dependncias ou eventos, mas vai encontrar tpicos sobre isso dentro dos captulos sobre a codificao,conforme formos precisando us-las.Com essa abordagemmais prtica espero lev-los pelas fases de planejamento, desenvolvimento de testes,codificao e deploy de um aplicativo web com o Zend Framework 2 e outras tecnologias teis ao dia adia.Que o desafio comece!

    1

  • AgradecimentosQuero agradecer a algumas pessoas em especial.A toda a equipe da Coderockr por me ajudar com idias e aguentar minhas reclamaes quando noencontrava algo na documentao do Zend Framework.A equipe da Unochapec, que acabou fornecendo a experincia de desenvolvimento de um grande projetousando essa nova tecnologia. Em especial o Cristian Oliveira, Cledir Scopel e Lissandro Hoffmeister peloapoio.E minha namorada Mirian Giseli Aguiar pela reviso de portugus e por respeitar minha ausncia e mauhumor por algumas semanas.Obrigado a todos.

    2

  • InstalandoO processo de instalao do Zend Framework 2 foi um dos tpicos que teve maior avano desde a versoanterior. Ficou realmente muito mais fcil de instalar e atualizar o framework e suas dependncias.

    Requisitos

    Para criarmos um projeto usando o Zend Framework 2 precisamos atender os seguintes requisitos:

    Um servidor Web. O mais usado o Apache mas pode ser configurado usando outros comoo IIS. Os exemplos desse livro consideram o uso do servidor Apache. O PHP 5.4 possui umservidor web embutido, mas no considerei o uso dele nesse livro pois nem todos os ambientesde desenvolvimento esto atualizados para esta verso recente da linguagem. No caso de usar oApache necessrio que o mdulo mod_rewrite esteja habilitado. No arquivo de configuraobasta adicionar as linhas abaixo, ou alter-las para refletir o seguinte:1 LoadModule rewrite_module modules/mod_rewrite.so2 AddModule mod_rewrite.c3 AllowOverride all Um banco de dados. No algo obrigatrio mas no nosso caso iremos usar o banco MySQL. Claroque voc pode usar outro banco como o SQLite ou o PostgreSQL, mas os exemplos sero escritospara o MySQL.

    PHP 5.3.3 ou superior. Extenso intl do PHP. O framework usa esta extenso para formatar datas e nmeros. Esta extensopode ser instalada usando-se o comando pecl do PHP.

    Caso esteja usando Windows ou MacOSX estes requisitos podem ser facilmente cumpridos instalando-se um dos pacotes de desenvolvimento famosos como oXAMPP (Windows e MacOSX ) ou o MAMP(MacOSX), que possuem todos os pacotes j configurados.Usando-se Linux basta usar o sistema de gerenciamento de pacotes (apt-get, yum, etc) para instalar ospacotes necessrios.

    Instalando o framework

    A forma mais recomendada de iniciar um projeto usar um dos esqueletos de aplicao que estodisponveis no Github. A documentao oficial do framework recomenda o uso do:https://github.com/zendframework/ZendSkeletonApplicationO que vamos fazer nesse curso usar um esqueleto que criei, baseado no oficial da Zend, mas comalgumas novas classes que facilitam o desenvolvimento. Alm disso, o esqueleto que iremos usar j vemcom as configuraes necessrias para usarmos testes automatizados e um mdulo de modelo, com suasconfiguraes. Venho usando esse esqueleto em aplicaes reais e pretendo manter o cdigo open sourcee atualizado.Para iniciarmos o nosso projeto vamos clonar o projeto usando o git. O primeiro passo acessarmos nossodiretrio de projetos. No meu MacOSX esse diretrio o /Users/eminetto/Documents/Projects/ mas vocpode mud-lo para qualquer diretrio do seu sistema operacional.Vamos executar os comandos:

    3

  • Instalando 4

    1 cd /Users/eminetto/Documents/Projects/2 git clone [email protected]:eminetto/ZendSkeletonApplication.git zf2napratica

    Isso vai criar um diretrio chamado zf2napratica com o cdigo do esqueleto.Se voc no tiver o git instalado na sua mquina pode fazer o download e descompactar no diretrio. Odownload pode ser feito na url:https://github.com/eminetto/ZendSkeletonApplication/zipball/master

    Instalar dependncias com Composer

    Ao clonar (ou fazer o download) do esqueleto da aplicao ainda no temos o framework em si. A formamais rpida de termos o framework instalado usando a ferramenta Composer. O Composer umaferramenta criada para instalar e atualizar dependncias de cdigo em projetos PHP. Para entender emdetalhes como funciona o Composer eu recomendo esse screencast e o site oficial da ferramenta.Vamos usar o composer para instalar o framework:

    1 cd zf2napratica2 curl - http://getcomposer.org/installer | php3 php composer.phar install

    Com isso o Composer far o download do framework e todas as suas dependncias, bem como configurarum autoloader que o framework usar.Bem mais fcil e rpido do que as verses antigas, que pediam cadastro no site da Zend.

    Configurar o Vhosts do Apache

    Um hbito que eu tenho sempre que desenvolvo um novo projeto criar um servidor virtual na minhamquina para isolar o ambiente do projeto. Isso facilita bastante os testes, a organizao dos projetos eat mesmo o deploy do aplicativo para o servidor de produo no final do desenvolvimento.Para isso vamos configurar um servidor virtual no Apache. No arquivo httpd.conf (ou apache.conf ou naconfigurao de servidores virtuais do seu sistema operacional) adicionar o seguinte:

    1 2 ServerName zf2napratica.dev3 DocumentRoot /Users/eminetto/Documents/Projects/zf2napratica/public4 SetEnv APPLICATION_ENV "development"5 SetEnv PROJECT_ROOT "/Users/eminetto/Documents/Projects/zf2napratica"6 7 DirectoryIndex index.php8 AllowOverride All9 Order allow,deny10 Allow from all11 12

  • Instalando 5

    https://gist.github.com/4003621 necessrio alterar os caminhos nas opes DocumentRoot, PROJECT_ROOT e Directory para refletiremo caminho correto em sua mquina. preciso tambm alterar o arquivo hosts do sistema operacional para adicionar o endereo zf2napratica.devpois o mesmo no existe em nenhum DNS.No Linux e Mac OSX, alterar o /etc/hosts e adicionar a linha:

    1 127.0.0.1 zf2napratica.dev

    No Windows o arquivo que deve ser alterado o c:\windows\system32\drivers\etc\hosts e a linha a seradicionada igual a citada acima.

    Instalando o PHPUnit/PHPQATools

    Vamos usar o framework PHPUnit para gerar testes automatizados durante o desenvolvimento. Parainstal-lo precisamos usar o pear, gerenciador de pacotes do PHP :

    1 pear config-set auto_discover 12 pear install pear.phpqatools.org/phpqatools

    Se estiver usando Linux ou MacOSX necessrio adicionar o comando sudo no incio de cada comandoacima.Com esses passos temos um ambiente instalado e podemos iniciar o planejamento do nosso primeiroprojeto usando o Zend Framework 2.

  • Definindo o projetoDescrio

    Na minha opinio a melhor forma de aprender uma nova ferramenta, linguagem ou sistema operacional quando voc realmente precisa resolver algum problema com ela. Pensando nisso, esse livro baseadona construo de um aplicativo: um blog.Mas um blog? Por alguns motivos:

    um problema fcil de se entender. Todo mundo sabe como um blog funciona, seus requisitos efuncionalidades, ento a fase de requisitos do projeto fcil de completar.

    Um blog apresenta um grande nmero de funcionalidades comuns a vrios outros sites, comomdulos, controle de acesso e permisses, upload de arquivos, tratamento de formulrios, cache,tradues, integrao com servios externos, etc.

    A grande maioria dos frameworks possui um exemplo como desenvolver um blog usando X,ento fica mais fcil para comparao se voc j estudou algum outro framework como CakePHP,CodeIgniter ou mesmo Ruby on Rails

    Modelagem

    Agora que o convenci (ou no) que desenvolver um blog pode lhe ajudar a entender o Zend Framework,vamos mostrar a modelagem das tabelas:

    Sim-ples, como deveria ser. ## Criao das tabelas Usando alguma ferramenta, como o PHPMyAdmin,SequelPro, ou o bom e velho terminal, possvel criar a estrutura do banco usando os comandos SQLabaixo:

    6

  • Definindo o projeto 7

    1 create database zf2napratica;2 create database zf2napratica_test;34 GRANT ALL privileges ON zf2napratica.* TO zend@localhost IDENTIFIED BY 'zen\5 d';6 GRANT ALL privileges ON zf2napratica_test.* TO zend@localhost IDENTIFIED BY\7 'zend';89 use zf2napratica;1011 CREATE TABLE IF NOT EXISTS `users` (12 `id` INT NOT NULL AUTO_INCREMENT ,13 `username` VARCHAR(200) NOT NULL ,14 `password` VARCHAR(250) NOT NULL ,15 `name` VARCHAR(200) NULL ,16 `valid` TINYINT NULL ,17 `role` VARCHAR(20) NULL ,18 PRIMARY KEY (`id`) )19 ENGINE = InnoDB;2021 CREATE TABLE IF NOT EXISTS `posts` (22 `id` INT NOT NULL AUTO_INCREMENT ,23 `title` VARCHAR(250) NOT NULL ,24 `description` TEXT NOT NULL ,25 `post_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ,26 PRIMARY KEY (`id`) )27 ENGINE = InnoDB;2829 CREATE TABLE IF NOT EXISTS `comments` (30 `id` INT NOT NULL AUTO_INCREMENT ,31 `post_id` INT NOT NULL ,32 `description` TEXT NOT NULL ,33 `name` VARCHAR(200) NOT NULL ,34 `email` VARCHAR(250) NOT NULL ,35 `webpage` VARCHAR(200) NOT NULL ,36 `comment_date` TIMESTAMP NULL ,37 PRIMARY KEY (`id`, `post_id`) ,38 INDEX `fk_comments_posts` (`post_id` ASC) ,39 CONSTRAINT `fk_comments_posts`40 FOREIGN KEY (`post_id` )41 REFERENCES `posts` (`id` )42 ON DELETE NO ACTION43 ON UPDATE NO ACTION)44 ENGINE = InnoDB;

    https://gist.github.com/4011976No script acima criamos duas bases de dados (zf2napratica e zf2napratica_test) que vamos usar para obanco de produo e o banco de testes, respectivamente. Voltaremos a esse banco de testes nos prximostpicos.

  • Definindo o projeto 8

    Configurando o projeto

    O Zend Framework 2 conta com arquivos de configurao separados que so unificados no momento daexecuo.Os principais arquivos que iremos usar durante o projeto so:

    config/application.config.php: Arquivo com as configuraes gerais da aplicao. So configuraesusadas por todos os mdulos e componentes.

    config/test.config.php: Arquivo com as configuraes usadas pelos testes automatizados quecriaremos no decorrer do projeto.

    config/autoload/global.php e config/autoload/local.php: O arquivo global.php usado como auxiliarao application.config.php pois tambm contm configuraes para a aplicao como um todo.A idia colocar neste arquivo configuraes que podem mudar de acordo com a mquina dodesenvolvedor. Um exemplo so as configuraes da conexo com o banco de dados. Estasconfiguraes podem ser alteradas para as mquinas locais, dos desenvolvedores. Para isso odesenvolvedor sobrescreve as configuraes no local.php. O arquivo local.php no deve ser salvono controle de verses (svn ou git por exemplo).

    module/Nome/config/module.config.php: Configuraes especficas ao mdulo.

    Os arquivos de configurao so geralmente scripts PHP que retornam arrays de configurao. So rpidosdurante a execuo e de fcil leitura.

    global.php

    1

  • Definindo o projeto 9

    1

  • Definindo o projeto 10

    Configuraes do PHPUnit

    Vamos usar o mdulo Skel como modelo para os nossos novos mdulos. Iniciamos usando as configura-es do PHPUnit copiando alguns arquivos para o mdulo Application:

    1 cp module/Skel/tests/Bootstrap.php module/Application/tests/2 cp module/Skel/tests/phpunit.xml module/Application/tests/

    Precisamos agora alterar os arquivos para usarmos no novo mdulo. No phpunit.xml, linha 8:

    1

    e linha 24:

    1

    Vamos tambm alterar a configurao do arquivo Bootstrap.php:

    1 static function getModulePath()2 {3 //mudar o caminho do modulo4 return __DIR__ . '/../../../module/Application';5 }

    Projeto definido e configurado. Agora, mos a obra!

  • ModelosVamos comeo o desenvolvimento pela primeira camada da nossa aplicao MVC: os modelos. Paraisso vamos criar um arquivo para cada tabela e estes devem ser armazenados no diretrio Model do srcdo mdulo, como no exemplo: module/Application/src/Application/Model . Todos os modelos so classesPHP que extendem a classe Core\Model\Entity.Mas antes de iniciarmos criando as entidades vamos criar os testes para elas, seguindo a filosofia do TDD.Primeiro precisamos criar um diretrio para os nossos primeiros testes. No Linux/Mac podemos usar ocomando:

    1 mkdir module/Application/tests/src/Application/Model

    Odiretrio tests/src emula a estrutura do diretrio src domdulo, assim teremos diretrios com osmesmosnomes, indicando ao que os testes se referem.No captulo anterior ns criamos duas bases de dados no bancoMySQL: a zf2napratica e a zf2napratica_-test. Vamos agora usar a base de dados de teste. Antes de cada teste ser executado precisamos preparar oestado inicial do banco de dados. Para isso precisamos criar um arquivo chamado module/Application/-data/test.data.php, com o contedo abaixo.Primeiro criamos o diretrio data:

    1 mkdir module/Application/data

    E inclumos seu contedo:

    1

  • Modelos 12

    23 PRIMARY KEY (id, post_id) ,24 INDEX fk_comments_posts (post_id ASC) ,25 CONSTRAINT fk_comments_posts26 FOREIGN KEY (post_id )27 REFERENCES posts (id )28 ON DELETE NO ACTION29 ON UPDATE NO ACTION)30 ENGINE = InnoDB;',3132 'drop' =>'drop table comments;'33 ),34 );

    https://gist.github.com/4011989Antes de cada teste ser executado as classes do mdulo Core vo usar esse arquivo como parmetro e criaras tabelas (usando o comando SQL no create). Aps cada teste o drop ser executado e a tabela de testedeixa de existir.OBS: Para um projeto maior, com mais tabelas e mais testes esse procedimento pode comear a ficardemorado e complexo demanter. Para estes casos podemos usar outras tcnicas como criar a base de dadosno banco SQLite em memria ou mesmo usar o conceito de Mocks para emular a existncia do banco dedados. Mas para nosso pequeno e didtico projeto estes procedimentos servem para seu propsito.

    Criando o teste para a entidade Post

    Vamos iniciar criando o teste da entidade Post, que salvo no arquivo module/Application/tests/src/Ap-plication/Model/PostTest.php:

    1

  • Modelos 13

    2122 /**23 * @depends testGetInputFilter24 */25 public function testInputFilterValid($if)26 {27 //testa os filtros28 $this->assertEquals(4, $if->count());2930 $this->assertTrue($if->has('id'));31 $this->assertTrue($if->has('title'));32 $this->assertTrue($if->has('description'));33 $this->assertTrue($if->has('post_date'));34 }3536 /**37 * @expectedException Core\Model\EntityException38 */39 public function testInputFilterInvalido()40 {41 //testa se os filtros esto funcionando42 $post = new Post();43 //title s pode ter 100 caracteres44 $post->title = 'Lorem Ipsum e simplesmente uma simulacao de texto da i\45 ndustria tipografica e de impressos. Lorem Ipsum simplesmente uma simulac\46 ao de texto da industria tipografica e de impressos';47 }4849 /**50 * Teste de insercao de um post valido51 */52 public function testInsert()53 {54 $post = $this->addPost();5556 $saved = $this->getTable('Application\Model\Post')->save($post);5758 //testa o filtro de tags e espacos59 $this->assertEquals('A Apple compra a Coderockr', $saved->description)\60 ;61 //testa o auto increment da chave primaria62 $this->assertEquals(1, $saved->id);63 }6465 /**66 * @expectedException Core\Model\EntityException67 * @expectedExceptionMessage Input invlido: description =68 */69 public function testInsertInvalido()

  • Modelos 14

    70 {71 $post = new Post();72 $post->title = 'teste';73 $post->description = '';7475 $saved = $this->getTable('Application\Model\Post')->save($post);76 }7778 public function testUpdate()79 {80 $tableGateway = $this->getTable('Application\Model\Post');81 $post = $this->addPost();8283 $saved = $tableGateway->save($post);84 $id = $saved->id;8586 $this->assertEquals(1, $id);8788 $post = $tableGateway->get($id);89 $this->assertEquals('Apple compra a Coderockr', $post->title);9091 $post->title = 'Coderockr compra a Apple';92 $updated = $tableGateway->save($post);9394 $post = $tableGateway->get($id);95 $this->assertEquals('Coderockr compra a Apple', $post->title);96 }9798 /**99 * @expectedException Core\Model\EntityException100 * @expectedExceptionMessage Could not find row 1101 */102 public function testDelete()103 {104 $tableGateway = $this->getTable('Application\Model\Post');105 $post = $this->addPost();106107 $saved = $tableGateway->save($post);108 $id = $saved->id;109110 $deleted = $tableGateway->delete($id);111 $this->assertEquals(1, $deleted); //numero de linhas excluidas112113 $post = $tableGateway->get($id);114 }115116 private function addPost()117 {118 $post = new Post();

  • Modelos 15

    119 $post->title = 'Apple compra a Coderockr';120 $post->description = 'A Apple compra a Coderockr ';121 $post->post_date = date('Y-m-d H:i:s');122123 return $post;124 }125 }

    https://gist.github.com/4011992Se esse foi seu primeiro contato com um teste automatizado escrito usando-se o framework PHPUnitimagino que voc deve estar um pouco perdido e chocado com a quantia de cdigo acima. Vou explicaros detalhes mais importantes.

    1 namespace Application\Model;2 use Core\Test\ModelTestCase;3 use Application\Model\Post;4 use Zend\InputFilter\InputFilterInterface;

    O Zend Framework 2 usa extensivamente o conceito de Namespaces que foi introduzido no PHP 5.3. Naprimeira linha indicamos a qual espao o cdigo que vamos escrever pertence, sendo obrigatrio sempretermos um. Nas prximas linhas indicamos quais componentes vamos precisar e de quais namespaceseles pertencem. Fazendo isso as classes so automaticamente carregadas, sem precisarmos nos preocuparem usar include ou require, deixando essa tarefa para o mecanismo de autoloading implementado peloframework. Mais informaes sobre namespaces podem ser encontradas no manual do PHP.

    1 /**2 * @group Model3 */

    Indica a qual grupo de testes este teste pertence. No algo obrigatrio, mas facilita a execuo dos testesem grupos separados.

    1 public function testGetInputFilter()

    Responsvel pelo teste da existncia de um conjunto de filtros na entidade. Filtros e validadores serousados para garantirmos a segurana da nossa aplicao. Veremos mais sobre eles mais tarde.

    1 public function testInputFilterValid($if)

    Enquanto o teste anterior verificava a existncia de um conjunto de filtros, aqui testamos cada um doscampos que queremos filtrar. Por enquanto testamos se existe um filtro em especfico para cada item daentidade.

    1 public function testInputFilterInvalido()

    Neste teste verificamos se o filtro do item title est funcionando. Ns queremos que o campo no tenhamais do que 100 caracteres.

  • Modelos 16

    1 public function testInsert()

    Este teste importante pois nos traz novos conceitos. O mais importante deles o uso do $this->getTable(Aplication\Model\Post). Esta chamada nos retorna uma instncia de um TableGatewaygenrico que desenvolvi e est no mdulo Core. A funo de um TableGateway realizar operaessobre entidades pois elas no possuem comportamento, sendo apenas representaes dos dados. Asprincipais operaes como inserir, remover, atualizar, pesquisar sero sempre feitas atravs desse gateway.Existem muitas vantagens nessa abordagem como podermos mudar a camada das entidades (substituirpor entidades do ORM Doctrine por exemplo) ou mudar o comportamento de todas as entidades apenasalterando o gateway. O testInsert() tenta salvar a entidade na tabela e verifica se obteve um id, que gerado automaticamente pelo banco de dados (linha 61).

    1 public function testInsertInvalido()

    Simula uma incluso invlida e verifica se gerada uma exception, no caso uma EntityException. Se elafoi gerada significa que o validador do campo est funcionando corretamente.

    1 public function testUpdate() e public function testDelete()

    Estes dois testes testam a alterao e excluso de um registro e no apresentam novos conceitos.

    Rodando os testes pela primeira vez

    Agora que criamos o nosso primeiro teste vamos rodar o comando phpunit para verificarmos o resultado:

    1 phpunit -c module/Application/tests/phpunit.xml2 PHPUnit 3.7.7 by Sebastian Bergmann.34 Configuration read from /Users/eminetto/Documents/Projects/zf2napratica/mod\5 ule/Application/tests/phpunit.xml67 PHP Fatal error: Class 'Application\Model\Post' not found in /Users/eminet\8 to/Documents/Projects/zf2napratica/module/Application/tests/src/Application\9 /Model/PostTest.php on line 15

    Esse primeiro erro era esperado pois ainda no criamos a classe Post, o que faremos agora.

    Criando a entidade Post

    O primeiro passo criar o diretrio (caso no exista) de modelos:

    1 mkdir module/Application/src/Application/Model

    E criar dentro dele o arquivo Post.php com o contedo:

  • Modelos 17

    1

  • Modelos 18

    50 {51 if (!$this->inputFilter) {52 $inputFilter = new InputFilter();53 $factory = new InputFactory();5455 $inputFilter->add($factory->createInput(array(56 'name' => 'id',57 'required' => true,58 'filters' => array(59 array('name' => 'Int'),60 ),61 )));6263 $inputFilter->add($factory->createInput(array(64 'name' => 'title',65 'required' => true,66 'filters' => array(67 array('name' => 'StripTags'),68 array('name' => 'StringTrim'),69 ),70 'validators' => array(71 array(72 'name' => 'StringLength',73 'options' => array(74 'encoding' => 'UTF-8',75 'min' => 1,76 'max' => 100,77 ),78 ),79 ),80 )));8182 $inputFilter->add($factory->createInput(array(83 'name' => 'description',84 'required' => true,85 'filters' => array(86 array('name' => 'StripTags'),87 array('name' => 'StringTrim'),88 ),89 )));9091 $inputFilter->add($factory->createInput(array(92 'name' => 'post_date',93 'required' => false,94 'filters' => array(95 array('name' => 'StripTags'),96 array('name' => 'StringTrim'),97 ),98 )));

  • Modelos 19

    99100 $this->inputFilter = $inputFilter;101 }102 return $this->inputFilter;103 }104 }

    https://gist.github.com/4011993Como citei anteriormente, as entidades so filhas da Core\Model\Entity e so apenas representaes dosdados da base de dados. Nesta classe descrevemos o nome da tabela ($tableName), os atributos ($title porexemplo) e os seus filtros (na public function getInputFilter()).Alguns detalhes que podemos configurar quanto aos atributos da entidade:

    Se so obrigatrios ou no, usando o required com valor true ou false (exemplo: linha 58) Os filtros. Exemplos podem ser encontrados no filtro Int (linha 60), que transforma o campo eminteiro; StripTags (linha 68) que remove quaisquer tags HTML/JavaScript no texto; e StringTrim(linha 69) que remove espaos no comeo e final do texto. Existem outros filtros que podem serencontrados no manual do framework, alm da possibilidade de criarmos outros.

    As validaes. Um exemplo pode ser encontrado no StringLength (linhas 73 a 77) que valida seo campo possui a codificao de carecteres UTF-8 e se o tamanho est entre 1 e 100. Caso algumdesses parmetros no for respeitado (por exemplo tentando salvar 101 caracteres) ser gerada umaEntityException indicando que o campo est invlido. Existem outros validadores que podem serencontrados no manual do framework, alm da possibilidade de criarmos outros.

    Ao centralizarmos a configurao dos filtros e validadores na entidade podemos reus-los em outrospontos do projeto, como no uso de formulrios ou at no acesso via uma API.

    Rodando os testes novamente

    Agora que temos o cdigo da entidade Post podemos executar novamente os testes:

    1 phpunit -c module/Application/tests/phpunit.xml2 PHPUnit 3.7.7 by Sebastian Bergmann.3 Configuration read from /Users/eminetto/Documents/Projects/zf2napratica/mod\4 ule/Application/tests/phpunit.xml5 .......6 Time: 3 seconds, Memory: 18.50Mb7 OK (7 tests, 17 assertions)8 Generating code coverage report in Clover XML format ... done9 Generating code coverage report in HTML format ... done

    E agora os testes devem funcionar. Caso algum teste falhe ser indicado qual foi e o provvel motivo dafalha, auxiliando na correo.

  • Modelos 20

    Criando o teste da entidade Comment

    Vamos agora criar o teste da entidade Comment, no arquivo module/Application/tests/src/Model/Com-mentTest.php:

    1

  • Modelos 21

    45 $this->assertTrue(46 $if->has('webpage')47 );48 $this->assertTrue(49 $if->has('comment_date')50 );51 }5253 /**54 * @expectedException Core\Model\EntityException55 * @expectedExceptionMessage Input invlido: email =56 */57 public function testInputFilterInvalido()58 {59 $comment = new Comment();60 //email deve ser um e-mail vlido61 $comment->email = 'email_invalido';62 }6364 /**65 * Teste de insercao de um comment valido66 */67 public function testInsert()68 {69 $comment = $this->addComment();70 $saved = $this->getTable('Application\Model\Comment')->save($comment);71 $this->assertEquals(72 'Comentrio importante alert("ok");', $saved->description73 );74 $this->assertEquals(1, $saved->id);75 }7677 /**78 * @expectedException Zend\Db\Adapter\Exception\InvalidQueryException79 */80 public function testInsertInvalido()81 {82 $comment = new Comment();83 $comment->description = 'teste';84 $comment->post_id = 0;85 $saved = $this->getTable('Application\Model\Comment')->save($comment);86 }8788 public function testUpdate()89 {90 $tableGateway = $this->getTable('Application\Model\Comment');91 $comment = $this->addComment();92 $saved = $tableGateway->save($comment);93 $id = $saved->id;

  • Modelos 22

    9495 $this->assertEquals(1, $id);9697 $comment = $tableGateway->get($id);98 $this->assertEquals(99 '[email protected]', $comment->email100 );101102 $comment->email = '[email protected]';103 $updated = $tableGateway->save($comment);104105 $comment = $tableGateway->get($id);106 $this->assertEquals('[email protected]', $comment->email);107 }108109 /**110 * @expectedException Zend\Db\Adapter\Exception\InvalidQueryException111 * @expectedExceptionMessage Statement could not be executed112 */113 public function testUpdateInvalido()114 {115 $tableGateway = $this->getTable('Application\Model\Comment');116 $comment = $this->addComment();117 $saved = $tableGateway->save($comment);118 $id = $saved->id;119 $comment = $tableGateway->get($id);120 $comment->post_id = 10;121 $updated = $tableGateway->save($comment);122 }123124 /**125 * @expectedException Core\Model\EntityException126 * @expectedExceptionMessage Could not find row 1127 */128 public function testDelete()129 {130 $tableGateway = $this->getTable('Application\Model\Comment');131 $comment = $this->addComment();132 $saved = $tableGateway->save($comment);133 $id = $saved->id;134135 $deleted = $tableGateway->delete($id);136 $this->assertEquals(1, $deleted); //numero de linhas excluidas137138 $comment = $tableGateway->get($id);139 }140141 private function addPost()142 {

  • Modelos 23

    143 $post = new Post();144 $post->title = 'Apple compra a Coderockr';145 $post->description = 'A Apple compra a Coderockr ';146 $post->post_date = date('Y-m-d H:i:s');147148 $saved = $this->getTable('Application\Model\Post')->save($post);149 return $saved;150 }151152 private function addComment()153 {154 $post = $this->addPost();155 $comment = new Comment();156 $comment->post_id = $post->id;157 $comment->description = 'Comentrio importante alert("ok"); ';159 $comment->name = 'Elton Minetto';160 $comment->email = '[email protected]';161 $comment->webpage = 'http://www.eltonminetto.net';162 $comment->comment_date = date('Y-m-d H:i:s');163 return $comment;164 }165 }

    https://gist.github.com/4011994

    Criando o cdigo da entidade Comment

    Se rodarmos os testes agora vamos enfrentar aquele erro devido a no existncia da classe Comment.Ento vamos agora criar o cdigo da entidade e salvar no arquivo module/Application/src/Application/-Model/Comment.php:

    1

  • Modelos 24

    17 {1819 /**20 * Nome da tabela. Campo obrigatrio21 * @var string22 */23 protected $tableName ='comments';2425 /**26 * @var int27 */28 protected $id;2930 /**31 * @var int32 */33 protected $post_id;3435 /**36 * @var string37 */38 protected $description;3940 /**41 * @var string42 */43 protected $name;4445 /**46 * @var string47 */48 protected $email;4950 /**51 * @var string52 */53 protected $webpage;5455 /**56 * @var datetime57 */58 protected $comment_date;5960 /**61 * Configura os filtros dos campos da entidade62 *63 * @return Zend\InputFilter\InputFilter64 */65 public function getInputFilter()

  • Modelos 25

    66 {67 if (!$this->inputFilter) {68 $inputFilter = new InputFilter();69 $factory = new InputFactory();7071 $inputFilter->add($factory->createInput(array(72 'name' => 'id',73 'required' => true,74 'filters' => array(75 array('name' => 'Int'),76 ),77 )));7879 $inputFilter->add($factory->createInput(array(80 'name' => 'post_id',81 'required' => true,82 'filters' => array(83 array('name' => 'Int'),84 ),85 )));8687 $inputFilter->add($factory->createInput(array(88 'name' => 'description',89 'required' => true,90 'filters' => array(91 array('name' => 'StripTags'),92 array('name' => 'StringTrim'),93 ),94 )));9596 $inputFilter->add($factory->createInput(array(97 'name' => 'email',98 'required' => true,99 'filters' => array(100 array('name' => 'StripTags'),101 array('name' => 'StringTrim'),102 ),103 'validators' => array(104 array(105 'name' => 'EmailAddress',106 ),107 ),108 )));109110 $inputFilter->add($factory->createInput(array(111 'name' => 'name',112 'required' => true,113 'filters' => array(114 array('name' => 'StripTags'),

  • Modelos 26

    115 array('name' => 'StringTrim'),116 ),117 'validators' => array(118 array(119 'name' => 'StringLength',120 'options' => array(121 'encoding' => 'UTF-8',122 'min' => 1,123 'max' => 100,124 ),125 ),126 ),127 )));128129 $inputFilter->add($factory->createInput(array(130 'name' => 'webpage',131 'required' => true,132 'filters' => array(133 array('name' => 'StripTags'),134 array('name' => 'StringTrim'),135 ),136 'validators' => array(137 array(138 'name' => 'StringLength',139 'options' => array(140 'encoding' => 'UTF-8',141 'min' => 1,142 'max' => 200,143 ),144 ),145 ),146 )));147148 $inputFilter->add($factory->createInput(array(149 'name' => 'comment_date',150 'required' => false,151 'filters' => array(152 array('name' => 'StripTags'),153 array('name' => 'StringTrim'),154 ),155 )));156157 $this->inputFilter = $inputFilter;158 }159 return $this->inputFilter;160 }161 }

    https://gist.github.com/4011996

  • Modelos 27

    A principal novidade nestes cdigos o uso do validador EmailAddress que verifica se o valor informado um endereo vlido. O restante dos cdigos seguem o que foi explicado na entidade Post.Podemos agora executar novamente os testes para verificar se todos esto passando.A entidade User ser criada nos prximos captulos, quando criarmos o mdulo de administrao dosistema.

    Queries

    Neste captulo vimos como manipular uma entidade usando o conceito de um TableGateway mas emalguns casos precisamos criar consultas mais complexas, como fazer join entre tabelas ou criar subqueries.Para facilitar a criao de consultas que funcionem em diversos bancos de dados o framework fornececomponentes, dentro do namespace Zend\Db\Sql.Vamos ver alguns exemplos.O primeiro passo que precisamos fazer indicar o uso do componente:

    1 use Zend\Db\Sql\Sql;

    A construo do objeto Sql depende de uma conexo com o banco de dados. Para isso vamos usar aconexo configurada no projeto:

    1 $adapter = $this->getServiceLocator()->get('DbAdapter');

    Obs: veremos com detalhes o que significa o getServiceLocator() no captulo sobre servios. Por enquantonos basta saber que essa linha de cdigo retorna a conexo atual com o banco de dados.Consulta simples, como um SELECT * FROM posts

    1 $sql = new Sql($adapter);2 $select = $sql->select()->from('posts');3 $statement = $sql->prepareStatementForSqlObject($select);4 $results = $statement->execute();5 foreach ($results as $r) {6 echo $r['id'], ' - ', $r['title'], '';7 }

    Consulta com clausula WHERE, como um SELECT * FROM posts WHERE id = 10:

  • Modelos 28

    1 $sql = new Sql($adapter);2 $select = $sql->select()3 ->from('posts')4 ->where(array('id' => 10));56 $statement = $sql->prepareStatementForSqlObject($select);7 $results = $statement->execute();8 foreach ($results as $r) {9 echo $r['id'], ' - ', $r['title'], '';10 }

    Consulta com seleo de campos e clusulaWHERE, como um SELECT id, title FROMpostsWHEREid > 10:Neste caso podemos usar de duas formas:

    1 $sql = new Sql($adapter);2 $select = $sql->select()3 ->columns(array('id','title'))4 ->from('posts')5 ->where('id > 10');6 $statement = $sql->prepareStatementForSqlObject($select);7 $results = $statement->execute();8 foreach ($results as $r) {9 echo $r['id'], ' - ', $r['title'], '';10 }

    Ou usando o componente Predicate. Primeiro precisamos importar o componente:

    1 use Zend\Db\Sql\Predicate\Predicate;

    E usamos da seguinte forma:

    1 $sql = new Sql($adapter);2 $select = $sql->select()3 ->columns(array('id','title'))4 ->from('posts');56 $where = new Predicate();7 $where->greaterThan('id',10);8 $select->getRawState($select::WHERE)->addPredicate($where);9 $statement = $sql->prepareStatementForSqlObject($select);10 $results = $statement->execute();11 foreach ($results as $r) {12 echo $r['id'], ' - ', $r['title'], '';13 }

    Consulta com seleo de campos e clusula WHERE e INNER JOIN, como um SELECT posts. id,posts. title, comments. description, comments.name FROM posts INNER JOIN comments ONcomments.post_id = post.id WHERE posts.id > 10 order by title:

  • Modelos 29

    1 $sql = new Sql($adapter);2 $select = $sql->select();3 $select->columns(array('id','title'))4 ->from('posts')5 ->join(6 'comments',7 'comments.post_id = posts.id',8 array('description'),9 $select::JOIN_INNER10 )11 ->order(array('title'));1213 $where = new Predicate();14 $where->greaterThan('posts.id',10);1516 $select->getRawState($select::WHERE)->addPredicate($where);1718 $statement = $sql->prepareStatementForSqlObject($select);19 $results = $statement->execute();20 foreach ($results as $r) {21 echo $r['id'], ' - ', $r['title'], '-', $r['name'], ' - ', $r['description\22 '] . '';23 }

    Em qualquer momento possvel imprimir a consulta que foi gerada, o que pode ser til para fins dedebug:

    1 echo $select->getSqlString();

    Mais detalhes no manual do frameworkVamos agora trabalhar com a prxima camada, os controladores.

  • ControladoresOs controladores so responsveis pela interao com o usurio, interceptando as invocaes por urls,cliques em links, etc. O controlador ir receber a ao do usurio, executar algum servio, manipularalguma entidade e, geralmente, renderizar uma tela da camada de viso, para gerar uma resposta para ousurio.O primeiro controlador que iremos criar o responsvel por mostrar todos os posts de nossa tabela, paraque o usurio os visualize.Para criar um controlador precisamos escrever uma classe que implemente a interface Dispatchable. OZend Framework fornece algumas classes que implementam esta interface, facilitando o nosso uso, como aAbstractActionController e aAbstractRestfulController. No nosso exemplo vamos estender uma classe queconsta no mdulo Core a Core\Controller\ActionController que ir nos fornecer algumas funcionalidadesadicionais, como o mtodo getTable() que foi comentado no captulo anterior.

    Criando os testes

    Novamente iniciaremos o processo criando o teste para o nosso controlador. Assim podemos pensar emsuas funcionalidades antes de iniciarmos a codificao. Primeiro precisamos criar o diretrio de testes doscontroladores:

    1 mkdir module/Application/tests/src/Application/Controller

    Vamos agora criar o arquivo de testes, no arquivomodule/Application/tests/src/Application/Controller/In-dexControllerTest.php

    1

  • Controladores 31

    22 /**23 * Nome da rota. Geralmente o nome do modulo24 * @var string25 */26 protected $controllerRoute = 'application';2728 /**29 * Testa o acesso a uma action que nao existe30 */31 public function test404()32 {33 $this->routeMatch->setParam('action', 'action_nao_existente');34 $result = $this->controller->dispatch($this->request);35 $response = $this->controller->getResponse();36 $this->assertEquals(404, $response->getStatusCode());37 }3839 /**40 * Testa a pagina inicial, que deve mostrar os posts41 */42 public function testIndexAction()43 {44 // Cria posts para testar45 $postA = $this->addPost();46 $postB = $this->addPost();4748 // Invoca a rota index49 $this->routeMatch->setParam('action', 'index');50 $result = $this->controller->dispatch(51 $this->request, $this->response52 );5354 // Verifica o response55 $response = $this->controller->getResponse();56 $this->assertEquals(200, $response->getStatusCode());5758 // Testa se um ViewModel foi retornado59 $this->assertInstanceOf(60 'Zend\View\Model\ViewModel', $result61 );6263 // Testa os dados da view64 $variables = $result->getVariables();65 $this->assertArrayHasKey('posts', $variables);6667 // Faz a comparao dos dados68 $controllerData = $variables["posts"];69 $this->assertEquals(70 $postA->title, $controllerData[0]['title']

  • Controladores 32

    71 );72 $this->assertEquals(73 $postB->title, $controllerData[1]['title']74 );75 }7677 /**78 * Adiciona um post para os testes79 */80 private function addPost()81 {82 $post = new Post();83 $post->title = 'Apple compra a Coderockr';84 $post->description = 'A Apple compra a Coderockr ';85 $post->post_date = date('Y-m-d H:i:s');86 $saved = $this->getTable('Application\Model\Post')->save($post);87 return $saved;88 }89 }

    https://gist.github.com/4012000Vamos aos detalhes do cdigo.

    1 /**2 * @group Controller3 */

    Similar aos testes de entidades, indicamos que este teste pertence a um grupo: o Controller. Assimpodemos executar somente os testes deste grupo caso necessrio.

    1 class IndexControllerTest extends ControllerTestCase

    Todos os testes de controladores devem estender a classe ControllerTestCase.

    1 protected $controllerFQDN = 'Application\Controller\IndexController';

    Namespace do controlador. necessria para os testes.

    1 protected $controllerRoute = 'application';

    Nome da rota a ser usada. Geralmente o mesmo nome do mdulo, com letras minsculas. Voltaremosa falar sobre as rotas no captulo de criao de novos mdulos.

  • Controladores 33

    1 public function test404()

    Este teste verifica o que acontece caso o usurio tente acessar uma action no existente.Vamos abrir um pequeno parnteses aqui, e comentar sobre controladores e actions. Geralmente oscontroladores so classes com o nome terminando em Controller como o IndexController (apesar dissono ser mais obrigatrio no Zend Framework 2 ainda continua se usando esse padro). Cada controladorpossui uma ou mais actions que so mtodos pblicos cujo nome termina comAction como o indexAction.As actions so as aes que os usurios podem acessar via URL, links ou botes na tela. Por exemplo, casoo usurio acesse a url:

    1 http://zf2napratica.dev/application/index/index/id/1

    Isto traduzido pelo framework usando o padro:

    1 http://servidor/modulo/controller/action/parametro/valor

    Ento:

    Servidor = zf2napratica.dev Mdulo = Application Controller = IndexController.php Action = indexAction (dentro do arquivo IndexController.php) Parmetro = id Valor = 1

    Caso o usurio tente acessar uma action que no existe um erro gerado, exatamente o que o teste test404()verifica.

    1 public function testIndexAction()

    Este o teste mais importante. Primeiro criamos dois registros na tabela Post (addPost), depois simulamoso acesso a action index ($this->routeMatch->setParam(action, index);). O comando dispatch executa oacesso efetivamente, como se fosse um usurio acessando a URL no navegador. Aps verificar se a pginafoi encontrada (statusCode 200) verificamos se recebemos uma instncia de ViewModel e se ela possui osdados dos posts. A ViewModel a representao em forma de objeto da nossa camada de viso, que serrenderizada na forma de HTML no navegador. Iremos criar a viso ainda neste captulo.Agora podemos executar nossos testes novamente. Podemos executar somente os testes do grupoController para que o teste seja executado mais rapidamente:

    1 phpunit -c module/Application/tests/phpunit.xml --group=Controller

    Os testes iro falhar pois ainda no criamos o controlador e sua viso.

  • Controladores 34

    Criando o controlador

    Vamos criar o controlador, no arquivomodule/Application/src/Application/Controller/IndexController.php:

    1

  • Controladores 35

    mais detalhes sobre os ViewHelpers em um prximo captulo.O contedo do arquivo index.phtml :

    1 2 3 4 Criar Post5 6 7 8 Lista de Posts9 10 11 12 Ttulo13 Texto14 Data de Cadastro15 Opes16 17 18 19 20 21 22 23 24 32 33 34

  • Controladores 36

    47 48

    https://gist.github.com/4012005Alguns pontos importantes do index.phtml:

    1

    A varivel posts a mesma que foi enviada pelo controlador, ou seja, a lista de registros vindos da tabelapost.

    1

    Este um exemplo de uso de um ViewHelper, o escapeHtml, que limpa quaisquer tags HTML que possamexistir no texto. O mesmo vale para o dateFormat que faz a formatao das datas.

    1 2 Criar Post3

    Estamos criando links para acessar um outro controlador, que criaremos no prximo captulo, oIndexController do mdulo Admin.

    Layouts

    Neste momento cabe explicar um novo conceito: layouts. O Zend Framework trabalha com a idiade layouts, que so molduras ou pginas padro mescladas com as vises dos controladores, como aindex.phtml.Usando uma imagem para ilustrar um exemplo:

  • Controladores 37

    Wireframe mostrando o layout

    Em todas as pginas teremos um ttulo, uma imagem e um rodap, com informaes sobre o autor,copyright, etc. A nica informao que mudar o contedo das pginas, como o index.phtml queacabamos de criar, mas o cabealho e o rodap permanecem os mesmos.Podemos ter vrios layouts e usarmos o mais indicado em cada controlador ou action. O cdigo do layoutpadro do Zend Framework 2 est no arquivo module/Application/view/layout/layout.phtml:

    1 2 3 4 5 11 13 14

  • Controladores 38

    20 ico'21 )22 )23 ->prependStylesheet($this->basePath() . '/css/bootstrap-r\24 esponsive.min.css')25 ->prependStylesheet($this->basePath() . '/css/style.css')26 ->prependStylesheet($this->basePath() . '/css/bootstrap.m\27 in.css')28 ?>2930 31 41 42 43 44 45 46 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 Entrar63 64 65 66 67 68

  • Controladores 39

    69 70 71 72 73 2005 - 2012 by Zend Technologies Ltd.74 75 76 77 78 79 80

    https://gist.github.com/4012012Este arquivo faz uso de diversos ViewHelpers que no iremos ver agora, como o headLink ou o translate,e possui uma estrutura bem complexa de design e CSS. Mas a parte mais importante para ns agora alinha:

    1

    nesse ponto que a viso ser renderizada no layout, ou seja, o contedo do index.phtml vai ser mescladocom o restante. Como todos os controladores usam esse layout, por padro esse comportamento ser igualem todos, a menos que o desenvolvedor altere a configurao de algum deles. O layout pode ser muitosimples, apenas precisa constar essa linha, que imprime o $this->content.

    Executando os testes novamente

    Executando os testes novamente todos devem passar. Caso algum falhe preciso revisar os cdigos econfiguraes que vimos nesse captulo.No prximo captulo vamos incrementar nosso controlador com um paginador.

    Desafio

    Neste captulo vimos o teste e o controlador para manipular a entidade Post. Como desafio deixo aoleitor a incluso de uma nova funcionalidade: a listagem de comentrios do post. Lembre-se de alteraro IndexControllerTest para incluir o novo teste que ir verificar a existncia dos comentrios, alterar oIndexController e tambm a view para mostr-los.Os cdigos desse desafio esto disponveis no repositrio do Github:https://github.com/eminetto/zf2napraticaVoc tambm pode contribuir com exemplos fazendo um fork do repositrio e colocando sua soluo.

  • PaginadorA indexAction que criamos no IndexController possui um problema srio. Atualmente ela faz a buscade todos os registros da tabela post e simplesmente as envia para a ViewModel renderizar. Mas o queacontece se tivermos milhares de registros na tabela? Um srio problema de performance, pois o usurioir esperar por vrios minutos at a pgina ser renderizada (isso se o servidor no cortar a transmissoantes). Para resolver esse problema usaremos o componente Zend\Paginator.O Paginator facilita a paginao da informao e foi desenvolvido com os seguintes princpios:

    Paginar qualquer tipo de dados, no apenas o resultado de banco de dados. Podemos paginar arraysou classes que implementem a interface Iterator do SPL.

    Recuperar apenas os resultados necessrios para a visualizao, aumentando a performance. Ser independente dos outros componentes do framework para facilitar ao desenvolvedor us-loinclusive em outros projetos.

    Adicionando o teste do paginador

    Vamos adicionar um novo teste ao nosso arquivomodule/Application/tests/src/Application/Controller/In-dexControllerTest.php e fazer uma pequena alterao no testIndexAction():

    1 /**2 * Testa a pagina inicial, que deve mostrar os posts3 */4 public function testIndexAction()5 {6 // Cria posts para testar7 $postA = $this->addPost();8 $postB = $this->addPost();910 // Invoca a rota index11 $this->routeMatch->setParam('action', 'index');12 $result = $this->controller->dispatch($this->request, $this->response);1314 // Verifica o response15 $response = $this->controller->getResponse();16 $this->assertEquals(200, $response->getStatusCode());1718 // Testa se um ViewModel foi retornado19 $this->assertInstanceOf('Zend\View\Model\ViewModel', $result);2021 // Testa os dados da view22 $variables = $result->getVariables();23 $this->assertArrayHasKey('posts', $variables);2425 // Faz a comparao dos dados26 //mudamos a linha abaixo27 $controllerData = $variables["posts"]->getCurrentItems()->toArray();

    40

  • Paginador 41

    28 $this->assertEquals($postA->title, $controllerData[0]['title']);29 $this->assertEquals($postB->title, $controllerData[1]['title']);30 }3132 /**33 * Testa a pagina inicial, que deve mostrar os posts com paginador34 */35 public function testIndexActionPaginator()36 {37 // Cria posts para testar38 $post = array();39 for($i=0; $i< 25; $i++) {40 $post[] = $this->addPost();41 }4243 // Invoca a rota index44 $this->routeMatch->setParam('action', 'index');45 $result = $this->controller->dispatch($this->request, $this->response);4647 // Verifica o response48 $response = $this->controller->getResponse();49 $this->assertEquals(200, $response->getStatusCode());5051 // Testa se um ViewModel foi retornado52 $this->assertInstanceOf('Zend\View\Model\ViewModel', $result);5354 // Testa os dados da view55 $variables = $result->getVariables();5657 $this->assertArrayHasKey('posts', $variables);5859 //testa o paginator60 $paginator = $variables["posts"];61 $this->assertEquals(62 'Zend\Paginator\Paginator', get_class($paginator)63 );64 $posts = $paginator->getCurrentItems()->toArray();65 $this->assertEquals(10, count($posts));66 $this->assertEquals($post[0]->id, $posts[0]['id']);67 $this->assertEquals($post[1]->id, $posts[1]['id']);6869 //testa a terceira pagina da paginacao70 $this->routeMatch->setParam('action', 'index');71 $this->routeMatch->setParam('page', 3);72 $result = $this->controller73 ->dispatch($this->request, $this->response);74 $variables = $result->getVariables();75 $controllerData = $variables["posts"]->getCurrentItems()->toArray();76 $this->assertEquals(5, count($controllerData));

  • Paginador 42

    77 }

    https://gist.github.com/4012024Alm de testarmos se a ViewModel agora possui um objeto do tipo Zend\Paginator\Paginator verificamosse foi mostrado apenas 10 registros por pgina (o valor padro do paginador) e o que acontece quandoacessamos a pgina 3 ($this->routeMatch->setParam(page, 3);), o que seria equivalente a acessar a URL:

    1 http://zf2napratica.dev/application/index/index/page/3

    Se executarmos os testes agora eles devem falhar, pois ainda no adicionamos o paginador no nossocontrolador.

    Adicionando o paginador no IndexController

    Precisamos incluir novos namespaces no incio do arquivo, para usarmos o paginador:

    1 use Zend\Paginator\Paginator;2 use Zend\Paginator\Adapter\DbSelect as PaginatorDbSelectAdapter;

    Agora vamos alterar a indexAction:

    1 /**2 * Mostra os posts cadastrados3 * @return void4 */5 public function indexAction()6 {7 $post = $this->getTable('Application\Model\Post');8 $sql = $post->getSql();9 $select = $sql->select();1011 $paginatorAdapter = new PaginatorDbSelectAdapter($select, $sql);12 $paginator = new Paginator($paginatorAdapter);13 $paginator->setCurrentPageNumber($this->params()->fromRoute('page'));1415 return new ViewModel(array(16 'posts' => $paginator17 ));18 }

    https://gist.github.com/4012025Vamos aos detalhes do cdigo.

  • Paginador 43

    1 $sql = $post->getSql();2 $select = $sql->select();

    Como vamos paginar informaes vindas do banco de dados precisamos de um objeto select e um sql,que so necessrios para o paginador funcionar. Felizmente o TableGateway fornece esses componentes,o que facilita o acesso.

    1 $paginatorAdapter = new PaginatorDbSelectAdapter($select, $sql);2 $paginator = new Paginator($paginatorAdapter);

    Conforme comentado no incio do captulo, o paginador genrico, ento precisamos passar comoparmetro o que iremos paginar, nesse caso um DbSelect (que eu apelidei de PaginatorDbSelectAdapterna importao do namespace).

    1 $paginator->setCurrentPageNumber($this->params()->fromRoute('page'));

    Essa configurao diz ao paginador em qual pgina ele se encontra. Caso no seja indicada nenhumapgina pela URL (page/3 por exemplo) a primeira pgina apresentada.Precisamos agora alterar a viso para que ela mostre o paginador.Adicionamos o cdigo abaixo no arquivo module/Application/view/application/index/index.phtml:

    1

    Estamos imprimindo o controlador de paginadores e passando como parmetros o paginador ($posts), oformato (Sliding, similar ao paginador da pgina de busca do Google) e qual o trecho de cdigo quecontm os links (prxima pgina, pgina anterior, etc).O contedo do terceiro parmetro algo novo para ns.

    Partials

    No terceiro parmetro da chamada ao paginationControl indicamos o partial onde consta o cdigo doslinks do paginador. Partials so pores de cdigo que podemos salvar e usar em diversas vises e layouts.Isso ajuda bastante a organizao de cdigo, pois podemos compartilhar um trecho deHTML ou PHP entrediversas vises.Precisamos criar o diretrio de partials do mdulo:

    1 mkdir module/Application/view/partials

    E criar o diretrio para armazenar os partials do paginador:

  • Paginador 44

    1 mkdir module/Application/view/partials/paginator

    O cdigo do arquivo mkdir module/Application/view/partials/paginator/control.phtml :

    1 2 3 4

  • Paginador 45

    46 >47 48 49 50

  • MdulosNosso projeto de blog precisa agora de outra funcionalidade importante: a criao dos posts. Mas noqueremos que qualquer visitante possa alterar ou remover posts, apenas os usurios com permisso paratal. Uma forma de fazermos isso e mantermos o cdigo organizado criando um novo mdulo, o Admin.O conceito de mdulos j existia nas verses anteriores do framework, mas no Zend Framework 2ele ganhou uma importncia maior. Mdulos so agrupamentos de cdigos que podem ser facilmenteincludos em qualquer projeto, sendo totalmente desacoplados. Podemos criar aqui um mdulo Admintotalmente genrico e us-lo em qualquer projeto que criarmos no futuro, como um CMS ou um frum.Existem diversos mdulos prontos, criados pela comunidade de desenvolvedores e distribudos gratuita-mente no site http://modules.zendframework.com.Vamos criar o nosso mdulo Admin usando como base o mdulo Skel, para agilizar o processo.O primeiro passo duplicar o diretrio module/Skel criando o diretrio module/Admin. No Linux/Mac:

    1 cp -r module/Skel/ module/Admin

    Configurando o novo mdulo

    Passos:

    Alterar o namespace no arquivomodule/Admin/Module.php:

    1 namespace Admin;

    Alterar omodule/Admin/config/module.config.php

    1

  • Mdulos 47

    20 'module' => 'admin'21 ),22 ),23 'may_terminate' => true,24 'child_routes' => array(25 'default' => array(26 'type' => 'Segment',27 'options' => array(28 'route' => '/[:controller[/:action]]',29 'constraints' => array(30 'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',31 'action' => '[a-zA-Z][a-zA-Z0-9_-]*\32 ',33 ),34 'defaults' => array(),35 ),36 //permite mandar dados pela url37 'child_routes' => array(38 'wildcard' => array(39 'type' => 'Wildcard'40 ),41 ),42 ),43 ),44 ),45 ),46 ),47 'view_manager' => array(48 //the module can have a specific layout49 /* 'template_map' => array(50 'layout/layout' => __DIR__ . '/../view/layout/layout.phtml',51 ),*/52 'template_path_stack' => array(53 'admin' => __DIR__ . '/../view',54 ),55 ),56 //module can have a specific db configuration57 /* 'db' => array(58 'driver' => 'PDO_SQLite',59 'dsn' => 'sqlite:' . __DIR__ .'/../data/admin.db',60 'driver_options' => array(61 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION62 )63 )*/64 );

    https://gist.github.com/4012030O arquivo module.config.php o responsvel por configurar todo o comportamento do mdulo.

  • Mdulos 48

    No Zend Framework 1 existia muita mgica, muito comportamento que era automaticamente executado.Isso acarretavamenos cdigo para o desenvolvedor, mas retirava um pouco da performance e flexibilidade.ComoZend Framework 2 as coisas so bemmais explcitas, praticamente todo o comportamento passivelde alterao e modificao. Isso aumentou o nmero e a complexidade dos arquivos de configurao masdeu um grande aumento de performance e flexibilidade.O module.config.php um bom exemplo dsso. Como podemos ver no arquivo acima praticamente tudo configurado, desde quais so os controladores disponveis (array controllers) at a rota (array router). Issonos trouxe a flexibilidade de podermos ter um layout e vises especficas para o mdulo (array view_-manager) e at uma conexo diferente com o banco de dados (array db).Voltaremos a alterar o module.config.php nos prximos tpicos, para incluir um novo controlador porexemplo. Mas alguns pontos dificilmente precisaremos alterar, notadamente o array router pois aconfigurao genrica o suficiente para a grande maioria dos casos.

    Configurar diretrios

    Podemos remover o diretrio Skel dentro do src e criar um novo diretrio chamado src/Admin com seussub-diretrios:

    1 rm -rf module/Admin/src/Skel2 mkdir module/Admin/src/Admin3 mkdir module/Admin/src/Admin/Controller4 mkdir module/Admin/src/Admin/Model5 mkdir module/Admin/src/Admin/Service

    Faremos o mesmo com o diretrio tests:

    1 rm -rf module/Admin/tests/src/Skel2 mkdir module/Admin/tests/src/Admin3 mkdir module/Admin/tests/src/Admin/Controller4 mkdir module/Admin/tests/src/Admin/Model5 mkdir module/Admin/tests/src/Admin/Service

    Conforme comentado no primeiro captulo, o framework ainda no possui uma ferramenta para facilitar acriao desta estrutura de diretrios e nomomento o indicado usarmos estes projetos Skel comomodelos.

    Alterar as configuraes do tests/Bootstrap.php

    1 static function getModulePath()2 {3 //mudar o caminho do modulo4 return __DIR__ . '/../../../module/Admin';5 }

    Adicionar o mdulo no application.config.php

    Para cada mdulo que criarmos no projeto sempre precisaremos inclu-lo no application.config.php paraque ele esteja disponvel aos usurios:

  • Mdulos 49

    1 'modules' => array(2 'Application',3 'Core',4 //'Skel',5 'Admin'6 ),

    Temos agora trs mdulos no sistema, dois deles independentes entre si (o Application e o Admin), sendoque ambos dependem apenas do mdulo Core.

    Configurar os dados para os testes

    Precisamos agora configurar os dados para os testes do mdulo Admin. O arquivo module/Admin/data/-test.data.php vai ter os comandos SQL do arquivo do Application mais o comando de criao da tabelauser. Essa duplicao de informao pode ser resolvida com mudanas na estrutura dos testes, mas eudeixei desta forma para ser mais didtico e no aumentar a complexidade. Recomendo a leitura avanadada documentao do PHPUnit antes de implementar essa estrutura em projetos maiores, principalmentea documentao sobre Fixtures e Mocks.O contedo do arquivo module/Admin/data/test.data.php:

    1

  • Mdulos 50

    29 ON UPDATE NO ACTION)30 ENGINE = InnoDB;',31 'drop' =>'drop table comments;'32 ),33 'users' => array(34 'create' => 'CREATE TABLE if not exists users (35 id INT NOT NULL AUTO_INCREMENT ,36 username VARCHAR(200) NOT NULL ,37 password VARCHAR(250) NOT NULL ,38 name VARCHAR(200) NULL ,39 valid TINYINT NULL ,40 role VARCHAR(20) NULL ,41 PRIMARY KEY (id) )42 ENGINE = InnoDB;' ,43 'drop' => 'drop table users;',44 ),45 );

    https://gist.github.com/4012031

    Criando a entidade User

    Vamos agora criar o teste e o cdigo da entidade User que vai ser usada pelo novo mdulo.Criamos o arquivo module/Admin/tests/src/Admin/Model/UserTest.php:

    1

  • Mdulos 51

    23 * @depends testGetInputFilter24 */25 public function testInputFilterValid($if)26 {27 //testa os filtros28 $this->assertEquals(6, $if->count());29 $this->assertTrue(30 $if->has('id')31 );32 $this->assertTrue(33 $if->has('username')34 );35 $this->assertTrue(36 $if->has('password')37 );38 $this->assertTrue(39 $if->has('name')40 );41 $this->assertTrue(42 $if->has('valid')43 );44 $this->assertTrue(45 $if->has('role')46 );47 }4849 /**50 * @expectedException Core\Model\EntityException51 */52 public function testInputFilterInvalidoUsername()53 {54 //testa se os filtros estao funcionando55 $user = new User();56 //username so pode ter 50 caracteres57 $user->username = 'Lorem Ipsum e simplesmente uma simulacao de text\58 o da industria tipografica e de impressos. Lorem Ipsum e simplesmente uma s\59 imulacao de texto da indstria tipografica e de impressos';60 }6162 /**63 * @expectedException Core\Model\EntityException64 */65 public function testInputFilterInvalidoRole()66 {67 //testa se os filtros estao funcionando68 $user = new User();69 //role s pode ter 20 caracteres70 $user->role = 'Lorem Ipsum e simplesmente uma simulacao de texto da\71 industria tipografica e de impressos. Lorem Ipsum e simplesmente uma simul\

  • Mdulos 52

    72 acao de texto da indstria tipografica e de impressos';73 }7475 /**76 * Teste de insercao de um user valido77 */78 public function testInsert()79 {80 $user = $this->addUser();81 //testa o filtro de tags e espaos82 $this->assertEquals('Steve Jobs', $user->name);83 //testa o auto increment da chave primaria84 $this->assertEquals(1, $user->id);85 }8687 /**88 * @expectedException Core\Model\EntityException89 * @expectedExceptionMessage Input invlido: username =90 */91 public function testInsertInvalido()92 {93 $user = new user();94 $user->name = 'teste';95 $user->username = '';9697 $saved = $this->getTable('Admin\Model\user')->save($user);98 }99100 public function testUpdate()101 {102 $tableGateway = $this->getTable('Admin\Model\User');103 $user = $this->addUser();104105 $id = $user->id;106107 $this->assertEquals(1, $id);108109 $user = $tableGateway->get($id);110 $this->assertEquals('Steve Jobs', $user->name);111112 $user->name = 'Bill Gates';113 $updated = $tableGateway->save($user);114115 $user = $tableGateway->get($id);116 $this->assertEquals('Bill Gates', $user->name);117 }118119 /**120 * @expectedException Core\Model\EntityException

  • Mdulos 53

    121 * @expectedExceptionMessage Could not find row 1122 */123 public function testDelete()124 {125 $tableGateway = $this->getTable('Admin\Model\User');126 $user = $this->addUser();127 $id = $user->id;128 $deleted = $tableGateway->delete($id);129 $this->assertEquals(1, $deleted); //numero de linhas excluidas130 $user = $tableGateway->get($id);131 }132133 private function addUser()134 {135 $user = new User();136 $user->username = 'steve';137 $user->password = md5('apple');138 $user->name = 'Steve Jobs';139 $user->valid = 1;140 $user->role = 'admin';141142 $saved = $this->getTable('Admin\Model\User')->save($user);143 return $saved;144 }145 }

    https://gist.github.com/4012034O cdigo similar aos testes das entidades anteriores.Vamos criar agora o cdigo da entidade, no arquivo module/Admin/src/Admin/Model/User.php:

    1

  • Mdulos 54

    19 * Nome da tabela. Campo obrigatrio20 * @var string21 */22 protected $tableName ='users';2324 /**25 * @var int26 */27 protected $id;2829 /**30 * @var string31 */32 protected $username;3334 /**35 * @var string36 */37 protected $password;3839 /**40 * @var string41 */42 protected $name;4344 /**45 * @var int46 */47 protected $valid;4849 /**50 * @var string51 */52 protected $role;5354 /**55 * Configura os filtros dos campos da entidade56 *57 * @return Zend\InputFilter\InputFilter58 */59 public function getInputFilter()60 {61 if (!$this->inputFilter) {62 $inputFilter = new InputFilter();63 $factory = new InputFactory();6465 $inputFilter->add($factory->createInput(array(66 'name' => 'id',67 'required' => true,

  • Mdulos 55

    68 'filters' => array(69 array('name' => 'Int'),70 ),71 )));7273 $inputFilter->add($factory->createInput(array(74 'name' => 'username',75 'required' => true,76 'filters' => array(77 array('name' => 'StripTags'),78 array('name' => 'StringTrim'),79 ),80 'validators' => array(81 array(82 'name' => 'StringLength',83 'options' => array(84 'encoding' => 'UTF-8',85 'min' => 1,86 'max' => 50,87 ),88 ),89 ),90 )));9192 $inputFilter->add($factory->createInput(array(93 'name' => 'password',94 'required' => true,95 'filters' => array(96 array('name' => 'StripTags'),97 array('name' => 'StringTrim'),98 ),99 )));100101 $inputFilter->add($factory->createInput(array(102 'name' => 'name',103 'required' => true,104 'filters' => array(105 array('name' => 'StripTags'),106 array('name' => 'StringTrim'),107 ),108 )));109110 $inputFilter->add($factory->createInput(array(111 'name' => 'valid',112 'required' => true,113 'filters' => array(114 array('name' => 'Int'),115 ),116 )));

  • Mdulos 56

    117118 $inputFilter->add($factory->createInput(array(119 'name' => 'role',120 'required' => true,121 'filters' => array(122 array('name' => 'StripTags'),123 array('name' => 'StringTrim'),124 ),125 'validators' => array(126 array(127 'name' => 'StringLength',128 'options' => array(129 'encoding' => 'UTF-8',130 'min' => 1,131 'max' => 20,132 ),133 ),134 ),135 )));136137 $this->inputFilter = $inputFilter;138 }139140 return $this->inputFilter;141 }142 }

    https://gist.github.com/4001782Podemos agora executar o testes do grupo Model:

    1 phpunit -c module/Admin/tests/phpunit.xml --group=Model

    Os testes devem passar. Caso algum falhe preciso revisar os cdigos e testes.Vamos agora trabalhar na prxima funcionalidade importante, a autenticao.

  • ServiosAgora que temos um mdulo administrativo precisamos identificar alguns usurios como membrosvlidos da administrao do nosso blog, ou seja, precisamos de alguma forma de autenticao. Paraisso vamos usar dois novos conceitos: o componente Zend\Authentication e os servios.O Zend\Authentication um componente usado para autenticar os usurios de forma fcil e flexvel.Podemos armazenar as credenciais dos usurios em vrios modos, como banco de dados, LDAP, HTTP,etc, bastando escolher o adapter mais adequado. Ele tambm nos ajuda a manter as credenciais do usurioentre as pginas da nossa aplicao, o que vai ser muito til para ns.Quanto ao conceito de servios. Nosso projeto atual possui apenas uma interface de gerenciamento webcom a qual os usurios podem fazer a autenticao e postar novos textos. Poderamos ento colocara lgica da autenticao em um controlador. Mas podemos no futuro precisar de uma outra interface,mobile por exemplo, e iremos precisar da mesma lgica de autenticao. A melhor forma de fazermosisso criarmos um servio, uma poro de cdigo que pode ser facilmente usado por um controlador,outro servio ou, mais tarde, uma API. O Zend Framework 2 usa intensamente o conceito de servios efacilita a criao dos mesmos.

    Servio de autenticao

    Vamos ento criar um servio chamado Auth que vai ser responsvel pela autenticao dos usurios e,mais tarde, a autorizao, verificando se determinado usurio pode ou no acessar uma action.Iniciamos pelos testes, no arquivo module/Admin/tests/src/Admin/Service/AuthTest.php:

    1

  • Servios 58

    24 * Authenticao sem parmetros25 * @expectedException \Exception26 * @return void27 */28 public function testAuthenticateWithoutParams()29 {30 $authService = $this->serviceManager->get('Admin\Service\Auth');3132 $authService->authenticate();33 }3435 /**36 * Authenticao sem parmetros37 * @expectedException \Exception38 * @expectedExceptionMessage Parmetros invlidos39 * @return void40 */41 public function testAuthenticateEmptyParams()42 {43 $authService = $this->serviceManager->get('Admin\Service\Auth');44 $authService->authenticate(array());45 }4647 /**48 * Teste da autenticao invlida49 * @expectedException \Exception50 * @expectedExceptionMessage Login ou senha invlidos51 * @return void52 */53 public function testAuthenticateInvalidParameters()54 {55 $authService = $this->serviceManager->get('Admin\Service\Auth');56 $authService->authenticate(array(57 'username' => 'invalid', 'password' => 'invalid')58 );59 }6061 /**62 * Teste da autenticao Invlida63 * @expectedException \Exception64 * @expectedExceptionMessage Login ou senha invlidos65 * @return void66 */67 public function testAuthenticateInvalidPassord()68 {69 $authService = $this->serviceManager->get('Admin\Service\Auth');70 $user = $this->addUser();7172 $authService->authenticate(array(

  • Servios 59

    73 'username' => $user->username, 'password' => 'invalida')74 );75 }7677 /**78 * Teste da autenticao Vlida79 * @return void80 */81 public function testAuthenticateValidParams()82 {83 $authService = $this->serviceManager->get('Admin\Service\Auth');84 $user = $this->addUser();8586 $result = $authService->authenticate(87 array('username' => $user->username, 'password' => 'apple')88 );89 $this->assertTrue($result);9091 //testar a se a authenticao foi criada92 $auth = new AuthenticationService();93 $this->assertEquals($auth->getIdentity(), $user->username);9495 //verica se o usurio foi salvo na sesso96 $session = $this->serviceManager->get('Session');97 $savedUser = $session->offsetGet('user');98 $this->assertEquals($user->id, $savedUser->id);99 }100101 /**102 * Limpa a autenticao depois de cada teste103 * @return void104 */105 public function tearDown()106 {107 parent::tearDown();108 $auth = new AuthenticationService();109 $auth->clearIdentity();110 }111112 /**113 Teste do logout114 * @return void115 */116 public function testLogout()117 {118 $authService = $this->serviceManager->get('Admin\Service\Auth');119 $user = $this->addUser();120121 $result = $authService->authenticate(

  • Servios 60

    122 array('username' => $user->username, 'password' => 'apple')123 );124 $this->assertTrue($result);125126 $result = $authService->logout();127 $this->assertTrue($result);128129 //verifica se removeu a identidade da autenticao130 $auth = new AuthenticationService();131 $this->assertNull($auth->getIdentity());132133 //verifica se o usurio foi removido da sesso134 $session = $this->serviceManager->get('Session');135 $savedUser = $session->offsetGet('user');136 $this->assertNull($savedUser);137 }138139 private function addUser()140 {141 $user = new User();142 $user->username = 'steve';143 $user->password = md5('apple');144 $user->name = 'Steve Jobs';145 $user->valid = 1;146 $user->role = 'admin';147148 $saved = $this->getTable('Admin\Model\User')->save($user);149 return $saved;150 }151 }

    https://gist.github.com/4012038Vamos analisar o que temos de novo no cdigo acima.

    1 $authService = $this->serviceManager->get('Admin\Service\Auth');

    Estamos usando o componente ServiceManager do Zend Framework 2 para instanciar nosso servio deautenticao (e a Session). Vou explicar sobre ele daqui a pouco, quando criarmos o cdigo do servio.

    1 public function tearDown()

    Essa uma funo do prprio PHPUnit que estamos estendendo aqui. Ela executada sempre aps todosos testes e ns estamos garantindo que a identidade do usurio seja removida para evitar problemas comoutros testes.Vamos agora criar o cdigo do nosso servio e explicar os novos conceitos envolvidos no cdigo. O arquivomodule/Admin/src/Admin/Service/Auth.php:

  • Servios 61

    1

  • Servios 62

    50 ->setTableName('users')51 ->setIdentityColumn('username')52 ->setCredentialColumn('password')53 ->setIdentity($params['username'])54 ->setCredential($password);55 $result = $auth->authenticate($authAdapter);5657 if (! $result->isValid()) {58 throw new \Exception("Login ou senha invlidos");59 }6061 //salva o user na sesso62 $session = $this->getServiceManager()->get('Session');63 $session->offsetSet('user', $authAdapter->getResultRowObject());6465 return true;66 }6768 /**69 * Faz o logout do sistema70 *71 * @return void72 */73 public function logout() {74 $auth = new AuthenticationService();75 $session = $this->getServiceManager()->get('Session');76 $session->offsetUnset('user');77 $auth->clearIdentity();78 return true;79 }80 }

    https://gist.github.com/4012039Vamos aos detalhes:

    1 class Auth extends Service

    Para que uma classe seja considerada um servio preciso que ela implemente a interface ServiceMana-gerAwareInterface. A classe Core\Service faz essa implementao, por isso nossos servios vo estend-la.Outra vantagem dos servios herdarem a mesma classe a possibilidade que isso nos fornece de expansode regras no futuro.

    1 public function __construct($dbAdapter = null)

    A instanciao do servio precisa de uma conexo com o banco de dados, porque nesta verso iremosarmazenar as credenciais dos usurios no banco de dados. Mais sobre isso nas prximas linhas.

  • Servios 63

    1 public function authenticate($params)

    Este o mtodo que faz a autenticao, recebendo um array com o username e o password do usuriopara validao.

    1 $password = md5($params['password']);

    Vamos armazenar as senhas usando o padro de hash MD5, por isso precisamos usar o mesmo mtodopara podermos comparar a senha que o usurio digitou com a que est no banco de dados.

    1 $auth = new AuthenticationService();2 $authAdapter = new AuthAdapter($this->dbAdapter);

    Criamos uma instncia do componente AuthenticationService e indicamos qual o adaptador ondeestamos armazenando os dados do usuri (que iremos receber na instanciao do servio).

    1 $authAdapter2 ->setTableName('users')3 ->setIdentityColumn('username')4 ->setCredentialColumn('password')5 ->setIdentity($params['username'])6 ->setCredential($password);7 $result = $auth->authenticate($authAdapter);

    Fazemos a autenticao, indicando qual a tabela, a coluna do username, a coluna da senha e os dadosque o usurio nos mandou. A varivel $result possui o resultado da autenticao e podemos us-lo paraverificar se a mesma foi feita corretamente, o que fazemos nas prximas linhas do arquivo.

    1 $session = $this->getServiceManager()->get('Session');2 $session->offsetSet('user', $authAdapter->getResultRowObject());

    Usamos aqui um novo componente, muito importante, a Session. Armazenar dados na sesso algo queos desenvolvedores PHP vem fazendo a anos e imagino que seja um conceito j trivial. O Zend Frameworkimplementa isso na forma do componente Zend\Session\Container, que possui os mtodos offsetSet paraarmazenar contedos na sesso e offsetGet para recuper-los.

    ServiceManager

    Vamos agora nos aprofundar um pouco nos conceitos do Zend Framework 2.Como vimos no construtor da classe Auth, para instanciar o servio precisamos passar como parmetrouma instncia de um adapter (no nosso caso uma conexo com o banco de dados) ou seja, a classe Authtem uma dependncia. A vantagem de usarmos isso que o comportamento da classe fica mais flexvel,pois podemos enviar uma conexo diferente dependendo do mdulo, do ambiente de testes, etc.O problema que o processo de criao da instncia de Auth ficou um pouco mais burocrtico, poisprecisamos criar primeiro o adapter. O Zend Framework fornece uma forma de nos auxiliar nesseprocesso, o ServiceManager. Podemos registrar nele todas as dependncias das nossas classes e ele seencarrega de cri-las para ns, sempre que precisarmos.Temos duas formas de configur-lo. A primeira criando um novo mtodo no arquivo module/Admin/-Module.php:

  • Servios 64

    1 /**2 * Retorna a configurao do service manager do mdulo3 * @return array4 */5 public function getServiceConfig()6 {7 return array(8 'factories' => array(9 'Admin\Service\Auth' => function($sm) {10 $dbAdapter = $sm->get('DbAdapter');11 return new Service\Auth($dbAdapter);12 },13 ),14 );15 }

    https://gist.github.com/4030845A segunda criando uma nova configurao no arquivo module/Admin/config/module.config.php:

    1 'service_manager' => array(2 'factories' => array(3 'Session' => function($sm) {4 return new Zend\Session\Container('ZF2napratica');5 },6 'Admin\Service\Auth' => function($sm) {7 $dbAdapter = $sm->get('DbAdapter');8 return new Admin\Service\Auth($dbAdapter);9 },10 )11 ),

    https://gist.github.com/4012041Ambas as formas tem a mesma funcionalidade. Vamos usar a segunda delas, a alterao no mo-dule.config.php.O que estamos dizendo ao framework : sempre que eu solicitar o servio Admin\Service\Auth executeesta funo annima, use a conexo com o banco de dados atual e me retorne uma instncia pronta doservio. O mesmo para o servio Session. por isso que no teste usamos:

    1 $authService = $this->serviceManager->get('Admin\Service\Auth');

    Quando fazemos isso o ServiceManager se encarrega de criar uma nova instncia, ou entregar uma que jexista e ele est armazenando para ns (podemos configurar o ServiceManager para sempre nos entregaruma nova instncia, caso desejemos).Perceba que dentro da funo que cria a instncia de Auth temos:

  • Servios 65

    1 $dbAdapter = $sm->get('DbAdapter');

    Estamos usando o ServiceManager para criar uma conexo com o banco de dados. A configurao doDbAdapter est no arquivo module/Core/Module.php.O Zend Framework 2 usa o ServiceManager internamente para fazer a criao de praticamente tudo, desdecontroladores at vises. um componente de grande utilidade.

    Rodando os testes

    Agora que criamos o cdigo do servio podemos rodar os testes novamente:

    1 phpunit -c module/Admin/tests/phpunit.xml --group=Service

    Com o servio de autenticao pronto podemos passar para o prximo tpico, que usar o servio nanossa aplicao.

  • FormulriosAgora que temos nosso servio de autenticao precisamos criar um formulrio para que o usurio possafazer o login no sistema.

    Formulrio de login

    O Zend Framework 2 possui um componente para auxiliar na criao dos formulrios, o Zend\Form.Vamos criar o nosso primeiro formulrio, o module/Admin/src/Admin/Form/Login.php:

    1

  • Formulrios 67

    38 ),39 ));40 }41 }

    https://gist.github.com/4012044O cdigo bastante explicativo, mas vou citar alguns pontos importantes:

    1 $this->setAttribute('method', 'post');2 $this->setAttribute('action', '/admin/auth/login');

    Estamos configurando o mtodo de envio dos dados para o mtodo POST e indicando que a ACTIONdo formulrio, ou seja, para onde os dados sero enviados, a url /admin/auth/login. Isso significa queiremos tambm criar um novo controlador neste captulo, o AuthController.

    1 $this->add(array(2 'name' => 'username',3 'attributes' => array(4 'type' => 'text',5 ),6 'options' => array(7 'label' => 'Username',8 ),9 ));

    Neste trecho estamos criando um input HTML do tipo text e configurando seu nome e label. Para cadainput existe um componente no namespace Zend\Form\Element, como pode ser visto no manual doframework.

    Controlador de autenticao

    Vamos criar um novo controlador para fazer uso do nosso novo formulrio e do servio de autenticaoque criamos no captulo anterior.Os testes para o novo controlador esto no module/Admin/tests/src/Admin/Controller/AuthController-Test.php:

  • Formulrios 68

    1

  • Formulrios 69

    50 $variables = $result->getVariables();5152 $this->assertArrayHasKey('form', $variables);5354 // Faz a comparao dos dados55 $this->assertInstanceOf('Zend\Form\Form', $variables['form']);56 $form = $variables['form'];57 //testa os tens do formulrio58 $username = $form->get('username');59 $this->assertEquals('username', $username->getName());60 $this->assertEquals('text', $username->getAttribute('type'));6162 $password = $form->get('password');63 $this->assertEquals('password', $password->getName());64 $this->assertEquals('password', $password->getAttribute('type'));65 }6667 /**68 * @expectedException Exception69 * @expectedExceptionMessage Acesso invlido70 */71 public function testLoginInvalidMethod()72 {73 $user = $this->addUser();7475 // Invoca a rota index76 $this->routeMatch->setParam('action', 'login');77 $result = $this->controller->dispatch(78 $this->request, $this->response79 );80 }8182 public function testLogin()83 {84 $user = $this->addUser();8586 // Invoca a rota index87 $this->request->setMethod('post');88 $this->request->getPost()->set('username', $user->username);89 $this->request->getPost()->set('password', 'apple');9091 $this->routeMatch->setParam('action', 'login');92 $result = $this->controller->dispatch(93 $this->request, $this->response94 );9596 // Verifica o response97 $response = $this->controller->getResponse();98 //deve ter redirecionado

  • Formulrios 70

    99 $this->assertEquals(302, $response->getStatusCode());100 $headers = $response->getHeaders();101 $this->assertEquals('Location: /', $headers->get('Location'));102 }103104 public function testLogout()105 {106 $user = $this->addUser();107108 $this->routeMatch->setParam('action', 'logout');109 $result = $this->controller->dispatch(110 $this->request, $this->response111 );112113 // Verifica o response114 $response = $this->controller->getResponse();115 //deve ter redirecionado116 $this->assertEquals(302, $response->getStatusCode());117 $headers = $response->getHeaders();118 $this->assertEquals('Location: /', $headers->get('Location'));119 }120121 private function addUser()122 {123 $user = new User();124 $user->username = 'steve';125 $user->password = md5('apple');126 $user->name = 'Steve Jobs';127 $user->valid = 1;128 $user->role = 'admin';129130 $saved = $this->getTable('Admin\Model\User')->save($user);131 return $saved;132 }133 }

    https://gist.github.com/4012046O teste parecido com os anteriores. A principal diferena testarmos a existncia de uma instncia deZend\Form\Form e os seus elementos.Vamos agora criar o cdigo do novo controlador, no arquivo module/Admin/src/Admin/Controller/Auth-Controller.php:

  • Formulrios 71

    1

  • Formulrios 72

    50 }5152 /**53 * Faz o logout do usurio54 * @return void55 */56 public function logoutAction()57 {58 $service = $this->getService('Admin\Service\Auth');59 $auth = $service->logout();6061 return $this->redirect()->toUrl('/');62 }63 }

    https://gist.github.com/4012047Alguns detalhes:

    1 public function indexAction()

    Instancia o Form e o envia para a viso. Nada de complexo nesse ponto.

    1 public function loginAction()2 public function logoutAction()

    Usamos agora o nosso servio de autenticao. Note que usamos o mtodo $this->getService() que umafacilidade da classe Core/ActionController que simplifica a chamada do ServiceManager, sem alterar emnada seu funcionamento.Precisamos adicionar o novo controlador na configurao do nosso mdulo. Para isso vamos adicionaruma nova linha no array invokables do arquivo module/Admin/config/module.config.php:

    1 'Admin\Controller\Auth' => 'Admin\Controller\AuthController',

    O ltimo passo criarmos a viso para o novo controlador. Precisamos criar o diretrio:

    1 mkdir module/Admin/view/admin/auth

    E o arquivo module/Admin/view/admin/auth/index.phtml:

    1

  • Formulrio