Bando de Dados e Models

O Banco de Dados

Agora que nos temos o módulo Album configurado com controllers, ações e views, está na hora de olhar para a seção de models de nossa aplicação. Lembre-se que os models são a parte que lida com o proposito pricipal de uma aplicação (também chamado de “regras de negócio”) e, em nosso caso, lida com o banco de dados. Nos iremos usar a classe Zend\Db\TableGateway\TableGateway do Zend Framework que serve para procurar, inserir, atualizar e deletar linhas dobanco de dados.

Também vamos usar MySQL, atraves do driver PDO do PHP, portanto crie um banco de dados com o nome de zf2tutorial, e rode as seguintes instruções SQL para criar a tabela de albuns com alguns dados nela.

CREATE TABLE album (
  id int(11) NOT NULL auto_increment,
  artist varchar(100) NOT NULL,
  title varchar(100) NOT NULL,
  PRIMARY KEY (id)
);
INSERT INTO album (artist, title)
    VALUES  ('The  Military  Wives',  'In  My  Dreams');
INSERT INTO album (artist, title)
    VALUES  ('Adele',  '21');
INSERT INTO album (artist, title)
    VALUES  ('Bruce  Springsteen',  'Wrecking Ball (Deluxe)');
INSERT INTO album (artist, title)
    VALUES  ('Lana  Del  Rey',  'Born  To  Die');
INSERT INTO album (artist, title)
    VALUES  ('Gotye',  'Making  Mirrors');

(Os dados de teste escolhidos são os mais vendidos na Amazon UK no momento que esse tutorial foi escrito em sua versão original)

Nos temos alguns dados em um banco de dados e podemos escrever um model bastante simples para eles.

Os Arquivos de Models

O Zend Framework não possui um componente Zend\Model por que os models são nossas regras de negócios e depende de você decidir como quer que elas funcionem, Existem muitos componentes que você pode usar para isso dependendo de suas necessidades. Um dos métodos e ter uma classe model representando cada uma das entidades de sua aplicação e então usar objetos mapeadores que carregam e salvam essas entidades no banco de dados. Outro abordagem pode ser utilizar um Object-relational mapping (ORM), como Doctrine ou Propel.

Para esse tutorial nos vamos criar um model basnte simples atraves da criação de uma classe AlbumTable que usa a classe Zend\Db\TableGateway\TableGateway na qual cada um dos albuns será um Objeto Album (conhecido com entity). Essa é a implementação do modelo padrão Table Data Gateway que permite interação com os dados contidos na tabela do banco de dados. Esteja ciente de que esse modelo Table Data Gateway pode se tornar limitado em sistemas maiores. Também existe uma tentação por colocar o acesso ao banco de dados dentro das ações do controller já que essas são implementadas pela classe Zend\Db\TableGateway\AbstractTableGateway. Não Faça Isso!

Vamos começar criando um arquivo chamado Album.php em module/Album/src/Album/Model:

<?php
namespace Album\Model;

class Album
{
    public $id;
    public $artist;
    public $title;

    public function exchangeArray($data)
    {
        $this->id     = (!empty($data['id'])) ? $data['id'] : null;
        $this->artist = (!empty($data['artist'])) ? $data['artist'] : null;
        $this->title  = (!empty($data['title'])) ? $data['title'] : null;
    }
}

Nosso objeto de entidade Album é uma classe PHP cimples. Para que ela funcione com a classe TableGateway do Zend\Db, nos precisamos implementar o método``exchangeArray()``. Esse método simplesmente copia os dados passados em um array para as propriedades de nossa entidade. Nós iremos implementar filtros para usar com os formulários posteriormente.

Em seguida nos criamos um arquivo``AlbumTable.php`` no diretório module/Album/src/Album/Model com o seguinte código:

<?php
namespace Album\Model;

use Zend\Db\TableGateway\TableGateway;

class AlbumTable
{
    protected $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function fetchAll()
    {
        $resultSet = $this->tableGateway->select();
        return $resultSet;
    }

    public function getAlbum($id)
    {
        $id  = (int) $id;
        $rowset = $this->tableGateway->select(array('id' => $id));
        $row = $rowset->current();
        if (!$row) {
            throw new \Exception("Could not find row $id");
        }
        return $row;
    }

    public function saveAlbum(Album $album)
    {
        $data = array(
            'artist' => $album->artist,
            'title'  => $album->title,
        );

        $id = (int) $album->id;
        if ($id == 0) {
            $this->tableGateway->insert($data);
        } else {
            if ($this->getAlbum($id)) {
                $this->tableGateway->update($data, array('id' => $id));
            } else {
                throw new \Exception('Album id does not exist');
            }
        }
    }

