Arquivo do autor:Fernando Mantoan

Ubuntu 13.04 corrigindo bug de integração do pacote unity-webapps-gmail

Após formatar meu computador e fazer uma instalação limpa do Ubuntu 13.04, comecei a instalar alguns pacotes de webapps para integrar diversas aplicações web com meu desktop Unity. Um problema que encontrei, foi que a integração com o GMail, via unity-webapps-gmail, não exibia no menu de mensagens o ícone “GMail”, assim como a contagem de posts não-lidos no ícone do Unity.

Investiguei um pouco o código Javascript responsável por fazer essa integração e descobri que o funcionamento dessa integração é via diversas leituras do HTML da página do GMail, algumas relacionadas a textos exibidos na página do GMail. Infelizmente estes textos podem acabar variando dependendo do idioma do usuário. Quando o texto varia, este método irá retornar null e dará um erro no código padrão da integração. Se tiver curiosidade, entre no arquivo /usr/share/unity-webapps/userscripts/unity-webapps-gmail/GMail.user.js, e confira no método checkMessagesCount() o seguinte trecho:

var tag = document.evaluate('//div[@aria-label="Navigate to"]/span', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;

O que ele faz é basicamente buscar em um div pelo atributo aria-label=”Navigate to”, para verificar se seu elemento span contém Gmail ou Mail. No idioma português do GMail, isso será traduzido para: aria-label=”Navegar para”.

Portanto, temos algumas mudanças básicas. Primeiramente, vamos fazer com que tudo funcione perfeitamente mesmo que o método evaluate() não encontre o que procura. Para fazer isso, modifique a linha:

if (tag.textContent != 'Gmail' && tag.textContent != 'Mail') {

Para:

if (tag != null && tag.textContent != 'Gmail' && tag.textContent != 'Mail') {

Com isso, a integração já funcionará. Para fazer com que o método evaluate() funcione para o GMail em português, mude a linha:

var tag = document.evaluate('//div[@aria-label="Navigate to"]/span', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;

Para:

var tag = document.evaluate('//div[@aria-label="Navegar para"]/span', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;

unity-webapps-gmail

Se você resolveu de alguma outra forma, ou se essa solução foi útil para você, por favor deixe um comentário. Mais informações: https://bugs.launchpad.net/ubuntu/+source/unity-webapps-gmail/+bug/1073397?comments=all.

Até a próxima.

Retrospectiva 2012 e que venha 2013

E 2012 já está terminando, e esse ano passou bem rápido. Nesse post vou falar um pouco sobre como o ano foi para mim, para o blog, e as expectativas para 2013.

Este ano foi marcado por projetos de sucesso no trabalho e também em projetos pessoais. O ano começou com um artigo sobre Quartz, ferramenta utilizada em um dos projetos aqui do instituto. Lá para o mês de março, consegui a permissão para publicar a monografia da especialização que concluí em dezembro de 2011, que aborda o protocolo OAuth em um caso de estudo sobre Single-Sign-On.

Entre março e maio eu conheci a CodeSchool e a partir dela o framework Backbone.js, o que resultou no início da Série sobre Backbone.js. A idéia sobre essa série era dar uma introdução sobre o framework e na parte 6 do artigo mostrar abordagens mais complexas na construção de uma aplicação completa. Infelizmente não consegui terminar a série neste ano, mas acredito que até fevereiro ou março de 2013 os dois artigos que faltam (parte 5 e parte 6) serão publicados.

Logo após a publicação do primeiro artigo sobre Backbone.js, criei um pequeno plugin para integrar o Doctrine 2 com o componente Zend_Paginate, do Zend Framework 1. Alavanquei a escrita sobre Zend Framework 1 ao publicar o artigo sobre I18N e L10N com o Zend Framework 1. Conheci então o Vagrant e escrevi um pequeno texto sobre como fazer funcionar a última versão do Ubuntu (na época 12.04).

Depois disso foi publicado o segundo artigo da série sobre Backbone.js, dessa vez falando sobre View, e o terceiro artigo falando sobre Model.

Logo em seguida à publicação deste artigo, parti para São Paulo para fazer o treinamento FJ-91 Arquitetura e Design de Software Java. Foi uma experiência incrível, tanto por conhecer a cidade de São Paulo quanto com relação ao curso e à Caelum. É sem sombra de dúvidas uma empresa que concentra grandes profissionais do Brasil, referência de treinamentos de desenvolvimento, e onde obtive conhecimentos e experiências valiosas com o instrutor Alberto Souza. Já havia feito dois treinamentos in company, um com o Adriano Almeida e outro com o Ricardo Valeriano, ambos em 2011, só que ir diretamente na Caelum é uma experiência muito boa, conheci um pessoal do Uol, da Claro, de órgãos públicos e afins, e valeu muito à pena, parabéns a todos da Caelum pelo grande trabalho, ao ITAI pela oportunidade de realizar este treinamento, e aos três instrutores da Caelum citados pela oportunidade de conhecer melhor a Caelum, devo dizer que admiro muito todos vocês.

Voltando para Foz com muita coisa em mente, idéias, novos conhecimentos, ânimo renovado, eu e o Anderson desenvolvemos a aplicação mobile não-oficial para a Latinoware 2012. Retiramos ela da Play Store devido a alguns problemas, porém o código-fonte encontra-se no Github e qualquer suporte necessário basta entrar em contato conosco. Ah, e essa aplicação rendeu um fruto muito legal, aparecemos em um programa de televisão aqui da região chamado Caminhos do Oeste, você pode assistir a entrevista logo abaixo. Além disso, contamos com 472 instalações, o que também é um número bem legal para a primeira app que publicamos na Play Store.

No mesmo dia que foi publicada a aplicação mobile para a Latinoware 2012 também foi publicado o quarto artigo da série sobre Backbone.js, agora falando sobre Collection. Todos os artigos da série sobre Backbone.js foram também publicados no iMasters, e este quarto artigo entrou na seção de destaque do site, muito gratificante.

Backbone.js destaque iMasters

Backbone.js destaque iMasters

O pessoal do BrazilJS Foundation também contribuiu muito com essa série sobre Backbone.js, divulgando os artigos em duas edições do BrazilJS Weekly (a terceira edição e a sétima), obrigado também a vocês por isso!

Depois disso, o blog ficou meio abandonado, mas não parou só por aí esse ano. Primeiramente, trabalhando com algumas funcionalidades do Zend Framework 2, encontrei um bug no componente Captcha, mais especificamente no View Helper do formulário, enviei um patch e foi aceito pelo pessoal! Foi oficialmente minha primeira contribuição para um projeto open-source, e foi logo para o Zend Framework, um projeto tão grande, e que tenho me dedicado desde 2009, muito bom mesmo! Já havia contribuído com bug reporting de outros projetos como PrimeFaces, onde também estive bem ativo este ano, porém eles não são muito maleáveis com relação à contribuições. Logo após isso, estive usando bastante o Composer e encontrei mais um bug, desta vez referente à construção dos cabeçalhos HTTP para fazer uma requisição por trás de um proxy. Também enviei um patch para o pessoal do Composer, sugeriram outra implementação, e mais uma contribuição foi aceita!

Essa experiência com projetos open-source foi muito boa, ainda mais da parte do Zend Framework que sempre foi bem criterioso para contribuições, exigindo um CLA, patch com testes unitários e etc. Com o GitHub ficou muito mais fácil de contribuir.

Este ano li o livro Arquitetura e Design de Software, do pessoal da Caelum, que também contribuiu para muitas coisas. Escreverei um review mais detalhado sobre este ótimo livro, vale muito à pena tanto para devs Java quanto de outras plataformas.

Posso dizer que esse ano foi ótimo, tanto profissionalmente quanto pessoalmente, e estou torcendo para que 2013 seja ainda melhor, idéias não faltam basta agora procurar executar todas elas.

Para finalizar este post, gostaria de agradecer a todos os leitores do blog e do iMasters por todo feedback, sugestões, elogios e mensagens de motivação, obrigado mesmo e espero conseguir sempre produzir conteúdo que agregue valor à vocês. Agradeço também aos participantes da Latinoware 2012 por todo o feedback que deram sobre a app mobile, obrigado mesmo, o apoio de vocês na Play Store e os comentários que recebi no evento, tanto dos palestrantes quanto dos participantes, foram ótimos. Um muito obrigado também ao pessoal do Caminhos do Oeste por entrarem em contato e pela oportunidade de participar do programa.

Até 2013!

Série Backbone.js: Parte 4 – Collection

Backbone.js

O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Dentre os componentes, encontra-se a Collection, que representa um conjunto ordenado de Models e traz diversos métodos úteis para trabalhar com coleções de dados.

Introdução

No primeiro artigo desta série foi apresentado o framework Backbone.js, seus principais conceitos e aspectos, e uma introdução rápida através de um “Hello World”. No segundo artigo da série foi apresentada a classe Backbone.View, demonstrando sua utilização, templates e a construção de uma View para um exemplo simples de blog. No terceiro artigo da série foi apresentada a classe Backbone.Model junto com um simples backend escrito em Sinatra, possibilitando o trabalho com dados dinâmicos no exemplo do blog, e também foi modificada a View para suportar o mecanismo de templates Mustache.

Neste quarto artigo da série de seis artigos sobre Backbone.js será apresentada a classe Backbone.Collection, com exemplos práticos, listagem dos métodos disponíveis para se trabalhar com coleções de dados na Underscore.js, suporte a eventos, integração com servidor, melhorias no backend Sinatra desenvolvido e implementação da listagem de postagens do blog.

Backbone.Collection

Uma coleção é um conjunto de dados ordenados. A classe Backbone.Collection representa uma coleção de Models, fornecendo diversos métodos úteis para se trabalhar com estes conjuntos além da possibilidade de manipular eventos que ocorrem em uma coleção.

Para criar uma classe Collection customizada basta utilizar o método extend(properties, [classProperties]) que recebe como parâmetro as propriedades da coleção assim como o parâmetro opcional classProperties que define parâmetros diretamente no construtor da coleção.

Além de criar uma coleção customizada é possível definir o atributo model para configurar com qual Backbone.Model a coleção irá trabalhar. Ao se definir um Model também é possível trabalhar com estruturas Javascript puras, no formato de hashs, que são convertidas para o Model definido.

Uma coleção das postagens do blog pode ser definida da seguinte forma:

var Posts = Backbone.Collection.extend({
    model: Post
});

Assim como nas demais classes Backbone, para se definir um construtor para a Backbone.Collection, basta criar um método initialize(). O construtor padrão recebe como parâmetro um conjunto de Models e parâmetros opcionais que incluem também um comparator, utilizado para definir a ordenação das coleções, explicado mais a diante.

Internamente na classe Backbone.Collection todos os Models são mantidos em um array, definido no atributo models. A melhor prática para se trabalhar com os Models é utilizar os métodos manipuladores da classe, porém caso seja necessário acessar diretamente o array de Models, este atributo pode ser utilizado.

Assim como na classe Backbone.Model, a classe Backbone.Collection implementa o método toJSON(), utilizado para definir a notação JSON do objeto. Este método pode ser utilizado para serializar e persistir uma coleção completa. Este método está em conformidade com a API JSON Javascript.

var myPosts = new Posts([
    {title: "Post um", text: "Conteúdo do Post um"},
    {title: "Post dois", text: "Conteúdo do Post dois"},
    {title: "Post três", text: "Conteúdo do Post três"},
]);
alert(JSON.stringify(myPosts));

Underscore.js

Para iterar pela coleção é possível utilizar 28 funções fornecidas pela biblioteca Underscore.js. Cada função tem um objetivo distinto e a lista é bem vasta, portanto será apresentada apenas uma tabela com cada função e na documentação da Underscore.js você pode obter mais informações sobre cada função individualmente.

chain every filter find
first forEach groupBy include
indexOf initial invoke isEmpty
last lastIndexOf map max
min reduce reduceRight reject
rest size some sortBy
sortedIndex shuffle toArray without

Manipulando a coleção

A classe Backbone.Collection fornece diversos métodos para se trabalhar com os dados dos Models. Estes métodos permitem adicionar ou remover elementos, obter elementos, ordenar, entre outros. O método add(), por exemplo, permite adicionar um Model (ou um array de Models) à coleção. Ao executar o método add() o evento “add” será disparado, a menos que o parâmetro {silent: true} seja definido. Para adicionar o Model em uma determinada posição da coleção o parâmetro {at: index} pode ser definido. No callback do evento “add” é possível obter o índice em que o elemento foi adicionado no array options.

var posts = new Posts();
posts.on("add", function(model, collection, options) {
    console.log("O model " + model.get('title') + " foi adicionado na posição " + options.index);
});
posts.add([
    {title: "Post um", text: "Conteúdo do post um"},
    {title: "Post dois", text: "Conteúdo do post dois"}
]);
posts.add({
    title: "Post tres", text: "Post tres"
}, {
    at: 0
});

O método remove() pode ser utilizado para remover um ou mais Models de uma coleção. Ele disparará o evento “remove” a menos que o parâmetro {silent: true} seja definido. Ao se definir um callback para o evento “remove”, o primeiro parâmetro corresponderá ao Model sendo removido e o segundo conterá um array de opções, onde o índice pode ser obtido no atributo options.index.

var posts = new Posts();
posts.on("remove", function(model, collection, options) {
    console.log("O model " + model.get('title') + " foi removido da posição " + options.index);
});
var models = [
    {
        id: 1, title: "Post um", text: "Conteúdo do post um"
    },
    {
        id: 2, title: "Post dois", text: "Conteúdo do post dois"
    }
];
posts.add(models);
posts.remove({id: 1});

Para obter um determinado Model da coleção o método get() é utilizado, o mesmo recebe como parâmetro o valor do atributo id do Model a ser obtido.

var post = posts.get(2);
console.log(JSON.stringify(post));

Outra forma de obter um Model da coleção é através de seu atributo client id. No artigo anterior foi explicado que o client id é um identificador único atribuído pelo Backbone a um objeto que ainda não foi gravado no servidor. Para obter um Model por seu cid o método getByCid() pode ser utilizado.

posts.add({title: "Post um", text: "Conteúdo do post um"});
var post = posts.getByCid('c8');
console.log(JSON.stringify(post));

Por último, pode-se obter um Model através de sua posição no array, com o método at(). Este método é útil quando a coleção está ordenada, caso ela não esteja o método irá obter os Models na ordem em que foram inseridos.

var post = posts.at(0);
console.log(JSON.stringify(post));

Para adicionar um Model no final de uma coleção pode-se utilizar o método push(), que possui os mesmos parâmetros de add().

posts.push({title: "Novo post", text: "Post adicionado"});

O método pop() remove o último Model da coleção e retorna-o. Este método recebe os mesmos parâmetros opcionais do método remove().

console.log(posts.length);
postRemoved = posts.pop();
console.log(posts.length);
console.log(postRemoved.get('title'));

Para adicionar um Model no início de uma coleção o método unshift() é utilizado, definindo os mesmos parâmetros do método add().

posts.unshift({title: "Novo post", text: "Post adicionado"});

O método shift() remove o primeiro Model da coleção e retorna-o. Este método recebe os mesmos parâmetros opcionais do método remove().

console.log(posts.length);
postRemoved = posts.shift();
console.log(posts.length);
console.log(postRemoved.get('title'));

Similar a um array nativo do Javascript, a classe Backbone.Collection possui o atributo length, que retorna o número de Models contidos na coleção.

console.log("Existem " + posts.length + " postagens na coleção.");

Uma coleção, como dito anteriormente, é um cojunto ordenado de Models. O atributo comparator da classe Backbone.Collection define uma função para manter uma coleção ordenada e por padrão esta função não está definida. Isso significa que ao se definir um comparator os Models serão inseridos em seus índices corretos no array collection.models. Um comparator pode ser uma função definida com um simples argumento, que é executada pelo método sortBy() da biblioteca Underscore.js, ou como uma função que recebe dois argumentos e é executada pela função sort() do Javascript. Ao se definir um comparator utilizado pelo método sortBy() o mesmo deverá receber como parâmetro um Model e deverá retornar um valor numérico ou uma string indicando como o Model deve ser ordenado com relação aos demais.

var posts = new Backbone.Collection;
// Ordena pelo nome de usuário
posts.comparator = function(post) {
    return post.get('username');
};
posts.add({title: "Postagem 1", text: "Minha postagem", username: "Fernando"});
posts.add({title: "Postagem 2", text: "Minha postagem 2", username: "Guest"});
posts.add({title: "Postagem 3", text: "Minha postagem 3", username: "Fernando"});

console.log(JSON.stringify(posts));

Um comparator utilizado pela função sort() do Javascript deverá receber dois Models e retornar -1 caso o primeiro Model deva ser adicionado antes do segundo, 0 caso os dois sejam equivalentes e 1 caso o primeiro Model deva ser adicionado depois do segundo.

var orders = new Backbone.Collection;
// Ordena pelo parâmetro "count" do menor para o maior
orders.comparator = function(firstModel, secondModel) {
    if (firstModel.get('count') < secondModel.get('count'))
        return -1;
    else if (firstModel.get('count') > secondModel.get('count'))
        return 1;
    return 0;
};
orders.add({count: 1});
orders.add({count: 3});
orders.add({count: 2});
orders.add({count: 2});
orders.add({count: 5});
orders.add({count: 4});

console.log(JSON.stringify(orders));

Para forçar que uma coleção seja re-ordenada o método sort() é utilizado. Geralmente este método não precisa ser chamado já que a função comparator garantirá que a ordenação seja mantida sempre. Ao executar o método sort() o evento “reset” será disparado na Backbone.Collection, a menos que o parâmetro {silent: true} seja definido.

var orders = new Backbone.Collection;
orders.comparator = function(firstModel, secondModel) {
    if (firstModel.get('count') < secondModel.get('count'))
        return -1;
    else if (firstModel.get('count') > secondModel.get('count'))
        return 1;
    return 0;
};

orders.add({count: 1});
orders.add({count: 3});
orders.add({count: 2});
orders.add({count: 2});
orders.add({count: 5});
orders.add({count: 4});

console.log(JSON.stringify(orders));

orders.on("reset", function() {
    console.log("Collection reseted");
});
orders.sort();

O método pluck() pode ser utilizado para obter um array de um determinado atributo presente no conjunto de Models. Utilizar o pluck() é o equivalente a utilizar o método map() e retornar um único atributo do iterator.

var users = posts.pluck('username');
console.log("The users of the blog are: " + JSON.stringify(users));

Outro método importante para obter valores da coleção é o where(), que irá retornar um array dos Models da coleção que se equivalem aos atributos e valores definidos. Este método é útil quando se deseja fazer buscas na coleção. O exemplo abaixo obtém os posts do usuário “guest”.

var guestPosts = posts.where({username: 'Guest'});
console.log("The guest posts are: " + JSON.stringify(guestPosts));

Interagindo com o servidor

Através da classe Backbone.Collection é possível interagir com dados dinâmicos de um servidor, permitindo manipular uma coleção com operações de gravação, assim como obter um conjunto de dados. Para definir o endpoint de uma coleção o atributo url é configurado. Um aspecto importante deste atributo é que ao configurá-lo todos os Models da Collection utilizarão ele para construir suas URLs individuais. Para exemplificar isso, considere o código abaixo.

var Post = Backbone.Model.extend({});
var PostList = Backbone.Collection.extend({
    url: "/posts",
    model: Post
});

var posts = new PostList();
posts.add({
    id: 1,
    title: "Meu post",
    text: "Conteudo"
});

Neste código nota-se que não foi definido nenhum parâmetro relacionado ao endpoint no Model, em contraste com o que foi implementado no artigo anterior. E, através da url, o Model consegue criar cada endereço para os endpoints de criação, remoção, atualização e obtenção de dados.

var post = posts.get(1);
console.log(post.url());
post.save();

Seguindo o exemplo apresentado acima, a coleção utilizará o endpoint /posts para obter a listagem das postagens. Como na coleção o atributo url está configurado, os Models utilizarão este atributo para gerar os seguintes endpoints:

  • POST /posts – Insere um novo Post
  • GET /posts/:id – Obtém um Post individual
  • PUT /posts/:id – Atualiza o Post atual
  • DELETE /posts/:id – Deleta o Post atual

Para sincronizar a coleção com o último conjunto de Models do servidor o método fetch() é utilizado. Assim que a classe receber uma resposta do servidor todos os dados atuais são zerados a partir do método reset(), explicado logo abaixo, e os novos dados obtidos são definidos. Este método recebe como parâmetro um array de opções que pode conter callbacks para sucesso (options.success) e erro (options.error), e ambas callbacks recebem como parâmetro a coleção em questão e a resposta do servidor. A resposta do servidor deve ser um array de objetos utilizando a notação JSON.

posts.on("reset", function() {
    console.log("Collection zerada");
});
posts.fetch({
    success: function(collection, response) {
        console.log("A resposta foi: " + response);
    }
});

Caso seja necessário modificar o comportamento padrão de zerar a coleção é possível definir no array de opções o hash {add: true}, que diz à Backbone.Collection que os dados devem ser apenas adicionados aos já existentes.

posts.on("reset", function() {
    console.log("Este método nunca será chamado pelo fetch com add true");
});
posts.fetch({
    add: true,
    success: function(collection, response) {
        console.log("A resposta foi: " + response);
    }
});

Além destes parâmetros opcionais, também é possível definir parâmetros suportados na API jQuery.ajax. Um exemplo disso é para obter dados paginados. O código abaixo pode ser utilizado para isso:

Posts.fetch({data: {page: 3}});

Utilizar o método fetch() é interessante somente em casos de lazy-loading, ou seja, quando a coleção não é populada diretamente no carregamento da página. Caso seja necessário renderizar a página já com dados populados pode-se utilizar o método reset(), que irá remover todos os Models atuais da coleção e adicionar os novos Models definidos como primeiro parâmetro do método. No final da execução do método reset() será disparado um evento “reset”, a menos que o parâmetro opcional {silent: true} seja definido no método. Um exemplo disso é ilustrado pela documentação oficial do Backbone.js, onde uma View Rails já popula uma Backbone.Collection da seguinte forma:

<script>
    var Posts = new Backbone.Collection;
    Posts.reset(<%= @posts.to_json %>);
</script>

Outro caso útil para o método reset() é chamá-lo sem nenhum parâmetro, que fará com que a coleção seja zerada e nenhum outro Model seja adicionado.

posts.reset();

Quando é feita a requisição de uma ou mais postagens a API retornará um objeto JSON crú, sem tipagem. Cabe ao Backbone verificar os atributos do objeto JSON e mapeá-los a um Model da aplicação. Na classe Backbone.Collection o método parse() é responsável por este mapeamento. Ele recebe como parâmetro a resposta em notação JSON e retorna um array de Models. Este método é utilizado automaticamente quando o método fetch() é executado. Caso exista a necessidade de modificar este mapeamento é possível sobrescrever o método parse(), porém este método passará a ser executado para todas as requisições GET da coleção em questão.

var PostsWithRoot = Backbone.Collection.extend({
    // A resposta é no formato {posts: []}
    parse: function(response) {
        return response.posts;
    }
});

Para criar uma nova instância de um Model em uma coleção pode-se utilizar o método create(), que recebe como parâmetro um hash de atributos/valores ou um objeto Model instanciado e não sincronizado com o servidor. Este método fará o equivalente a instanciar um novo Model a partir do hash ou utilizar o Model instanciado passado como parâmetro, gravá-lo no servidor, e adicioná-lo ao conjunto de Models após ele ter sido gravado com sucesso. O método irá retornar o Model criado ou false caso ocorra algum erro de validação. A principal condição para que o create() funcione é que o atributo model da classe Backbone.Collection esteja definido corretamente.

Ao criar o Model o evento “add” será disparado imediatamente na coleção, e o evento “sync” será disparado assim que o Model for criado com sucesso no servidor. Para fazer com que o Model só seja adicionado à coleção quando for gravado com sucesso basta definir o parâmetro {silent: true}.

var Posts = Backbone.Collection.extend({
    url: "/posts",
    model: Post
});

var posts = new Posts();

var newPost = new Post({
    title: "Título do novo post",
    text: "Conteúdo do novo post"
});

posts.create(newPost);

Eventos

Ao longo do artigo foram apresentados diversos métodos que a classe Backbone.Collection oferece. Com eles é possível adicionar um ou mais Models a um conjunto de dados, remover Models, limpar o conjunto, sincronizar com o servidor, gravar no servidor, entre outras operações. Cada operação disparará um ou mais eventos. Para resumir, uma coleção poderá disparar os seguintes eventos:

  • add: Quando um Model for adicionado ao conjunto de Models
  • sync: Quando a Collection sincronizar os dados com o servidor
  • reset: Quando uma Collection for limpada
  • remove: Quando um ou mais Models forem removidos do conjunto de dados
  • change: Quando ocorrer alteração na Collection

Tratar estes eventos é um ponto chave em aplicações Backbone. Eles auxiliarão a manter o estado da View sempre atualizado, mostrando ao usuário exatamente como estão os Models atualmente. Apesar de no Backbone.js não existir uma maneira fácil de fazer o binding de atributos e View, trabalhar com eventos e callbacks é uma solução aceitável e que dará ao usuário um feedback instantâneo de suas operações.

No restante do artigo será incrementado o blog desenvolvido até então, permitindo listar as postagens e manipular os Models diretamente na Collection. Também serão adicionados callbacks de eventos para exemplificar o que foi dito até aqui.

Backend

Para o restante do artigo é necessário alterar alguns trechos do código Ruby desenvolvido como backend no artigo anterior. A API desenvolvida no terceiro artigo não estava nem um pouco complacente com RESTful, principalmente porque o endpoint “posts” retornava a última postagem ao invés de todas as postagens. Mesmo que existam diversos outros fatores necessários para construir uma API RESTful consistente, como por exemplo a utilização de HATEOAS, modificar este retorno se torna essencial para que a API respeite mais os URIs e os métodos HTTP. Portanto o novo backend, denominado posts.rb, contém o seguinte código:

require 'sinatra'
require 'json'
require 'active_record'

ActiveRecord::Base.include_root_in_json = false

class Post < ActiveRecord::Base
end

Post.establish_connection(
    :adapter => "sqlite3",
    :database => "data.db"
)

# Apresenta a página index.html, que possui o código Backbone.js
get '/' do
  File.read(File.join('public', 'index.html'))
end

# Obtém todas as postagens do banco de dados
get '/posts' do
    content_type :json
    Post.all.to_json
end

# Obtém uma postagem por id
get 'posts/:id' do
    content_type :json
    post = Post.find params[:id]
    post.to_json
end

# Cria uma nova postagem
post '/posts' do
    content_type :json
    data = JSON.parse request.body.read

    post = Post.new
    post.title = data['title']
    post.text = data['text']

    post.save
    post.to_json
end

# Atualiza uma postagem existente
put '/posts/:id' do
    data = JSON.parse request.body.read

    post = Post.find params[:id]
    post.title = data['title']
    post.text = data['text']

    post.save
end

# Remove uma postagem
delete '/posts/:id' do
    Post.destroy params[:id]
end

Post.connection.close

É possível notar que, além de modificar o retorno do endpoint /posts, foi adicionado também o endpoint GET /posts/:id, para retornar os dados de uma única postagem, e o endpoint POST /posts foi modificado para retornar a postagem criada, permitindo que o Backbone mapeie o atributo id de uma postagem gravada. Para iniciar o backend Sinatra basta executar o comando abaixo.

ruby posts.rb

Implementando Collection, Model e View

Agora que o backend já está do jeito que é necessário a próxima etapa é modificar a aplicação do blog, para fornecer funcionalidades de listagem de postagens, criação de novas postagens e remoção de postagens.

O primeiro passo é modificar o arquivo public/index.html, para adicionar o template de postagem, template de formulário, os arquivos Javascript necessários e inicializar a View principal da aplicação.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Backbone Tutorial - Part 4 Collections</title>
    </head>
    <body>
        <header>
            <h1>Blog</h1>
            <a href="#" class="add-button">Novo Post</a>
        </header>

        <section id="content">
        </section>

        <!-- Templates -->
        <script type="text/template" id="post-template">
            <h2>{{title}}</h2>
            <p>{{text}}</p>
            <a href="#" class="remove-button">Remover Post</a>
        </script>
        <script type="text/template" id="post-form">
            <h2>Adicionar Post</h2>
            <p><label>Title: <input type="text" id="post-title" /></label></p>
            <p><label>Text: <textarea id="post-text"></textarea></label></p>
            <p><input type="submit" value="Salvar" /></p>
        </script>

        <script src="lib/jquery-min.js"></script>
        <script src="lib/underscore-min.js"></script>
        <script src="lib/backbone-min.js"></script>
        <script src="lib/mustache.js"></script>
        <script src="js/models/PostModel.js"></script>
        <script src="js/collections/PostList.js"></script>
        <script src="js/views/PostView.js"></script>
        <script src="js/views/PostFormView.js"></script>
        <script src="js/views/AppView.js"></script>
        <script>
            $(function() {
                var application = new AppView();
            });
        </script>
    </body>
</html>

A estrutura de pastas do Javascript foi modificada para que contenha uma pasta para Models, uma para Collections e uma para Views. É uma boa prática para saber exatamente a localização de cada pedaço da aplicação, mas vale lembrar que não existe uma regra que define essa estrutura de diretórios. O Model definido continua com o mesmo código do apresentado no artigo anterior:

var PostModel = Backbone.Model.extend({
    defaults: {
        title: "",
        text: ""
    },
    validate: function(attrs) {
        if (attrs.title == '')
            return 'O título é obrigatório';
        if (attrs.text == '')
            return 'O texto é obrigatório'
    }
});

O próximo passo é implementar a classe Collection que irá representar o conjunto de postagens do blog. Dentro do arquivo js/collections/PostList.js é criada então uma coleção com o nome PostsList, definindo também seu atributo url como /posts.

var PostList = Backbone.Collection.extend({
    model: PostModel,
    url: '/posts',
    comparator: function(post) {
       -post.get('id');
    }
});
var Posts = new PostList();

Observe que já é criada uma instância da Collection. Com isso o código básico da aplicação já está criado, faltando agora definir as Views. Primeiramente existe uma View responsável por apresentar os dados de uma postagem, esta view é a PostView, definida em js/views/PostView.js.

var PostView = Backbone.View.extend({
    tagName: 'article',
    className: 'page-posts',
    template: $('#post-template').html(),

    events: {
        "click .remove-button": "removePost"
    },

    initialize: function() {

        _.bindAll(this, 'render', 'removePost', 'remove');

        this.model.on("change", this.render);
        this.model.on("destroy", this.remove);
    },

    render: function() {
        var viewContent = Mustache.to_html(this.template, this.model.toJSON());
        this.$el.html(viewContent);
        return this;
    },

    removePost: function() {
        this.model.destroy();
    }
});

O primeiro passo é configurar os parâmetros básicos da View, conforme já apresentado nos artigos anteriores. A principal diferença aqui é o bind para o evento “destroy” e o método removePost. O bind fará com que a View seja removida da apresentação assim que o Model indicar, através do evento “destroy”, que foi excluído. Este evento é executado através do método model.destroy(), que, irá fazer uma requisição DELETE para o backend.

Falta agora criar uma View para apresentar o formulário de adição de postagens. Esse é um código um pouco mais extenso, porém sem muitas diferenças do apresentado no artigo anterior. O arquivo js/views/PostFormView.js define uma classe PostFormView para esse fim.

var PostFormView = Backbone.View.extend({
    tagName: 'form',
    className: 'page-form',
    id: 'post-form',
    attributes: {
        action: 'posts',
        method: 'POST'
    },
    events: {
        "submit" : "savePost"
    },

    initialize: function(model) {
        _.bindAll(this, 'render', 'savePost');

        this.template = $('#post-form').html();
    },

    render: function() {
        var rendered = Mustache.to_html(this.template);
        this.$el.html(rendered);

        this.titleInput = this.$el.find('#post-title');
        this.textInput = this.$el.find('#post-text');

        this.hide();
    },

    savePost: function(e) {
        e.preventDefault();

        this.model = new PostModel();
        this.model.on("error", this.showError);

        var title = this.titleInput.val();
        var text = this.textInput.val();

        this.model.set({
            title: title,
            text: text
        });

        if (this.model.isValid()) {
            Posts.create(this.model, {wait: true});
            this.hide();
            Posts.sort();
        }
    },

    hide: function() {
        this.$el.hide();
    },

    show: function() {
        this.titleInput.val('');
        this.textInput.val('');
        this.$el.toggle();
    },

    showError:function(model, error) {
        window.alert('Ocorreu um erro, motivo: ' + error);
    },
});

A principal diferença aqui é que o Form é exibido na mesma página das postagens, em contraste com o código criado no artigo anterior da série. Ao se trabalhar com Collection fica muito mais fácil de manipular os Models, controlar as Views e os eventos lançados pelas Collections/Models. Neste caso por exemplo gravar uma postagem é tão simples quanto: verificar se os dados são válidos, adicionar o Model à Collection definindo que o evento só será disparado quando o servidor der uma resposta, esconder o formulário. O último passo é criar a View principal js/views/AppView.js.

var AppView = Backbone.View.extend({
    el: $('#content'),

    initialize: function() {

        _.bindAll(this, 'render', 'addAll', 'addPost', 'showForm');

        Posts.bind('add', this.addPost);
        Posts.bind('reset', this.addAll);
        Posts.bind('sync', this.render);
        Posts.fetch();

        $('.add-button').on('click', this.showForm);

        this.form = new PostFormView();
        this.form.render();
        $('header').append(this.form.el);
    },

    render: function() {
        this.$el.empty();
        this.addAll();
    },

    addPost: function(post) {
        var view = new PostView({
            model: post
        });
        this.$el.append(view.render().el);
    },

    addAll: function() {
        Posts.each(this.addPost);
    },

    showForm: function() {
        this.form.show();
    }
});

Esse código é onde todo o trabalho em torno da Collection ocorre. Primeiramente são tratados os eventos add e reset. O add irá criar uma instância de ViewPost para exibir a postagem ao usuário. O reset irá varrer todos os Posts, através do método each(), e criar uma nova View para exibição. Esse é todo o código necessário até então.

A aplicação criada será composta então de: listar as postagens, adicionar uma postagem, remover uma postagem. A primeira tela com a lista das postagens é apresentada abaixo.

Ao clicar em Novo Post, o formulário é apresentado.

Deixar os campos em branco apresentará um erro de validação.

Preencher corretamente os campos fará com que seja adicionada a nova postagem.

Remover uma postagem fará com que uma requisição DELETE seja feita, ela seja excluída do banco e saia da tela de apresentação.

Uma coisa a se destacar nesse código desenvolvido é que foram necessários alguns truques para exibir/esconder o formulário e remover uma postagem. Como dito anteriormente, não existe a forma correta ou errada de se desenvolver aplicações com Backbone.js. Essa é a principal vantagem deste framework, assim como é a principal desvantagem. Por um lado isso permite a construção de códigos difíceis de compreender e dificulta o mapeamento de dependências. Por outro lado não obriga os desenvolvedores a fazer de uma única maneira. No próximo artigo e no seguinte tudo isso será desmistificado, e algumas boas práticas serão apresentadas. Também vale lembrar que ao se construir coleções deve-se avaliar bem os métodos a utilizar. Como são oferecidos diversos métodos para facilitar a vida do desenvolvedor, analisar cada um e utilizá-los podem facilitar bastante o desenvolvimento e entendimento da aplicação.

Código-fonte

O código-fonte de todos os artigos desta séria sobre Backbone.js encontra-se no repositório backbone-tutorial-series do meu GitHub.

Referências

Para a construção deste artigo a documentação do Backbone.js foi utilizada, em conjunto com alguns vídeos do curso de Backbone.js da CodeSchool. Também foi utilizada a documentação do Sinatra, e a documentação do ActiveRecord. Se quiser saber mais sobre HATEOAS clique aqui. Se quiser saber mais sobre os métodos oferecidos pela Underscore.js acesse aqui.

No próximo e penúltimo artigo da série serão apresentadas as classes Backbone.Router utilizada para construir o roteamento client-side nas aplicações que utilizam o framework Backbone.js, e Backbone.history utilizada para guardar os estados de mudança de URLs, assim como a função Backbone.sync utilizada para ler ou gravar dados no servidor.

Aplicação Android para a Latinoware 2012

Latinoware 2012

A Conferência Latino-Americana de Software Livre – Latinoware 2012 ocorrerá nos dias 17, 18 e 19 de Outubro, aqui em Foz do Iguaçu. Promovida pela Itaipu Binacional, Fundação Parque Tecnológico Itaipu – Brasil, Companhia de Informática do Paraná (Celepar) e Serviço Federal de Processamento de Dados (Serpro), a Latinoware é um evento que abre espaço para discussões e reflexões sobre a utilização do Software Livre na América Latina, além de promover a integração e a valorização latino-americana.

Esse ano, eu e meu amigo Anderson Rodrigo Davi, tivemos a curiosidade de pesquisar sobre uma aplicação Android contendo a grade do evento, o que facilitaria bastante para consultar as palestras do dia. Vimos que desde sua primeira edição, o evento nunca ganhou a devida atenção nestes dispositivos. Entrei em contato com o pessoal da organização e eles autorizaram a criação de uma aplicação simples para apresentar a grade de palestras dos três dias.

Só que, o que adiantava a gente construir uma aplicação fechada, sendo que é uma conferência de software livre? A idéia então foi a de construir a aplicação e além disso disponibilizar seu código-fonte, para que desenvolvedores que tenham interesse possam contribuir para chegar a uma aplicação com mais recursos, ou até que desenvolvedores mais experientes tenham o interesse em investir em uma aplicação semelhante.

Bom demos o pontapé inicial, você pode baixar a aplicação na Play Store. Se tiver interesse em contribuir com a aplicação, acesse o repositório no GitHub. Toda contribuição é bem-vinda, se quiser relatar algum bug, sugerir funcionalidade, contribuir com novas funcionalidades, traduzir para outros idiomas, etc. Temos alguns planos em mente como:

  • Construir uma base de dados localmente no aparelho, evitando consultar o backend;
  • Implementar o recurso de marcar palestras que serão assistidas;
  • Corrigir os bugs apresentados na versão atual;
  • Incluir demais funcionalidades implementadas pela comunidade.

Nos vemos na Latinoware 2012!

Zend Framework 2.0.0 Lançado

A comunidade Zend Framework anunciou oficialmente (http://framework.zend.com/blog/zend-framework-2-0-0-stable-released.html) o tão aguardado lançamento da versão estável do Zend Framework 2. Foram anos de trabalho no desenvolvimento desta nova versão que tinha como objetivo melhorar significantemente toda a sua estrutura, boas práticas, integração com outros frameworks,  padrões de projetos e etc.

Alguns novos componentes foram adicionados como o ModuleManager, ServiceManager, Zend\Di, EventManager, assim como modificações nos demais componentes como Zend_Locale e Zend_Translate que foram unificados no componente Zend\I18n, Zend\Mvc, e muitas outras modificações.

O principal ponto a se destacar nessa nova versão é a versão do PHP mínima necessária para o correto funcionamento do framework que, na versão 1.10+ era 5.2.4 e agora é 5.3+.

Em breve os artigos que escrevi para o Zend Framework 1.11+ serão atualizados para a nova versão do Zend Framework, mantendo é claro as versões anteriores.

Para fazer o download do Zend Framework 2.0.0 acesse o seguinte link: http://framework.zend.com/downloads/latest.

Série Backbone.js: Parte 3 – Model

Backbone.js

O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Dentre os componentes, encontra-se o Model, responsável por representar os dados de uma aplicação, conter regras de negócio, incluindo validações, conversões, controle de acessos, e definir os aspectos de persistência.

Introdução

No primeiro artigo desta série, foi apresentado o framework Backbone.js, seus principais conceitos e aspectos, e uma introdução rápida através de um “Hello World”. No segundo artigo da série, foi apresentada a classe Backbone.View, demonstrando sua utilização e a construção de uma View para um simples blog.

Neste terceiro artigo da série de seis artigos sobre Backbone.js, será apresentada a classe Backbone.Model, com exemplos práticos, integração com backend e utilização na classe Backbone.View.

Backbone.Model

Conforme dito no segundo artigo, apesar de muitos desenvolvedores não acharem o Backbone um framework completamente MVC, a documentação oficial faz uma analogia, comparando os componentes com o framework Ruby on Rails.

Seguindo essa premissa, a classe Backbone.Model representa a camada Model da aplicação, que, segundo a documentação oficial, é a fundação de aplicações Javascript, onde estarão presentes os dados dinâmicos, assim como a lógica que os envolve, como, por exemplo, conversões, validações, propriedades computadas e controle de acesso.

Continuando a idéia de desenvolver um simples blog, um Model que representa uma postagem pode ser definido conforme o código a seguir:

var PostModel = Backbone.Model.extend({});

A primeira premissa a se destacar na classe Backbone.Model, é que ela não possui funcionalidades para trabalhar com várias linhas de dados. Em outras palavras, diferente do Model do Ruby on Rails, o Model do Backbone só permitirá trabalhar com uma instância por vez e não fornecerá métodos como all() do ActiveRecord::Base. Para trabalhar com coleções de Models, outra classe é utilizada, e a mesma será apresentada no próximo artigo.

Backbone.Model também possui o método initialize(), que é o construtor padrão das classes do framework Backbone, porém em contraste das demais classes, é raro modificar o comportamento padrão da classe Backbone.Model, que receberá um hash contendo os atributos que serão definidos no Model. Para o PostModel, os atributos serão: títulotexto. O código abaixo demonstra a utilização do construtor.

var post = new PostModel({
    title: 'Primeiro Post',
    text: 'Conteúdo do post'
});

Para obter o valor de um atributo é utilizado o método get(), passando como parâmetro o atributo em questão. Para definir um novo valor a um atributo é utilizado o método set(), que recebe como parâmetro um hash contendo o identificador do atributo a ser definido, e seu novo valor. O código abaixo exemplifica a utilização destes métodos.

console.log(post.get('title')); // Primeiro Post
console.log(post.get('text')); // Conteúdo do post
post.set({text: 'Novo conteúdo'});
console.log(post.get('text'); // Novo conteúdo

Os atributos de um Model, definidos por set() ou pelo initialize() são armazenados internamente em um hash interno da classe Backbone.Model chamado attributes. Uma boa prática ao se trabalhar com a Backbone.Model é a de não utilizar diretamente o attributes e sim os métodos get()set().

Outro método para obter o valor de um atributo é o escape(), que funciona de maneira similar ao get(), com a diferença de retornar uma versão com tratamento de tags HTML, para prevenir XSS.

post.set({
    title: '<h1>HTML</h1>',
    text: '<h2><script>alert("xss");</script></h2>;'
});

console.log(post.escape('text')); // &lt;h2&gt;&lt;script&gt;alert(&quot;xss&quot;);&lt;&#x2F;script&gt;&lt;&#x2F;h2&gt;

O método has(), verifica se existe o atributo informado no objeto e se o mesmo não está null ou undefinedunset() irá remover um atributo do objeto e clear() irá limpar todos os atributos.

if (post.has('title')) {
    post.unset('title');
}
console.log(post.get('title')); // undefined
post.clear();
console.log(post.get('text')); // undefined

É possível notar que os atributos de um Model são definidos dinamicamente no framework Backbone, porém com o atributo defaults, é possível preencher o Model com atributos e valores padronizados, que só serão sobrescritos caso novos valores sejam definidos no construtor da classe ou via o método set(). O exemplo abaixo demonstra isso.

var PostModel = Backbone.Model.extend({
    defaults: {
        title: '',
        text: ''
    }
});
var post = new PostModel();
console.log(post.get('title')); // 
var post = new Post({title: 'Meu Post'});
console.log(post.get('title')); // Meu Post
console.log(post.get('text'); // 

Para converter um objeto Backbone.Model para notação JSON é utilizado o método toJSON(). Seu funcionamento é simples, ele irá utilizar o método _.clone() da biblioteca Underscore.js, e retornar um array contendo os atributos e valores do objeto. Esse método pode ser utilizado para persistência de dados, serialização, utilização em templates, entre outros.

var post = new PostModel({title: "Primeiro Post"});
post.set({text: "conteudo"});
alert(JSON.stringify(post)); // {"title":"Primeiro Post","text":"conteudo"}
view.template(post.toJSON());

A classe Backbone.Model possui também os atributos id, idAttribute e cid. O primeiro atributo é o identificador único do Model, e é utilizado para obter um determinado Model de uma coleção, assim como para construir URLs para o Model. O segundo atributo é utilizado quando o atributo id não é o identificador único de um Model. Ao definir idAttribute, o atributo customizado definido será mapeado internamente para o atributo id. E o terceiro atributo, cid (client id) é uma propriedade especial utilizada para identificar a primeira instância de Models, útil principalmente para casos onde os Models não estão gravados ainda no servidor, e não possuem um valor real para o atributo id, porém precisam estar presentes na parte visual. O atributo cid conterá valores no formato: c1, c2, c3, etc. O código abaixo demonstra a utilização de cada atributo:

console.log(post.id); // undefined

var PostModel = Backbone.Model.extend({
    idAttribute: 'timestamp'
});

var post = new PostModel({timestamp: '201201'});
console.log('Id: ' + post.id); // Id: 201201
console.log('Timestamp: ' + post.get('timestamp')); // Timestamp: 201201
console.log('CID: ' + post.cid); // CID: c4

Esses são os conceitos, atributos e métodos básicos para se trabalhar com a classe Backbone.Model. A partir desta etapa serão abordados os conceitos de persistência de dados e a PostView criada no artigo anterior será modificada para interagir com a classe PostModel.

Backend

Para as próximas etapas deste artigo, é necessário criar uma API RESTful, para utilizar em conjunto com os métodos de persistência e sincronização do Backbone. Neste artigo foi criada uma API simples utilizando utilizando o Sinatra, um framework Ruby que agiliza bastante a criação de APIs RESTful. O primeiro passo é criar um banco de dados utilizando SQLite:

sqlite3 data.db
sqlite> create table posts(id integer not null primary key autoincrement, title varchar(200), text varchar(255));

Com o banco de dados criado, é necessário instalar o Sinatra, junto com o driver sqlite3 para Ruby:

sudo gem install sinatra sqlite3 active_record

A próxima etapa é criar o arquivo posts.rb e incluir o seguinte código:

require 'sinatra'
require 'json'
require 'active_record'

# O JSON não deve conter um elemento ROOT, apenas os atributos
ActiveRecord::Base.include_root_in_json = false

class Post < ActiveRecord::Base
end

Post.establish_connection(
    :adapter => "sqlite3",
    :database => "data.db"
)

# Apresenta a página contendo o código HTML e Javascript
get '/' do
  File.read(File.join('public', 'index.html'))
end

# Endpoint GET para obter a última postagem do banco de dados
get '/posts' do
    content_type :json
    Post.last.to_json
end

# Endpoint POST para criar uma nova postagem
post '/posts' do
    data = JSON.parse request.body.read

    post = Post.new
    post.title = data['title']
    post.text = data['text']

    post.save
end

# Endpoint PUT para atualizar uma postagem existente
put '/posts/:id' do
    data = JSON.parse request.body.read

    post = Post.find params[:id]
    post.title = data['title']
    post.text = data['text']

    post.save
end

# Endpoint DELETE para remover uma postagem existente
delete '/posts/:id' do
    Post.destroy params[:id]
end

O primeiro parâmetro configura o ActiveRecord para não incluir um elemento pai ao se fazer a conversão para JSON, ou seja, apenas os atributos da classe estarão presentes. É definido então o Model Post, os parâmetros para conexão de banco de dados, uma rota padrão que irá apenas retornar o arquivo index.html da pasta public, uma rota GET /posts que irá retornar a última postagem do banco de dados, uma rota POST para criação de uma nova postagem, uma rota PUT para atualização de uma postagem e, por último uma rota DELETE para exclusão de uma postagem. Quem está acostumado a trabalhar com APIs RESTful irá notar que a rota GET /posts está inconsistente, principalmente por ela retornar apenas uma postagem, o que deveria ser feito em uma rota GET /posts/:id. Essa abordagem foi utilizada apenas para simplificar este artigo e a utilização no framework Backbone, porém no próximo artigo isso será modificado.

A próxima etapa é criar o arquivo index.html e incluí-lo na pasta public. Este arquivo irá conter o código HTML básico e o código Javascript com Backbone.js. Um exemplo simples desse arquivo é apresentado abaixo:

<!doctype html>
<html>
    <head>
        <title>Último Post</title>
        <meta charset="UTF-8" />
        <script src="../lib/jquery-min.js"></script>
        <script src="../lib/underscore-min.js"></script>
        <script src="../lib/backbone-min.js"></script>
        <script src="Models.js"></script>
    </head>
    <body>
    </body>
</html>

No arquivo Models.js encontra-se a classe PostModel, apresentada a seguir:

var PostModel = Backbone.Model.extend({
    defaults: {
        title: '',
        text: ''
    }
});

Para iniciar o servidor execute o seguinte comando:

ruby products.rb
== Sinatra has taken the stage ...
>> Listening on 0.0.0.0:4567

Não esqueça de incluir na pasta public/lib os frameworks: jQuery, Underscore.js e Backbone.js.

Configuração do Endpoint

Ao se definir uma classe que herda de Backbone.Model, existem dois atributos necessários para configuração do endpoint RESTful do servidor, o primeiro é o urlRoot e o segundo o url. O atributo urlRoot representa o prefixo da URL, quando não é utilizada a classe Backbone.Collection (mais sobre isso no próximo artigo), este prefixo será utilizado para gerar as URLs baseadas no atributo id do Model. O segundo atributo, url, contém o endereço completo onde o recurso do Model é encontrado no servidor. A partir dele é possível definir URLs localizadas em outros servidores, caso necessário. Este atributo será construído dinamicamente, caso não seja definido explicitamente, podendo ser a partir do atributo url da classe Backbone.Collection, ou, caso não seja usada a classe Backbone.Collection, a partir do atributo urlRoot da classe Backbone.Model. As URLs geradas podem, então, ter o formato: /[urlRoot]/id ou /[collection.url]/id.

Para a classe PostModel o atributo urlRoot deverá ser configurado da seguinte forma:

var PostModel = Backbone.Model.extend({
    urlRoot: 'posts',
    defaults: {
        title: '',
        text: ''
    }
});

Dessa forma, as URLs geradas serão:

  • GET: /posts ou /posts/id
  • POST: /posts
  • PUT: /posts/id
  • DELETE: /posts/id

Obtendo a última Postagem

Com o servidor devidamente iniciado e a classe PostModel configurada, é possível obter a última postagem através do método fetch():

var post = new PostModel();
post.fetch();

Este método irá limpar o estado atual da instância de PostModel, e sincronizar seus dados com os dados do servidor, utilizando o método Backbone.sync, que será explicado em um artigo futuro. Ele utilizará o primeiro endereço apresentado, /posts, e poderá executar dois callbacks: success executado quando o servidor retornar uma resposta e não apresentar erros, e error caso ocorra algum erro. Ambas são apresentadas abaixo:

post.fetch({
    success: function(model, response) {
        console.log(model.get('title'));
    },
    error: function(model, response) {
        window.alert('Ocorreu um erro');
    }
});

Ao testar esse código, será impresso no console Javascript do browser o título da última postagem existente no banco de dados.

Gravando uma Postagem

Para gravar uma postagem o método save() é utilizado. Internamente, assim como no caso do fetch(), ele utilizará o método Backbone.sync. Baseando-se no método isNew(), será executada uma requisição POST para a criação de um novo recurso no servidor ou um PUT para a atualização de um recurso já existente. isNew() considerará um objeto como um novo recurso ou não, caso o atributo id esteja vazio. O código abaixo grava uma nova postagem no servidor:

var post = new PostModel({
    title: 'First Post',
    text: 'Text of the post'
});
post.save();

Para atualizar uma postagem existente, o seguinte código é utilizado:

var post = new PostModel({
    id: 1,
    title: 'First Post',
    text: 'Text of the post'
});
post.save();

O método save() recebe os parâmetros opcionais: attributes e options. O primeiro parâmetro define quais os atributos que serão atualizados no servidor, porém, ao enviar para o servidor, o recurso como um todo estará presente. Para ilustrar isso, considere o seguinte exemplo:

var post = new PostModel();
// Obtém a última postagem
post.fetch({
    success: function(model, response) {
        // Será atualizado somente o atributo "title"
        post.save({
            title: 'Atualizar o titulo'
        });
    }
});

Quando o método save() faz a requisição, todos os valores do objeto são enviados, mas internamente na classe Backbone.Model, serão considerados atualizados somente os atributos definidos no método save(). O segundo parâmetro, options pode configurar diversos comportamentos, dentre eles os callbacks success e error. A principal diferença com relação aos callbacks do método fetch() é que caso ocorra algum erro de validação no PostModel, o callback error será executado. Mais sobre isso a seguir.

var post = new PostModel({
    id: 1,
    title: 'First Post',
    text: 'Text of the post'
});
post.save(
    null,
    {
        success: function (model, response) {
            console.log(model.get('title'));
        },
        error: function (model, response) {
            window.alert('Ocorreu um erro');
        }
    }
);

Removendo uma Postagem

Para remover um recurso no servidor é utilizado o método destroy(). Este método também executará internamente o método Backbone.sync, e fará uma requisição utilizando o método HTTP DELETE, definindo na URL o atributo id do Model. Caso o método isNew() retorne verdadeiro, o método destroy() retornará false e não fará a requisição. destroy() recebe como parâmetro uma hash opcional, que também aceita as callbacks success e error, assim como o parâmetro wait: true, útil ao se utilizar o Model com a classe Backbone.Collection. Ao utilizar wait: true, o Model em questão somente será removido da coleção quando houver uma resposta do servidor indicando sucesso na remoção. O método abaixo faz a remoção de uma postagem:

var post = new PostModel({
    id: 1
});
post.destroy({
    success: function(model, response) {
        console.log('Postagem removida com sucesso');
    },
    error: function(model, response) {
        window.alert('Ocorreu um erro');
    }
});

Eventos

A classe Backbone.Model trabalha com diversos eventos, permitindo sempre manter o estado de Backbone.Model atualizado nas demais partes da aplicação. Métodos como clear()unset()set() irão disparar o evento “change”, assim como os métodos fetch() e save(). Os método save() e destroy() irão disparar, além do “change”, um evento “sync” assim que a execução de uma requisição tiver uma resposta do servidor confirmando as operações de gravação e remoção, consecutivamente. O exemplo abaixo demonstra algum destes eventos:

var post = new PostModel();
post.on('change', function() {
    console.log('O evento change foi disparado');
});
post.on('sync', function() {
    console.log('O evento sync foi disparado');
});
post.set({
    title: 'Um titulo',
    text: 'Um texto'
}); // Dispara change
post.fetch(); // Dispara change

Os métodos save() e destroy() recebem também o parâmetro wait: true, conforme mencionado na seção de remoção de uma postagem. No método destroy() este atributo é utilizado ao se trabalhar com a classe Backbone.Collection e evita que um Model seja removido de uma coleção até que o servidor confirme a operação. No caso do método save(), este atributo garantirá que os atributos do Model só sejam atualizados caso o servidor confirme a gravação.

É possível disparar manualmente um evento “change” com o método change(). Os atributos modificados no último evento “change” ficam armazenados no atributo changedAttributes, no formato de hash. Para verificar se um atributo foi alterado no último evento “change” o método hasChanged() é utilizado.

var post = new PostModel();
post.on('change', function() {
    if (post.hasChanged('title'))
        console.log('o atributo titulo foi alterado');
    else
        console.log('o atributo titulo não foi alterado');
});
post.set({
    title: 'Evento manual',
    text: 'Texto'
}, {
    silent: true
});
post.change();

Para obter os valores anteriores aos do evento “change” é utilizado o método previous() que recebe como parâmetro o atributo a ser obtido. O método previousAttributes() também pode ser utilizado, o mesmo irá retornar um hash com todos os atributos anteriores à modificação.

var post = new PostModel({
    title: 'Valor antigo',
    text: 'Texto antigo'
});

post.on('change', function() {
    if (post.hasChanged('title')) {
        console.log('o atributo titulo foi alterado');
        console.log(post.previous('title'));
        console.log(post.get('title'));
    }
});
post.set({title: 'Novo valor'});
post.change();

Validação de Dados

A classe Backbone.Model suporta validação de dados antes de enviá-los ao servidor. Essa validação é definida no método validate(), que inicialmente não possui implementação. Este método será chamado pelo Backbone antes de executar os métodos set() ou save(). Caso os dados estejam válidos, o método não deve retornar nada, caso contrário um erro deve ser retornado, que pode ser desde uma string representando o erro ou um objeto de erro completo. Ao encontrar um erro, a execução de set() e save() será interrompida imediatamente, e um evento error será disparado pelo Backbone. O código abaixo faz a validação do preenchimento dos atributos title e text do PostModel.

var PostModel = Backbone.Model.extend({
    urlRoot: 'posts',
    defaults: {
        title: '',
        text: ''
    },
    validate: function(attrs) {
        if (attrs.title == '')
            return 'O título é obrigatório';
        if (attrs.text == '')
            return 'O texto é obrigatório'
    }
});

var post = new PostModel();
post.on('error', function(model, error) {
    alert(error);
});
post.save();

Para verificar se o Model está com dados válidos o método isValid() também pode ser utilizado, muito utilizado ao se trabalhar com entradas de dados. O exemplo abaixo utiliza o método isValid() para exibir uma mensagem informativa ao usuário.

var post = new PostModel();
post.on('error', function(model, error) {
    if (!model.isValid())
        alert('Erro de validação: ' + error);
    else
        alert(error);
});
post.save();

Model + View

No artigo anterior foi criada uma View simples para exibir o conteúdo de uma postagem. Primeiramente, a View será alterada para utilizar o mecanismo de template Mustache.js. Faça o download da última versão e inclua-o na pasta lib. Uma boa prática ao desenvolver aplicações com Backbone, é a de definir o template fora do código-fonte Javascript. O arquivo public/index.html contém o seguinte código:

<!doctype html>
<html>
    <head>
        <title>Último Post</title>
        <meta charset="UTF-8" />
        <script src="../lib/jquery-min.js"></script>
        <script src="../lib/underscore-min.js"></script>
        <script src="../lib/backbone-min.js"></script>
        <script src="Models.js"></script>
    </head>
    <body>
    </body>
</html>

A principal diferença entre o Mustache.js e o mecanismo de template do Underscore.js são os coringas para inclusão de conteúdo dinâmico, onde no Underscore é utilizado uma notação similar ao do ERB <%= coringa %> e no Mustache.js {{coringa}}. Neste template foi incluído um botão para direcionar o usuário para a página public/add.html que contém um formulário para adicionar uma nova postagem. Antes de ir para essa página, é necessário alterar a classe PostView:

var PostView = Backbone.View.extend({
    tagName: 'article',
    className: 'page-posts',
    events: {
        "click #remove-button": "removePost"
    },

    initialize: function() {
        _.bindAll(this, 'render', 'removePost', 'refresh');

        this.template = $('#post-template').html();

        this.model = new PostModel();

        this.model.on("change", this.render);
        this.model.on("destroy", this.refresh);
        this.model.fetch();
    },

    render: function() {
        console.log("Rendering...");
        var rendered = Mustache.to_html(this.template, this.model.toJSON());
        this.$el.html(rendered);
        $('body').append(this.el);
    },

    removePost: function() {
        this.model.destroy();
    },

    refresh: function() {
        this.model.clear({silent: true});
        this.model.fetch();
    }
});

Algumas mudanças foram necessárias para utilizar o framework Mustache.js. Primeiramente, o atributo template é criado no construtor da classe, pois neste momento o DOM já estará carregado e o template será encontrado. A segunda mudança é no método render() onde o método to_html() é utilizado, criando o HTML a partir do template e dos atributos dinâmicos definidos no PostModel obtidos a partir do método toJSON(). A terceira mudança é a adição do trecho _.bindAll(this, ‘render’, ‘removePost’, ‘refresh’), esse código irá permitir a utilização de this referenciando para a instância de PostView nos métodos definidos.

Ao executar a aplicação, o código desenvolvido renderizará a seguinte tela:

No código criado, são adicionadas callbacks para os eventos “change” e “destroy”. Nesta View o evento “change” será disparado no método fetch(), e seria disparado também no método clear(), caso não fosse definido o atributo silent: true. Tratando este evento com o método render(), fica garantido que as alterações do Model serão refletidas na View. Para testar este recurso, abra o console Javascript do navegador e execute o seguinte código:

postView.model.set({title: "Alteração de Título"});

Ao executá-lo, a View será atualizada automaticamente, renderizando a página a seguir:

Por último, ao remover uma postagem a partir do método removePost(), o callback do evento “destroy” será executado, e ele irá limpar o estado do Model e irá sincronizá-lo novamente com o servidor, obtendo a última postagem existente.

A segunda página é a public/add.html que permitirá incluir uma nova postagem:

<!doctype html>
<html>
    <head>
        <title>Nova Postagem</title>
        <meta charset="UTF-8" />
        <script src="lib/jquery-min.js"></script>
        <script src="lib/underscore-min.js"></script>
        <script src="lib/backbone-min.js"></script>
        <script src="lib/mustache.js"></script>
        <script src="js/models/PostModel.js"></script>
        <script src="js/views/PostFormView.js"></script>
    </head>
    <body>
        <script type="text/template" id="post-form">
            <h2>Adicionar Post</h2>
            <p><label>Title: <input type="text" id="post-title" /></label></p>
            <p><label>Text: <textarea id="post-text"></textarea></label></p>
            <p><input type="submit" value="Salvar" /></p>
        </script>
        <script>
            var postView = new PostFormView();
        </script>
    </body>
</html>

A classe PostFormView é definida no arquivo js/views/PostFormView.js:

var PostFormView = Backbone.View.extend({
    tagName: 'form',
    className: 'page-form',
    id: 'post-form',
    attributes: {
        action: 'posts',
        method: 'POST'
    },
    events: {
        "submit" : "savePost"
    },

    initialize: function(model) {
        _.bindAll(this, 'render', 'savePost', 'goToIndex');

        this.template = $('#post-form').html();
        this.model = new PostModel();

        this.model.on("error", this.showError);
        this.model.on("sync", this.goToIndex);
        this.render();
    },

    render: function() {
        var rendered = Mustache.to_html(this.template);
        this.$el.html(rendered);

        this.titleInput = this.$el.find('#post-title');
        this.textInput = this.$el.find('#post-text');

        $('body').append(this.el);
    },

    savePost: function(e) {
        e.preventDefault();

        var title = this.titleInput.val();
        var text = this.textInput.val();

        this.model.set({
            title: title,
            text: text
        });

        if (this.model.isValid())
            this.model.save();
    },

    showError:function(model, error) {
        window.alert('Ocorreu um erro, motivo: ' + error);
    },

    goToIndex: function() {
        window.location = 'index.html';
    }
});

Esse código irá adicionar callbacks para os eventos “error” e “sync”, assim como a View irá tratar o evento “submit” do formulário, obter os valores dos campos de texto, verificar se os dados são válidos, e criar um novo recurso no servidor. Caso ocorra algum erro de validação, um alert será exibido ao usuário. No evento “sync”, que será chamado quando o servidor retornar para o Backbone que a postagem foi criada, o usuário será redirecionado para a página contendo a última postagem.

E execução de add.html renderizará o seguinte formulário:

Caso algum dos campos não seja preenchido, será exibida uma mensagem similar à apresentada a seguir:

Se os dados forem válidos, o Backbone irá gravar os dados no servidor e a página public/index.html será exibida, contendo a postagem recém inserida:

Código-fonte

O código-fonte de todos os artigos desta séria sobre Backbone.js encontram-se, também, no repositório backbone-tutorial-series do meu GitHub.

Referências

Para a construção deste artigo a documentação do Backbone.js foi utilizada, em conjunto com alguns vídeos do curso de Backbone.js da CodeSchool. Também foi utilizada a documentação do Sinatra, e a documentação do ActiveRecord.

Como foi possível notar em todo o artigo, não foi construída uma listagem das postagens. Foi ressaltado logo no início que a classe Backbone.Model corresponde a uma instância apenas e não fornece métodos para trabalhar com coleções de dados. No próximo artigo, será apresentada a classe Backbone.Collection, com todos os métodos fornecidos para trabalhar com conjuntos de dados, criação da listagem das postagens do simples blog proposto, e também será alterado o backend desenvolvido com Sinatra, para deixá-lo um servidor RESTful mais consistente. Não percam.

Série Backbone.js: Parte 2 – View

Backbone.js

O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Dentre os componentes, encontra-se a View, responsável pela apresentação de dados em uma aplicação MVC. Neste segundo artigo da série de seis artigos sobre Backbone.js, será apresentada a classe Backbone.View, com exemplos práticos, utilização de templates, e integração com jQuery.

Introdução

No primeiro artigo desta série, foi apresentado o framework Backbone.js, seus principais conceitos e aspectos, e uma introdução rápida através de um “Hello World”. De agora em diante serão apresentados individualmente os componentes do Backbone.js de acordo com o artigo da série.

Backbone.View

A classe Backbone.View tem a responsabilidade de apresentar dados e elementos visuais aos usuários da aplicação. Ela representa a camada View, conforme o padrão arquitetural Model-View-Controller (MVC). Existe muita discussão sobre o Backbone aplicar corretamente o MVC, e segundo a documentação oficial, os componentes podem sim estar ligados ao padrão MVC aplicado, por exemplo, no Ruby on Rails.

Sua utilização pode ser simples, conforme apresentado no artigo anterior:

var HelloView = Backbone.View.extend({
    el: $('body'),
    initialize: function() {
        this.render();
    },
    render: function() {
        $(this.el).append("</pre>
        <h1>Hello World</h1>
        <pre>");
    }
});

A primeira coisa a se destacar sobre a classe Backbone.View é que ela é responsável pela apresentação de um Model, contendo a lógica de renderização deste Model. Mais detalhes sobre isso serão abordados no próximo artigo.

No código apresentado, pode-se notar a presença do atributo el. Este atributo representa um elemento DOM onde a View está ou será inserida. O el pode ser construído manualmente, como apresentado, ou pode ser construído baseando-se em outros atributos configurados na classe, explicados a seguir. O último ponto a se notar neste código é a presença do método initialize() que funciona como um construtor para uma classe do framework Backbone.

Para os exemplos a seguir, será considerado o desenvolvimento de um simples blog. A principal premissa de um blog é que o mesmo é composto por diversas postagens. Seguindo essa premissa, nota-se a presença de uma View para apresentar o conteúdo de um Post, o código para essa View pode ser escrito da seguinte forma:

var PostView = Backbone.View.extend({
    tagName: 'article',
    className: 'page-posts',
});

Para o exemplo, foi considerada a tag <article>, introduzida na especificação do HTML5, que traz semântica ao blog, já que uma postagem, em alguns casos, pode ser um artigo. De acordo com os parâmetros da classe apresentada, o elemento HTML final ficará similar à <article class="page-posts">. Para construção deste elemento HTML, a classe Backbone.View utilizará os seguintes atributos como base:

  • tagName – Nome da tag HTML do elemento que conterá a View
  • id – Atributo identificador da View atual
  • className – Classe CSS que estilizará a View
  • attributes – Demais atributos HTML que serão inseridos no elemento

A próxima etapa ao se definir uma View é implementar seu método render(). Esse método é responsável por renderizar a View através dos dados do Model e atualizar o atributo el, preenchendo-o com o HTML necessário para apresentação. Por padrão, o método render() existe na classe Backbone.View, porém não contém nenhuma implementação. Para que a nova classe PostView, renderize um Post o seguinte código pode ser utilizado:

render: function() {
    var htmlGenerated = "<h2>Nome do Post</h2>";
    htmlGenerated += "<p>Conteudo do post</p>";
    $(this.el).html(htmlGenerated);
}

Para ver o resultado do método render(), é necessário instanciar a classe PostView, executar o método render() e verificar o conteúdo do atributo el. Nesse exemplo, o atributo el é apresentado no console Javascript do browser:

var postView = new PostView();
postView.render();
console.log(postView.el);

Ao verificar o console, o seguinte resultado será apresentado:

<article class="page-posts">
    <h2>Nome do Post</h2>
    <p>Conteudo do post</p>
</article>

Esse exemplo utilizou o método $(this.el) padrão da jQuery, o que deixa o código dependente da biblioteca. Para diminuir este acoplamento, a classe Backbone.View possui o atributo $el. Esse atributo é construído automaticamente pelo Backbone, ao detectar a presença do framework jQuery ou Zepto, e é uma instância em cache para o $(this.el). É uma boa prática utilizar esse atributo. Modificando então o método render(), para utilizar o atributo $el, o código ficará da seguinte forma:

render: function() {
    var htmlGenerated = "<h2>Nome do Post</h2>";
    htmlGenerated += "<p>Conteudo do post</p>";
    this.$el.html(htmlGenerated);
}

O código funciona da mesma maneira, dando baixo acoplamento à biblioteca de terceiro utilizada na aplicação. A classe Backbone.View, fornece alguns outros métodos comuns para uma View, como, por exemplo o método remove() e o método make(). O primeiro método irá remover a View do DOM da página, e é equivalente ao método $(view.el).remove() da jQuery. O exemplo abaixo demonstra a utilização deste método.

// Remove a View
postView.remove();

O segundo método, make(), irá criar um novo elemento DOM, de acordo com os atributos informados, e irá retorná-lo. Este método é utilizado para criar internamente o atributo el padrão da Backbone.View.

var list = postView.make("ul", {"class": "list"}, "<li>A list</li>");
$('body').append(list);

Templates

Existem diversos mecanismos de templates que podem ser utilizados junto com a classe Backbone.View. Implementações como Mustache.js, e Underscore.js, entre outros. Como demonstrado nos exemplos anteriores, é possível construir o HTML como strings, e, outra alternativa seria construir os elementos visuais através do método document.createElement. Porém, utilizar templates é uma alternativa melhor, principalmente ao integrar Backbone.View com a classe Backbone.Model, apresentada no próximo artigo. Para os exemplos abaixo será utilizado o mecanismo de template da biblioteca Underscore.js.

Para definir um template na classe PostView, o atributo template é adicionado, em conjunto com o método _.template(), que é o mecanismo de template da Underscore.js:

var PostView = Backbone.View.extend({
    tagName: 'article',
    className: 'page-posts',
    template: _.template("<h2><%= title %></h2><p><%= content %></p>"),
    render: function() {
        this.$el.html(this.template({title: "Nome do Post", content: "Conteudo do Post"}));
    }
});
var postView = new PostView();
postView.render();
console.log(postView.el);

Esse código apresenta a notação especial de template “<%= %>”, que será interpretada pela Underscore.js em conjunto com a Backbone.View e substituirá os identificadores por atributos equivalentes no objeto sendo renderizado. Nesse exemplo, o objeto contém os atributos title e content, encontrados também no template, e renderizados na execução do método render(). Esse código se torna ainda mais prático ao se utilizar a classe Backbone.Model, que será apresentada no próximo artigo.

Eventos

É comum que uma View possua diversos eventos para interagir com o usuário permitindo, por exemplo, fazer uma busca AJAX no evento “blur” de um campo de texto ou algum processamento no evento “click” de um botão. O Backbone possui suporte a eventos na classe Backbone.View, e utiliza internamente a função delegate da jQuery. Para definir eventos na View, é utilizado o atributo events, que é um hash contendo os possíveis eventos da View em questão. Os eventos são definidos no formato {“seletor do evento”: “callback”}, onde callback pode ser o nome de um método definido na classe View em questão, ou uma função concreta. O código abaixo adiciona três eventos à classe PostView.

events: {
    "dblclick" : "fullScreen",
    "click #add-button" : "newPost",
    "blur #username" : "searchUsername"
},
newPost: function() {
    window.alert("Adicionar novo post");
},

searchUsername: function(e) {
    window.alert("Searching username " + e.target.value);
},

fullScreen: function() {
    window.alert("Post full screen");
}

No primeiro evento é tratado o clique duplo na área inteira de uma postagem, para abrí-la em tela cheia. O segundo evento trata a ação de clicar no botão “Adicionar Novo Post”. E o terceiro evento simula a execução de uma busca na lista de usuários do blog, no evento “blur” do input de nome de usuário. Ao se definir um evento sem um seletor específico, como no caso do “dblclick”, o mesmo é atribuído ao elemento HTML gerado por ele.

A atribuição desses eventos é feita pelo método delegateEvents(), da classe Backbone.View, que executa internamente a função delegate() da jQuery. Esse método é chamado ao se construir uma instância da View, garantindo que os eventos sempre estarão aplicados no decorrer da execução da View, a menos que seja implementado o contrário. O método delegateEvents() pode ser utilizado para modificar os eventos em tempo de execução e aceita como parâmetro uma hash similar a definida no atributo events. Para remover os eventos definidos na View, o método undelegateEvents() é utilizado, internamente ele utiliza a função undelegate() da jQuery.

A última etapa para testar a delegação de eventos é adicionar a renderização à página HTML. O código a seguir pode ser utilizado:

var postView = new PostView();
postView.render();
$('body').append(postView.el);

A página renderizada é apresentada na imagem a seguir:

Código-fonte

O código final criado no artigo é apresentado a seguir. Primeiro o código Javascript, que é definido no arquivo PostView.js:

var PostView = Backbone.View.extend({
    tagName: 'article',
    className: 'page-posts',
    template: _.template('<a href="#" id="add-button">Add Post</a><h2><%= title %></h2><p><%= content %></p><h3>Comments</h3><label>Username: <input id="username" type="text" /></label>'),
    events: {
        "dblclick" : "fullScreen",
        "click #add-button" : "newPost",
        "blur #username" : "searchUsername"
    },
    render: function() {
        this.$el.html(this.template({title: "Nome do Post", content: "Conteúdo do Post"}));
    },

    newPost: function() {
        window.alert("Adicionar novo post");
    },

    searchUsername: function(e) {
        window.alert("Searching username " + e.target.value);
    },

    fullScreen: function() {
        window.alert("Post full screen");
    }
});

Por último o código do arquivo index.html:

<!doctype html>
<html>
    <head>
        <title>Backbone Tutorial Series Part 2 - View</title>
        <meta charset="UTF-8" />
        <script src="../lib/jquery-min.js"></script>
        <script src="../lib/underscore-min.js"></script>
        <script src="../lib/backbone-min.js"></script>
        <script src="PostView.js"></script>
        <script>
            $(document).ready(function() {
                var postView = new PostView();
                postView.render();
                $('body').append(postView.el);
            });
        </script>
    </head>
    <body>
    </body>
</html>

O código-fonte de todos os artigos desta séria sobre Backbone.js encontram-se, também, no repositório backbone-tutorial-series do meu GitHub.

Referências

Para a construção deste artigo a documentação do Backbone.js foi utilizada, em conjunto com alguns vídeos do curso de Backbone.js da CodeSchool.

No próximo artigo, será apresentado o Model, que no Backbone é o responsável por representar dados de uma aplicação, conter regras de negócio e define aspectos de persistência (endpoint para gravação, exclusão, entre outros).

Vagrant e atualização dos pacotes da VM Ubuntu 12.04

Há muito tempo eu estava curioso para testar o Vagrant que, para quem não conhece, é uma ferramenta para auxiliar na gestão de configuração através de virtualização, utilizando como base o Oracle VirtualBox. Após ler o post do Elton Minetto, decidi botar a mão na massa e criar uma VM para meu ambiente de desenvolvimento Java e PHP. Comecei instalando a VM com a última versão LTS do Ubuntu:

vagrant box add precise32 http://files.vagrantup.com/precise32.box

Depois disso iniciei um projeto Vagrant com o comando:

vagrant init precise32

E iniciei a máquina virtual com:

vagrant up

Acessar a máquina criada por SSH é bem prático:

vagrant ssh

Estando na máquina, resolvi atualizar os pacotes com os comandos:

sudo apt-get update
sudo apt-get dist-upgrade

Desliguei a máquina virtual com o comando:

vagrant halt

E liguei novamente com o comando:

vagrant up

Depois dessa atualização, o kernel da máquina virtual foi atualizado, e o resultado do comando vagrant up veio acompanhado de um erro:

The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

mount -t vboxsf -o uid=`id -u vagrant`,gid=`id -g vagrant` v-root /vagrant

O problema ocorre porque ao atualizar o kernel, o pacote de Guest Additions precisa ser recompilado com os headers do kernel atual. Para solucionar o problema, foi necessário instalar os headers para o kernel atual:

sudo apt-get install build-essential linux-headers-`uname -r`

E re-adicionar o pacote Guest Additions:

sudo /etc/init.d/vboxadd setup

Ao reiniciar a máquina virtual, o erro desapareceu. Mais informações: Issue #733 no GitHub.

Internacionalização e Localização com Zend Framework

Zend Framework

Na web  atual os websites  e aplicações são acessados por um público distinto de usuários, cada um com sua cultura e idioma. Este fato pode ser percebido pela gama de serviços grandes, como Google e Twitter, que tendem a verificar o idioma nativo do usuário e adequar o conteúdo a este idioma. O Zend Framework é famoso por fornecer componentes para funcionalidades triviais de aplicações web  atuais, e a internacionalização e localização também é suportada nesta gama de componentes. Este artigo demonstrará os principais componentes envolvidos neste processo, com explanações detalhadas e exemplos práticos, incluindo duas abordagens de implementação no MVC do Zend Framework.

Introdução

Internacionalização (i18n) é o processo de construir uma aplicação que possa ser adaptada para diversos idiomas e regiões sem a necessidade de mudanças drásticas nos processos de engenharia do sistema. Localização (l10n) de um sistema é o processo de adaptar um sistema já internacionalizado para uma região ou idioma específico adicionando componentes para cada localização e traduzindo textos. Na localização são incluídos recursos para utilizar formatos de data/hora, moedas, convenções específicas para pluralização de palavras, além de tradução de idiomas.

Em suma, a internacionalização é a capacidade e flexibilidade de um sistema de mudar sua escrita para diversos idiomas utilizando algum mecanismo com essas capacidades, como, por exemplo, um bundle ou gettext. A localização fica com a responsabilidade de escolher e identificar o idioma e necessidades culturais exigidas pelo usuário, definindo qual o idioma internacionalizado deverá ser utilizado.

No Zend Framework o processo de localização e internacionalização é formado por diversos componentes, cada um com uma responsabilidade distinta:

  • Zend_Locale: Suporte de locais disponíveis para a localização dentro de outros componentes do Zend Framework;
  • Zend_Translate: Tradução de textos;
  • Zend_Date: Localização de data/hora;
  • Zend_Currency: Localização de moedas;
  • Zend_Locale_Format: Análise e geração de números localizados.
  • Zend_Locale_Data: Obtém textos padrões localizados como, por exemplo, nomes de países, nomes de linguagem e etc.

Zend_Locale

O Zend_Locale é o principal componente para identificar o idioma e a região de um usuário. Esta identificação é feita através de um locale, que é uma string padronizada que identifica estes parâmetros no formato idioma_REGIÃO. Por exemplo, o locale pt_BR representa o idioma Português do Brasil. Existe uma lista contendo combinações de locales que pode ser utilizada como referência para construir um locale. Todos os componentes do Zend Framework que suportam internacionalização e localização, utilizam o Zend_Locale, ou um locale, para fornecer normalização e formatação conforme o idioma/região do usuário.

Existem algumas formas de se configurar o Zend_Locale:

  • Por um locale específico;
  • Pelo idioma do browser do usuário (obtido por $_SERVER['HTTP_ACCEPT_LANGUAGE']);
  • Pelo idioma do servidor (pela função setlocale() do PHP);
  • A partir dos parâmetros do framework.

Estes exemplos são ilustrados a seguir:

// Construtor padrão, pega o locale do browser
$locale = new Zend_Locale();
// Construtor com um locale específico
$forceLocale = new Zend_Locale('pt_BR');
// Pega o locale do browser
$browserLocale = new Zend_Locale(Zend_Locale::BROWSER);
// Pega o locale do servidor
$serverLocale = new Zend_Locale(Zend_Locale::ENVIRONMENT);
// Pega o locale do framework
$frameworkLocale = new Zend_Locale(Zend_Locale::FRAMEWORK);

Existem casos onde não é possível detectar automaticamente o locale do usuário (como, por exemplo, na linha de comando), e para isso é necessário configurar um locale padrão, conforme o código a seguir:

Zend_Locale::setDefault('pt_BR');

Após a configuração do Zend_Locale, existem duas formas de utilizá-lo nas classes locale-aware (que suportam l10n), a primeira é definir manualmente o locale a ser utilizado e a segunda é configurar o locale para todas os componentes do Zend Framework:

$ptBR = new Zend_Locale('pt_BR');
$enUS = new Zend_Locale('en_US');
// No componente
$date = new Zend_Date(null, null, $enUS);
echo $date;
// Para todos os componentes
Zend_Registry::set('Zend_Locale', $ptBR);
$currency = new Zend_Currency();
echo $currency;

A classe Zend_Locale fornece diversos métodos úteis para o processo de internacionalização de uma aplicação. Dentre estes métodos encontram-se:

  • getLanguage() – Obtém o idioma configurado;
  • getRegion() – Obtém a região (localização) configurada;
  • getTranslationList() – Provê acesso para diversas informações localizadas, como, por exemplo, meses do ano, dias da semana, etc;
  • setLocale() – Muda o locale do objeto;
  • equals() – Compara dois objetos Zend_Locale, indicando se são iguais;
  • toString() – Obtém a string de identificação do locale.

Zend_Translate

Após os componentes do Zend Framework saberem exatamente o idioma/região do usuário, a próxima etapa é fornecer o conteúdo textual da aplicação traduzido. Essa etapa fica à cargo do componente Zend_Translate. Existem diversos adapters para fornecer as traduções da aplicação, alguns deles são listados a seguir:

  • Array – As string são fornecidas a partir de arrays PHP;
  • Gettext – Arquivos de string e traduções gettext;
  • Csv – Arquivos no formato CSV.

O manual do Zend Framework sugere tipos diferentes de estrutura de diretórios para armazenar os textos internacionalizados, neste artigo será utilizada a estrutura single directory, onde existirá um arquivo para cada idioma suportado, dentro da pasta languages. Para maiores informações consulte as referências do artigo. Dentro deste diretório serão criados os arquivos en.phpes.phppt.php, cada um contendo um array com três strings, e será utilizado o adapter Array. O conteúdo dos arquivos é apresentado abaixo:

//en.php
return array(
    'Exemplo' => 'Sample',
    'Internacionalizado' => 'Internationalized',
    'Texto 2' => 'Text 2'
);
// es.php
return array(
    'Exemplo' => 'Ejemplo',
    'Internacionalizado' => 'Internacionalizados',
    'Texto 2' => 'Texto 2'
);
// pt.php
return array(
    'Exemplo' => 'Exemplo',
    'Internacionalizado' => 'Internacionalizado',
    'Texto 2' => 'Texto 2'
);

Uma instância do Zend_Translate pode ser feita com o código a seguir:

$translate = new Zend_Translate(
    array(
        'adapter' => 'array',
        'content' => '/languages/en.php',
        'locale'  => 'en'
    )
);

O parâmetro adapter define qual adapter será utilizado no processo de tradução, content a fonte de dados onde se encontram as traduções e locale define o locale padrão para a fonte de dados informada. A próxima etapa é adicionar mais idiomas para o componente, conforme o código a seguir:

$translate->addTranslation(
    array(
        'content' => '/languages/pt.php',
        'locale' => 'pt'
    )
);
$translate->addTranslation(
array(
'content' => '/languages/es.php',
'locale' => 'es'
)
);

Com o Zend_Translate devidamente configurado, é possível utilizar strings internacionalizadas da seguinte forma:

// Retorna a string internacionalizada
$translate->_("Exemplo");
// Imprime a string internacionalizada
echo $translate->_("Internacionalizado");
// Obtém a string de acordo com o locale
echo $translate->_("Exemplo", "es");

O método _() recebe como parâmetro a chave que identifica o texto internacionalizado, e retorna o texto de acordo com a chave e o locale atual. Esta chave pode ser o texto em si, um índice numérico único, ou uma string única. Opcionalmente o método aceita como segundo parâmetro o locale a ser utilizado.

Além de obter uma string de acordo com o locale pelo método _(), é possível definir o locale atual da instância de Zend_Translate, com o método setLocale. Caso seu parâmetro seja um locale completo (en_US, por exemplo), e exista um arquivo somente identificado pelo nome do idioma, o componente procurará pela tradução mais próxima, no caso apresentado seria o en. E para verificar se uma determinada tradução existe, o método utilizado é o isAvailable. O código abaixo apresenta o uso de ambos os métodos.

// Mudando o locale, usará a tradução "pt"
$translate->setLocale("pt_BR");
echo $translate->_("Exemplo");
// Verifica se existe um locale
if ($translate->isAvailable("it")) {
    echo $translate->_("Pizza", "it");
}

Existem diversas opções e recursos adicionais para o componente Zend_Translate, um destes recursos é o de auto-detectar os arquivos de tradução de um diretório. Como dito anteriormente, o esquema de arquivos de tradução utilizado é o single directory, onde cada idioma suportado estará em seu arquivo específico, que terá como nome o idioma. Ao se configurar o parâmetro scan e content, na instanciação do Zend_Translate, o diretório em content será lido, e cada arquivo de tradução será adicionado automaticamente, sem a necessidade de chamar o método addTranslation. Existe dois valores possíveis para scan: Zend_Translate::LOCALE_FILENAME e Zend_Translate::LOCALE_DIRECTORY, cada um segue uma uma convenção diferente de estrutura de arquivos de internacionalização. Para este exemplo de single directory, o LOCALE_FILENAME é utilizado. O exemplo a seguir ilustra isso:

$translate2 = new Zend_Translate(
    array(
        'adapter' => 'array',
        'content' => 'languages/',
        'scan' => Zend_Translate::LOCALE_FILENAME
    )
);
echo $translate2->_("Exemplo") . "\n";
echo $translate2->_("Exemplo", "es") . "\n";
echo $translate2->_("Exemplo", "en") . "\n";

Existem ainda mais opções, consulte as referências do artigo para a documentação completa e opções do componente.

Aplicação de Exemplo

Depois de um pouco de teoria e estudo sobre a base de i18n e l10n com o Zend Framework é hora de criar uma aplicação simples com suporte a três idiomas. Duas abordagens de implementação serão apresentadas, a primeira será a detecção do idioma do usuário através do browser e a segunda através de URLs da aplicação (rotas).

Primeiramente, é necessário criar uma aplicação Zend Framework:

zf create project internationalization-localization

Os idiomas suportados serão: inglês (en), português (pt), espanhol (es). O adapter utilizado será o array, e os arquivos ficarão na pasta languages. O conteúdo de cada arquivo é apresentado abaixo:

// en.php
return array(
    'Titulo da Pagina' => 'Page Title',
    'Texto Internacionalizado de Exemplo' => 'Internationalized Text Sample',
    "A data atual é: %1\$s" => "The current date is: %1\$s",
    'O valor monetário do locale é:' => 'The currency value with the locale is:',
    'O locale de tradução é:' => 'The translate locale is:',
    'O Zend_Locale é:' => 'The Zend_Locale is:'
);
// es.php
return array(
    'Titulo da Pagina' => 'Título de la Página',
    'Texto Internacionalizado de Exemplo' => 'Ejemplo de Texto Internacionalizado',
    "A data atual é: %1\$s" => "La fecha actual es: %1\$s",
    'O valor monetário do locale é:' => 'El valor monetario de lo locale es:',
    'O locale de tradução é:' => 'La traducción es:',
    'O Zend_Locale é:' => 'El Zend_Locale es:'
);
// pt.php
return array(
    'Titulo da Pagina' => 'Título da Página',
    'Texto Internacionalizado de Exemplo' => 'Texto Internacionalizado de Exemplo',
    "A data atual é: %1\$s" => "A data atual é: %1\$s",
    'O valor monetário do locale é:' => 'O valor monetário do locale é:',
    'O locale de tradução é:' => 'O locale de tradução é:',
    'O Zend_Locale é:' => 'O Zend_Locale é:'
);

Alguns parâmetros de configuração (application/configs/application.ini) podem ser aproveitados em ambas as abordagens. O primeiro passo é configurar o Zend_Locale, conforme apresentado abaixo:

resources.locale.default = "en_US"

Essa configuração, define que o locale padrão da aplicação será o en_US. Ao adicionar esse parâmetro de configuração, o Zend_Application_Resource_Locale configurará o componente Zend_Locale e, após definir as configurações, armazenará o objeto Zend_Locale no Zend_Registry com a chave “Zend_Locale“, o que garante que todos os componentes locale-aware utilizarão o objeto definido.

O próximo passo é configurar o Zend_Translate, conforme os idiomas e arquivos-fonte da aplicação:

resources.translate.adapter = "array"
resources.translate.data = APPLICATION_PATH "/../languages"
resources.translate.options.scan = "filename"
resources.translate.options.disableNotices = true

Similar ao Zend_Locale, essa configuração irá definir uma instância de Zend_Translate para a aplicação através do Zend_Registry, neste caso com a chave “Zend_Translate“. Para exemplificar a utilização de ambos os componentes, o arquivo application/views/scripts/index/index.phtml será modificado para conter o seguinte código:

<?php $locale = Zend_Registry::get('Zend_Locale'); ?>
<html>
    <head>
        <meta charset="UTF-8" />
    </head>
    <body>
        <h1><?php echo $this->translate("Titulo da Pagina"); ?></h1>
        <p><?php echo $this->translate("Texto Internacionalizado de Exemplo"); ?></p>
        <p><?php echo $this->translate("A data atual é: %1\$s", new Zend_Date()); ?>
        <p><?php echo $this->translate("O valor monetário do locale é:"); ?> <?php echo new Zend_Currency(); ?></p>
        <p><?php echo $this->translate("O locale de tradução é:"); ?> <?php echo $this->translate()->getLocale(); ?></p>
        <p><?php echo $this->translate("O Zend_Locale é:"); ?> <?php echo $locale; ?></p>
    </body>
</html>

É possível notar a chamada para $this->translate(), essa chamada irá executar uma instância de Zend_View_Helper_Translate, e internamente chamar o método Zend_Translate::_(), apresentado anteriormente. Outro ponto importante é o coringa “%1\$s”, que define valor dinâmico à string traduzida. Neste caso o valor definido é uma nova instância de Zend_Date. Essa página irá demonstrar tanto a mudança dos valores de formatação dos componentes locale-aware quanto a apresentação dos textos de acordo com o idioma escolhido.

Uma premissa será definida para ambas as abordagens: um locale só será suportado caso existam as traduções correspondentes. Isso significa que, caso não exista uma determinada tradução para o locale, o mesmo não será configurado, sendo escolhido então o locale padrão (para este caso en_US).

Através do Browser do Usuário

Obter os parâmetros de locale a partir do browser do usuário não é uma tarefa complicada, conforme apresentado no início do artigo. Para essa abordagem o único requisito é a implementação de um Zend_Controller_Plugin para detectar qual o locale do usuário, verificar se existem traduções para o mesmo, e configurar corretamente o Zend_Locale e Zend_Translate, seguindo a premissa definida anteriormente.

O seguinte código define este plugin, devendo estar em library/FernandoMantoan/Plugin/Internationalization.php:

class FernandoMantoan_Plugin_Internationalization extends Zend_Controller_Plugin_Abstract
{
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $locale = Zend_Registry::get('Zend_Locale');
        $translate = Zend_Registry::get('Zend_Translate');
        if (!$translate->isAvailable($locale->getLanguage())) {
            $locale->setLocale("en_US");
            $translate->setLocale("en");
        }
        Zend_Registry::set('Zend_Locale', $locale);
        Zend_Registry::set('Zend_Translate', $translate);
    }
}

Primeiramente os objetos Zend_Translate e Zend_Locale são obtidos do container Zend_Registry, é feita a verificação da tradução para o locale, caso não ela não existe, o locale padrão é definido. Por último, os objetos são atualizados no container Zend_Registry. Para adicionar o plugin no processo de despacho do Zend Framework, os seguintes parâmetros de configuração são necessários no arquivo application/configs/application.ini:

autoloaderNamespaces[] = "FernandoMantoan"
resources.frontController.plugins.internationalization = "FernandoMantoan_Plugin_Internationalization"

Para testar o código é necessário modificar o idioma preferido no browser utilizado. No Mozilla Firefox, por exemplo, esta opção encontra-se em: Editar > Preferências > Idiomas (idioma preferencial). Ao se definir en-US, a página resultante é a seguinte:

Mudando para pt-BR o resultado é o seguinte:

Mudando para es-ES o resultado é:

E, caso seja definido um locale não suportado, como fr-FR, o resultado é:

Uma abordagem simplória, mas que funciona muito bem.

Através de Rotas

A segunda abordagem é através de rotas. Dentro da gama de componentes MVC do Zend Framework existe a família de componentes Zend_Controller_Router_*, que são utilizados para ler e definir rotas da aplicação. Uma rota é uma URI (Unified Resource Identifier) escrita após a URL base (endereço do servidor ou da aplicação, por exemplo), que é mapeada pelos componentes do Zend Framework para achar o Controller e Action correspondentes, assim como definir parâmetros dinâmicos. Para o exemplo em questão será definido um novo padrão de rotas, composto por: idioma, módulo, controlador, action e parâmetros adicionais. Um exemplo disso seria: http://servidor/en/products/list, que representa a listagem de produtos no idioma inglês.

Para essa configuração, será definido o método _initRoutes na classe application/Bootstrap.php, e o mesmo terá o seguinte conteúdo:

protected function _initRoutes()
{
    $frontController = Zend_Controller_Front::getInstance();
    $router = $frontController->getRouter();
    $router->removeDefaultRoutes();
    $router->addRoute(
        'fullRoute',
        new Zend_Controller_Router_Route('/:lang/:module/:controller/:action',
            array('lang' => ':lang')
        )
    );
    $router->addRoute(
        'languageControllerAction',
        new Zend_Controller_Router_Route('/:lang/:controller/:action',
            array('lang' => ':lang')
        )
    );
    $router->addRoute(
        'language',
        new Zend_Controller_Router_Route('/:lang',
            array('lang' => 'en',
                'module' => 'default',
                'controller' => 'index',
                'action' => 'index'
            )
        )
    );
    $router->addRoute(
        'languageController',
        new Zend_Controller_Router_Route('/:lang/:controller',
            array('lang' => 'en',
                'module' => 'default',
                'controller' => 'index',
                'action' => 'index'
            )
        )
    );
}

A primeira etapa do método é apagar as rotas padrões definidas pelo Zend Framework, através do método removeDefaultRoutes(). As linhas que seguem adicionam rotas para suportar as seguintes URIs:

  • /idioma/modulo/controlador/acao
  • /idioma/controlador/acao
  • /idioma/controlador
  • /idioma

E, define um valor padrão para cada parâmetro, caso algum deles seja omitido:

  • Idioma: en
  • Módulo: default
  • Controlador: IndexController
  • Action: indexAction

Com as rotas configuradas, é necessário criar um Zend_Controller_Plugin, localizado em library/FernandoMantoan/Plugin/LanguageRouteDetector.php, que ficará responsável por ler os parâmetros definidos nas rotas e configurar o Zend_Locale e Zend_Translateadequadamente:

class FernandoMantoan_Plugin_LanguageRouteDetector extends Zend_Controller_Plugin_Abstract
{
    protected $_supportedLanguages = array('pt', 'en', 'es');
    protected $_regions = array('pt' => 'BR', 'en' => 'US', 'es' => 'ES');

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $lang = $request->getParam('lang', '');

        $zendLocale = Zend_Registry::get('Zend_Locale');
        $zendTranslate = Zend_Registry::get('Zend_Translate');

        $locale = '';

        if (!in_array($lang, $this->_supportedLanguages)) {
            $locale = 'en_US';
        } else {
            $locale = $lang . '_' . $this->_regions[$lang];
        }

        $zendLocale->setLocale($locale);
        $zendTranslate->setLocale($locale);

        Zend_Registry::set('Zend_Locale', $zendLocale);
        Zend_Registry::set('Zend_Translate', $zendTranslate);
    }
}

Os dois primeiros arrays definem idiomas e regiões, e serão utilizados para verificar se a rota acessada contém um idioma válido da aplicação. A primeira etapa é verificar se o parâmetro “lang” definido na rota corresponde a um idioma válido, caso não seja um idioma válido o utilizado será “en_US“. Caso o idioma seja válido, obtém a região do idioma em questão e constrói um locale para configurar corretamente os objetos Zend_Translate e Zend_Locale. Esse plugin deve ser configurado no application/configs/application.ini, com as seguintes linhas:

autoloaderNamespaces[] = "FernandoMantoan"
resources.frontController.plugins.internationalization = "FernandoMantoan_Plugin_LanguageRouteDetector"

Para testar essa abordagem, é necessário modificar os parâmetros da URL conforme o idioma escolhido. Os websites e aplicações web que utilizam rotas diferentes para cada idioma fornecem links para elas, identificando-as através de bandeiras. Para isso, será modificado o arquivo application/views/scripts/index.phtml para adicionar o seguinte código:

<ul>
	<li><a href="<?php echo $this->url(array('lang' => 'pt')); ?>">pt</a></li>
	<li><a href="<?php echo $this->url(array('lang' => 'en')); ?>">en</a></li>
	<li><a href="<?php echo $this->url(array('lang' => 'es')); ?>">es</a></li>
</ul>

Ao clicar em cada um dos links, os textos e o idioma da página serão atualizados, e é possível notar que a URL apresentada na barra de endereços reflete o idioma escolhido em cada um dos links. A nível de teste, ao acessar “/fr” ou “/it“, o idioma é verificado e o padrão “en” é adotado, pelo fato de “fr” e “it” não serem suportados pela aplicação.

Conclusões

Atualmente o suporte a múltiplos idiomas é um ponto trivial de uma aplicação web, principalmente pela gama de usuários espalhados pelo mundo que podemo ter acesso às diversas aplicações da internet. O Zend Framework provê os componentes necessários para essa tarefa, e fornece a flexibilidade na implementação desse suporte, como demonstrado no artigo. Existem diversas formas de implementar a i18n e l10n, nesse artigo foram apresentadas duas formas simples, se você leitor utiliza outra abordagem, compartilhe nos comentários ou faça fork do repositório e um pull request da sua implementação.

Apesar a da lista de componentes localizáveis ser extensa, foram apresentados mais detalhadamente os componentes Zend_LocaleZend_Translate por serem específicos para a configuração de todo o processo de internacionalização dos demais componentes. Além deste fato, componentes como o Zend_Date, fornecem funcionalidades que vão além da formatação de acordo com a localização do usuário, como, por exemplo, comparação de datas, diferença de datas, e outros recursos. Para maiores informações consulte as referências do artigo.

Código-Fonte

Todo o código-fonte apresentado no artigo encontra-se no repositório zf-i18n-l10n-samples no GitHub.

Referências

Zend_Paginator_Adapter para Doctrine 2

O componente Zend_Paginator simplifica bastante a paginação de dados e fornece Adapters para suportar várias fontes de dados (objetos Zend_Db, arrays puras, etc). O FernandoMantoan_Paginator_Adapter_Doctrine foi criado para ser um Zend_Paginator_Adapter que fornece paginação para os objetos QueryBuilder do Doctrine 2.

Configuração

O primeiro passo é baixar o componente para o seu projeto:

git clone https://github.com/fernandomantoan/zf1-doctrine2-paginator-adapter

Basta copiar a pasta library/FernandoMantoan para a pasta library do seu projeto Zend Framework, e adicionar no application/configs/application.ini as seguintes linhas:

autoloaderNamespaces[] = FernandoMantoan

Uso

O Adapater necessita de um objeto QueryBuilder, para construir as consultas para contagem das páginas e paginação. O exemplo a seguir cria um QueryBuilder:

$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('c')
             ->from('Entity\Contacts')
             ->orderBy('c.birthdate');

Tendo o objeto QueryBuilder criado, basta instanciar o Adapter e o Zend_Paginator:

$adapter = new FernandoMantoan_Paginator_Adapter_Doctrine($queryBuilder);
$paginator = new Zend_Paginator($adapter);
$paginator->setCurrentPageNumber($currentPage)
          ->setItemCountPerPage(10);

Próximos Passos

O componente é bem simples e direto, foi criado para a utilização em um projeto e para suprir as necessidades do mesmo. Quem tiver interesse em contribuir, sugerir funcionalidades ou reportar bugs, basta acessar no GitHub, toda contribuição é bem-vinda.