    public function deleteAlbum($id)
    {
        $this->tableGateway->delete(array('id' => $id));
    }
}

Existe muita coisa acontecendo aqui. Primeiramente, nos configuramos uma propriedade protegida $tableGateway para a instancia de TableGateway que será passada no construtor. Nos iremos usar isso para realizar operações na tabela de nosso albuns no banco de dados.

Nos então criamos alguns métodos ajudantes que nossa aplicação irá utilizar para interagir com o table gateway. fetchAll() retorna todas as linhas de albuns do banco de dados como um ResultSet, getAlbum() retorna uma única linha como um objeto Album, saveAlbum() tanto cria uma nova linha no banco de dados quanto atualiza uma linha existente e deleteAlbum() remove completamente uma linha. O código de cada um desses métodos e, esperadamente, auto-explicativo.

Usando o ServiceManager para configurar o Table Gateway e injetar no AlbumTable

Com o objetivo de sempre termos a mesma instancia do nosso AlbumTable, no siremos usar o ServiceManager para definir como criar um. Isso é geralemnte feito na classe Module onde nos criamos o método chamado getServiceConfig() que é automaticamente chamado pelo ModuleManager e aplicado ao ServiceManager. Nos então estaremos aptos a solicita-lo no nosso controller quando precisarmos dele.

Para configurar o ServiceManager, nos podemos ou disponibilizar o nome da classe para ser instanciado ou uma factory (closure ou callback) que instancia o objeto quando o ServiceManager precisar dele. Nos vamos começar implementando o getServiceConfig() para prover a factory que criará o AlbumTable. Adicione esse método ao final do arquivo

Module.php no diretório module/Album.
<?php
namespace Album;

// Inclua essas instruções
use Album\Model\Album;
use Album\Model\AlbumTable;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\TableGateway;

class Module
{
    // métodps getAutoloaderConfig() e getConfig() aqui

    // Inclua esse método:
    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'Album\Model\AlbumTable' =>  function($sm) {
                    $tableGateway = $sm->get('AlbumTableGateway');
                    $table = new AlbumTable($tableGateway);
                    return $table;
                },
                'AlbumTableGateway' => function ($sm) {
                    $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                    $resultSetPrototype = new ResultSet();
                    $resultSetPrototype->setArrayObjectPrototype(new Album());
                    return new TableGateway('album', $dbAdapter, null, $resultSetPrototype);
                },
            ),
        );
    }
}

Esse método retorna um array de factories que irão ser mescladas pelo ModuleManager antes de serem passadas para o ServiceManager. A factory para Album\Model\AlbumTable usa o ServiceManager para criar um AlbumTableGateway que será passado para o AlbumTable. Nos também informamos ao ServiceManager``que um ``AlbumTableGateway é criado solicitando um Zend\Db\Adapter\Adapter (também do ServiceManager) e usando ele para criar o objeto TableGateway. Ao TableGateway é dito para usar um objeto Album sempre que ele criar uma nova linha de resultado. A classe TableGateway use o padrão de prototipagem para criar o conjunto de resultado e as entidades. Isso significa que ao inves de instanciar um novo objet quando solicitado o sistema clona um objeto previamente solicitado. veja PHP Constructor Best Practices and the Prototype Pattern Para mais detalhes (N.T.: em inglês).

Finalmente nos precisamos configurar o ServiceManager para que ele saiba como conseguir a classe Zend\Db\Adapter\Adapter. Isso é feito usando uma factory chamada Zend\Db\Adapter\AdapterServiceFactory a qual podemos configurar atraves do sistema de arquivos de configuração. O ModuleManager do Zend Framework 2 junta todas as configrações de cado um dos arquivos module.config.php dos módulos juntamente com os arquivos definidos em config/autoload (os arquivos *.global.php e depois *.local.php). Nos vamos adicionar nossa configuração de banco de dados no arquivo global.php que você deve enviar para seu sistema de controle de versão. Você pode usar local.php (fora do VCS) para armazenar as credenciais do seu banco de dados caso queira. Modifique o arquivo config/autoload/global.php (no diretório raiz do Zend Skeleton, não dentro do módulo Album) com o seguinte código:

<?php
return array(
    'db' => array(
        'driver'         => 'Pdo',
        'dsn'            => 'mysql:dbname=zf2tutorial;host=localhost',
        'driver_options' => array(
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ),
    ),
    'service_manager' => array(
        'factories' => array(
            'Zend\Db\Adapter\Adapter'
                    => 'Zend\Db\Adapter\AdapterServiceFactory',
        ),
    ),
);

Voê deve então inserir as credenciais de acesso ao seu banco de dados em config/autoload/local.php para que elas não estejam no seu repositório público (já que local.php é ignorado):

<?php
return array(
    'db' => array(
        'username' => 'YOUR USERNAME HERE',
        'password' => 'YOUR PASSWORD HERE',
    ),
);

Voltando ao Controller

Agora que o ServiceManager consegue criar uma instancia de AlbumTable para nos, nos podemos adcionar um método ao controler para requisita-lo. Inclua getAlbumTable() à classe AlbumController:

// module/Album/src/Album/Controller/AlbumController.php:
    public function getAlbumTable()
    {
        if (!$this->albumTable) {
            $sm = $this->getServiceLocator();
            $this->albumTable = $sm->get('Album\Model\AlbumTable');
        }
        return $this->albumTable;
    }

Você tamém deve adicionar:

protected $albumTable;

No topo da classe.

Nos aora podemos chamar getAlbumTable() a partir de nosso controller sempre que precisarmos de interação com nosso model.

Caso o service locator tenha sido configurado conrretamente em Module.php, nos devemos obter uma instancia de Album\Model\AlbumTable quando chamarmos getAlbumTable().

Listando os Albuns

Para listar os albuns nos precisamos solicita-los do model e passa-los para a view. Para fazer isso nos preenchemos a indexAction() do AlbumController. Atualize a indexAction() do AlbumController como a seguir:

// module/Album/src/Album/Controller/AlbumController.php:
// ...
    public function indexAction()
    {
        return new ViewModel(array(
            'albums' => $this->getAlbumTable()->fetchAll(),
        ));
    }
// ...

Com o Zend Framework 2 para passar variáveis para a view nos retornamos uma instancia de ViewModel que tem como primeiro parametro do construtor um array contendo os dados que nos precisamos. Esses são automaticamente passados para o arquivo de view. O objeto ViewModel também permite que você altere o arquivo de view que será usando, ma spor padrão é usado {nome do controller}/ {nome da ação}. Nos agora podemos preencher o arquivo index.phtml:

<?php
// module/Album/view/album/album/index.phtml:

$title = 'My albums';
$this->headTitle($title);
?>
<h1><?php echo $this->escapeHtml($title); ?></h1>
<p>
    <a href="<?php echo $this->url('album', array('action'=>'add'));?>">Add new album</a>
</p>

<table class="table">
<tr>
    <th>Title</th>
    <th>Artist</th>
    <th>&nbsp;</th>
</tr>
<?php foreach ($albums as $album) : ?>
<tr>
    <td><?php echo $this->escapeHtml($album->title);?></td>
    <td><?php echo $this->escapeHtml($album->artist);?></td>
    <td>
        <a href="<?php echo $this->url('album',
            array('action'=>'edit', 'id' => $album->id));?>">Edit</a>
        <a href="<?php echo $this->url('album',
            array('action'=>'delete', 'id' => $album->id));?>">Delete</a>
    </td>
</tr>
<?php endforeach; ?>
</table>

A primeira coisa que fizemos foi configurar o titulo da nossa página (usado no layout) e também passar esse titulo para a seção <head> usando o view helper headTitle() que irá ser exibido no barra de título do navegador. Nos então criamos um link para adcionar um novo album.

O Helper de view url() é fornecido pelo Zend Framework 2 e usado para criar os links que nos precisamos. O primeiro parâmetro de url() é o nome da rota que queremos usar para a construção da url, e o segundo parametro é um array com todas as variáveis que irão substituir os coringas dessa rota. Nesse caso nos usamos a nossa rota ‘album’ que está configurada para aceitar duas variáveis coringa: action e id.

Nos então iremos percorrer os $albums que forma passados pela ação do controller. O sistema de views do Zend Framweork 2 garante automaticamente que essas variáveis sejam extraidas paa o escopo do nosso arquivo de view, portanto nos não precisamos nos preocupar com prefixar elas com $this-> como faziamos com Zend Framework 1; mas você usa-lo se assim desejar.

Nos então criamos uma tabela para exibir o titulo e artista de cada um dos albuns e exibimos também links que possibilitam editar e excluir essas entradas. Um loop foreach: padrão é usado para percorrer a lista de albuns, e nos usamos a forma alternativa atraves do uso de dois-pontos e endforeach; já que essa forma é mais fácil de ser percebida do que tentar posicionar os colchetes. Novamente o helper de view url() é usado para criar os links de edição e exclusão.

Nota

Nos sempre usamos o helper escapeHtml() para ajudar na nossa proteção contra vunerabilidades de Cross Site Scripting (XSS) (veja http://en.wikipedia.org/wiki/Cross-site_scripting).

Se você abrir http://zf2-tutorial.localhost/album você deve ver isso:

../_images/user-guide.database-and-models.album-list.png