Tutorial Orbit
Este tutorial mostra como criar uma aplicação blog simples, apoiada por uma base de dados. Ela é muito simples porque não inclui nenhuma página de administração ("admin"); você tem que adicionar posts diretamente na base de dados (embora você possa postar através de um console Lua, e este tutorial mostrará como), mas tem uma interface para comentários nos posts.
O tutorial parte do princípio que você já tem o Orbit instalado (preferencialmente como parte do Kepler, ou pelo LuaRocks, e já tem um servidor web que aceita configuração WSAPI (o servidor web Xavante que vem com o Kepler é uma boa escolha).
Os códigos fontes para este blog estão nas pastas ´samples´ da distribuição do Orbit. Se você instalou o Orbit pelo Kepler ou LuaRocks, veja dentro da pasta ´rocks´ da sua instalação.
Inicialização
Você deve criar um arquivo ´blog.lua´, que será o arquivo principal de códigos fontes da nossa aplicação. A primeira coisa que você deve por no arquivo é o código para carregar o Orbit e outras bibliotecas que você usará no seu aplicativo.
require "orbit" require "orbit.cache" require "markdown"
Neste exemplo usaremos a página de cache do Orbit, e o parser Markdown para marcar os posts.
Todas as aplicações Orbit são módulos Lua, portanto incluiremos esta linha:
module("blog", package.seeall, orbit.app)
Isso configura o módulo ´blog´ e o inicializa como uma aplicação Orbit.
´orbit.app´ coloca muitas coisas no namespace do módulo ´blog´. Os mais importantes são os metódos ´dispatchget´, ´dispatchpost´ e ´model´ que permitem que você defina a funcionalidade principal da sua aplicação. Eles também definem a variável ´mapper´ que o Orbit usa para criar os modelos (Orbit inicializa essa variável para o seu mapeador objeto-relacional padrão). Por último, eles também definem os controles padrões para os códigos de erros 404 e 500 HTTP como as variáveis ´notfound´ e ´servererror´, respectivamente. Redefina essas variáveis se quiser páginas customizadas para a sua aplicação.
Vamos carregar um script de configuração para o blog (um modelo comum em aplicações). Você pode pegar este script aqui.
require "blog_config"
As próximas linhas carregam um driver de base de dados LuaSQL (definido na configuração), e configura o mapeador objeto relacional do Orbit.
require("luasql." .. database.driver) local env = luasql[database.driver]() mapper.conn = env:connect(unpack(database.conn_data)) mapper.driver = database.driver
O mapeador do Orbit precisa usar uma conexão de base de dados, e de qual driver você estiver usando (no momento apenas o "sqlite3" e "mysql" são aceitos).
Você precisa iniciar o mapeador antes de criar os modelos de sua aplicação porque o mapeador do Orbit consulta a base de dados durante a criação de modelos para pegar o esquema. Falando em esquema, agora é uma boa hora para criar a base de dados do seu blog. Parto do princípio que você está usando o SQLite3. Crie uma base de dados ´blog.db´ com o script SQL abaixo:
CREATE TABLE blog_post ("id" INTEGER PRIMARY KEY NOT NULL, "title" VARCHAR(255) DEFAULT NULL, "body" TEXT DEFAULT NULL, "n_comments" INTEGER DEFAULT NULL, "published_at" DATETIME DEFAULT NULL); CREATE TABLE blog_comment ("id" INTEGER PRIMARY KEY NOT NULL, "post_id" INTEGER DEFAULT NULL, "author" VARCHAR(255) DEFAULT NULL, "email" VARCHAR(255) DEFAULT NULL, "url" VARCHAR(255) DEFAULT NULL, "body" TEXT DEFAULT NULL, "created_at" DATETIME DEFAULT NULL); CREATE TABLE blog_page ("id" INTEGER PRIMARY KEY NOT NULL, "title" VARCHAR(30) DEFAULT NULL, "body" TEXT DEFAULT NULL);
O mapeador do Orbit usa o campo ´id´ para identificar objetos na base de dados, portanto você precisará de um para cada um dos tipos de objetos que estiver mapeando.
Por último, vamos iniciar o cache de páginas do Orbit antes de criar nossos modelos:
local cache = orbit.cache.new(blog, cache_path)
O cache de páginas acelera o acesso a qualquer página que você cacheie, mas você precisará ser cuidadoso e limpar o cache para uma página quando qualquer conteúdo nela mudar. Veremos como cachear e invalidar páginas na seção de controle deste tutorial.
Criando Modelos
Nossa aplicação de blog tem três tipos de objetos: posts, comentários e páginas "estáticas" (como a página de "Sobre" do blog, por exemplo). Não é coincidência que também temos três tipos de tabelas na base de dados, cada tabela mapeia um tipo de objeto que a nossa aplicação reconhece, e para cada tipo criaremos um modelo. Primeiro criaremos um objeto modelo para posts:
posts = blog:model "post"
O parâmetro para o método ´model´ é o nome de uma tabela na base de dados. O objeto ´posts´ que esse método cria representa a coleção de posts, e ao mesmo tempo é um protótipo para todos os posts (veremos as implicação disso em breve). O mapeador do Orbit cria um objeto funcional por conta própria: você pode fazer ´post:find(3)´, por exemplo, e pegar o post com ´id´ 3, ou ´post:findall("ncomments < ?", { 3, order = "published_at desc"})´ e ter uma lista de todos os posts com menos de três comentários, do mais recente ao mais antigo.
Você pode usar o método ´find´ pré-definido para todas as buscas na base de dados, mas ajuda simplificar buscas comuns nos seus métodos. Você pode fazer isso adicionando métodos no objeto ´posts´:
function posts:find_recent() return self:find_all("published_at is not null", { order = "published_at desc", count = recent_count }) end
As linhas acima adicionam um método ´find_recent´ no objeto ´posts´, retornando uma lista dos posts publicados mais recentementes (o número está no script de configuração), do mais recente ao mais antigo. A aplicação irá usar este método para gerar a lista de posts na home page, assim como a seção "Posts recentes" na lateral do blog.
Outra característica do nosso blog será a página de arquivo que mostra todos os posts de um certo mês e ano. Definiremos um método para isto também:
function posts:find_by_month_and_year(month, year) local s = os.time({ year = year, month = month, day = 1 }) local e = os.time({ year = year + math.floor(month / 12), month = (month % 12) + 1, day = 1 }) return self:find_all("published_at >= ? and published_at < ?", { s, e, order = "published_at desc" }) end
Este é o método mais complicado, já que temos que converter de um mês e ano simples para data de começo e fim no formato Lua padrão. Por último, também definiremos um método para retornar todos os meses (e anos) que tem posts, para mais tarde gerar os links para a seção "Arquivo" no sidebar:
function posts:find_months() local months = {} local previous_month = {} local posts = self:find_all("published_at is not null", { order = "published_at desc" }) for _, post in ipairs(posts) do local date = os.date("*t", post.published_at) if previous_month.month ~= date.month or previous_month.year ~= date.year then previous_month = { month = date.month, year = date.year, date_str = os.date("%Y/%m", post.published_at) } months[#months + 1] = previous_month end end return months end
Este método pega todos os posts na base de dados, ordenados por data, e iterates over them armazenando cada par de mês e ano numa lista.
We can also define methods for individual post objects by defining methods
in the posts
object, the only difference is how they are used (you use find_recent
by doing posts:find_recent()
, but you will use find_comments
by doing p:find_comments()
,
where p
is a particular post object. We will define a method to retrieve all comments of
a post:
Também podemos definir métodos para objetos de posts individuais definindo métodos no objeto ´posts´, a única diferença é como eles são usados (você usa ´findrecent´ criando ´posts:findrecent()´, mas você usará ´findcomments´ criando ´p:findcomments()´, onde ´p´ é um objeto post específico. Definiremos um método para recuperar todos os comentários de um post:
function posts:find_comments() return comments:find_all_by_post_id{ self.id } end
Este método usa um método pré-definido do objeto ´comments´ (que criaremos em breve) que pega todos os comentários com o campo ´post_id´ iguais ao id do post atual (´self.id´). Este método cria uma relação entre os posts e os comentários; uma versão futura do mapeador Orbit permitirá que você defina isso declaradamente.
Criar o objeto ´comments´ é simples:
comments = blog:model "comment"
Vamos adicionar um método de conveniência para comentários que constróem o link de comentário a partir de seus dados:
function comments:make_link() local author = self.author or strings.anonymous_author if self.url and self.url ~= "" then return "" .. author .. "" elseif self.email and self.email ~= "" then return "" .. author .. "" else return author end end
O objeto ´pages´ é mais simples ainda, a funcionalidade padrão fornecida pelo mapeador Orbit é suficiente, então criaremos com o ´model´:
pages = blog:model "pages"
Isso conclui a parte "model" da nossa aplicação. Agora podemos seguir para definir a flow página da aplicação, definindo controllers e mapeando-os para URLs.
Definindo controladores
Controladores são a interface entre a web e sua aplicação. Com o Orbit você pode
mapear a parte path das URLs de sua aplicação (pore exemplo, em http://myserver.com/myapp.ws/foo/bar
o path é /foo/bar) em controladores. Em termos Lua, um controlador Orbit é uma função que recebe
um objeto request/response (chamado web
) e parâmetros obtidos do path, e retorna um texto
que é enviado para o cliente (geralmente HTML, mas pode ser XML ou mesmo uma imagem).
Você mapeia paths em controladores com os métodos dispatch_get
e dispatch_post
para requisições GET e POST respectivamente. O primeiro parâmetro destes métodos é o
controlador, uma função Lua, e todos os outros parâmetros são os padrões de mapeamento,
escritos na sintaxe de padrões de strings de Lua, de forma que um controlador pode responder
a mapeamentos diversos.
Abaixo esta o controlador para a pagina principal do blog:
function index(web) local ps = posts:find_recent() local ms = posts:find_months() local pgs = pgs or pages:find_all() return render_index(web, { posts = ps, months = ms, recent = ps, pages = pgs }) end blog:dispatch_get(cache(index), "/", "/index")
A última linha estabelece o mapeamento entre a função index
e root da aplicação.
O chamado do cache
define o caching para esse controlador,
usando o cache que criamos anteriormente (este é outro idioma comum do Lua,
funciona como"decorators").
O controlador index
mostra todos os posts recentes, e é bem direto. Ele somente chama os dados modelo solicitados apartir do banco de dados, depois chama uma função auxiliar (chamada view na tecnologia MVC) para renderizar o código HTML atual.
Outro importante controlador é o que mostra posts únicos:
function view_post(web, post_id, comment_missing) local post = posts:find(tonumber(post_id)) if post then local recent = posts:find_recent() local pgs = pages:find_all() post.comments = post:find_comments() local months = posts:find_months() return render_post(web, { post = post, months = months, recent = recent, pages = pgs, comment_missing = comment_missing }) else return not_found(web) end end blog:dispatch_get(cache(view_post), "/post/(%d+)")
Aqui nós mapeamos todos os paths como /post/53 para o controlador view_post
. O pattern captura os números, e é passado por todo controlador pelo Orbit. Para /post/53, o controlador recebe a string ''53'' como post_id
e usa isto para chamar o post correspondente. Novamente, a renderização do HTML esta factored out para outra função, e este controlador esta cached.
Se nenhum post com este id for encontrado, o controlador default de páginas perdidas será chamado blog.not_found
(orbit.app
coloque isto no namespace do blog
)
Arquivos e paginas tem estruturas similares:
function view_archive(web, year, month) local ps = posts:find_by_month_and_year(tonumber(month), tonumber(year)) local months = posts:find_months() local recent = posts:find_recent() local pgs = pages:find_all() return render_index(web, { posts = ps, months = months, recent = recent, pages = pgs }) end blog:dispatch_get(cache(view_archive), "/archive/(%d%d%d%d)/(%d%d)") function view_page(web, page_id) local page = pages:find(tonumber(page_id)) if page then local recent = posts:find_recent() local months = posts:find_months() local pgs = pages:find_all() return render_page(web, { page = page, months = months, recent = recent, pages = pgs }) else not_found(web) end end blog:dispatch_get(cache(view_page), "/page/(%d+)")
Os arquivos utilizam o mesmo layout que o index, logo ele reutiliza seu gerador de HTML. Os arquivos também extraem dois parametros do path, o mês e o ano, logo os paths são como /archive/2008/05.
Finalmente você pode também pode definir arquivos estáticos com o método de conveniência dispatch_static
.
blog:dispatch_static("/head%.jpg", "/style%.css")
Esses também são patterns, logo os pontos são escapados. Você pode definir em sua aplicação, uma pasta como estática com blog:dispatch_static("/templates/.+")
. O Orbit sempre procura pelos arquivos nas pastas das aplicações. Claro que você pode deixar sua aplicação comportar somente conteúdos dinâmicos e deixar seu servidor web servir conteúdo estátic; dispatch_static
é só uma conveniência para ter aplicações "zero-configuration"
Tem um controlador para adicionar comentários. Este irá responder ao POST em vez de receber:
function add_comment(web, post_id) local input = web.input if string.find(input.comment, "^%s*$") then return view_post(web, post_id, true) else local comment = comments:new() comment.post_id = tonumber(post_id) comment.body = markdown(input.comment) if not string.find(input.author, "^%s*$") then comment.author = input.author end if not string.find(input.email, "^%s*$") then comment.email = input.email end if not string.find(input.url, "^%s*$") then comment.url = input.url end comment:save() local post = posts:find(tonumber(post_id)) post.n_comments = (post.n_comments or 0) + 1 post:save() cache:invalidate("/") cache:invalidate("/post/" .. post_id) cache:invalidate("/archive/" .. os.date("%Y/%m", post.published_at)) return web:redirect(web:link("/post/" .. post_id)) end end blog:dispatch_post(add_comment, "/post/(%d+)/addcomment")
O controlador add_comment
primeiro valida o input, delegando ao view_post
se o campo de comentário estiver vazio (o qual irá mostrar uma menssagem de erro na página). Você acessa o parâmetro POST pela tabela web.input
, que é convenientemente aliased para um input
local variável.
O controlador cria um novo objeto comment, preenche com dados e depois salva no banco de dados. Ele também atualiza o objeto post para aumentar o número de comment o post tem por um, alem de salvar. Depois continua para invalidar (em cache) todas as páginas que talvez mostre esta informação: o index, a página de postagem e os arquivos para este post em particular. Finalmente, ele redireciona para a página de postagem, que irá mostrar o novo comentário. Este é um idioma comum na programação web chamada POST-REDIRECT-GET, onde todo POST é seguido por um redirecionamente para um GET. Isso evita a dupla postagem no caso do usuário carregar a página novamente.
A única coisa que resta agora é a geração de HTML. Está é um tópico da próxima sessão.
Visualizações: Gerando HTML
Visualizações é o último componente do trio MVC. Para o Orbit, visualizações são funções simples que geram conteúdo (geralmente HTML), e são estritamente opcionais, o que significa que você pode devolver conteúdo diretamente do seu controle. Mas ainda é bom ter prática em programação para separar controles e visualizações.
Como você gera conteúdo é escolha sua: concatene correntes Lua, use ´table.concat´, use um template de biblioteca de terceiros... Orbit fornece geração de HTML/XML programático através de órbit.htmlify´, mas você está livre para usar qualquer método que preferir. Neste tutorial manteremos a geração programática, embora, assim como outros métodos (strings retas, Cosmo, etc.) são inteiramente documentadas em outro lugar.
Quando você htmlify uma função, o Orbit muda o ambiente da função permitindo que você gere HTML chamando as tags de funções. É melhor mostrar como funciona do que explicar, então aqui vai a visualização básica da aplicação do blog, ´layout´:
function layout(web, args, inner_html) return html{ head{ title(blog_title), meta{ ["http-equiv"] = "Content-Type", content = "text/html; charset=utf-8" }, link{ rel = 'stylesheet', type = 'text/css', href = web:static_link('/style.css'), media = 'screen' } }, body{ div{ id = "container", div{ id = "header", title = "sitename" }, div{ id = "mainnav", _menu(web, args) }, div{ id = "menu", _sidebar(web, args) }, div{ id = "contents", inner_html }, div{ id = "footer", copyright_notice } } } } end
Esta visualização é um decorador para outras visualizações, e gera o boilerplate para cada página do blog (cabeçalho, rodapé, sidebar). Você pode ver as funções de gerador HTML por todo o código, como ´title´, ´html´, ´head´, ´div´. Cada um tem ou uma string ou uma tabela, e gera o HTML correspondente. Se você dispensar uma tabela, a parte de banco de dados é concatenada e usada como conteúdo, enquanto a parte hash é usada como atributos HTML para aquela tag. Uma tag sem conteúdo gera um tag self-closing (´meta´ e ´link´ no código acima).
Digno de nota no código acima são as chamadas ´web:staticlink´ e as funções ´menu´ e ´sidebar´.O método ´staticlink´ gera um link para um recurso estático da aplicação, tirando o SCRIPT_NAME da URL (por exemplo, se a URL é http://myserver.com/myblog/blog.ws/index irá voltar como /myblog/style.css como o link).
As funções ´menu´ e ´sidebar´ são apenas visualizações de ajuda para gerar a barra de menu e sidebar do blog:
function _menu(web, args) local res = { li(a{ href= web:link("/"), strings.home_page_name }) } for _, page in pairs(args.pages) do res[#res + 1] = li(a{ href = web:link("/page/" .. page.id), page.title }) end return ul(res) end function _sidebar(web, args) return { h3(strings.about_title), ul(li(about_blurb)), h3(strings.last_posts), _recent(web, args), h3(strings.blogroll_title), _blogroll(web, blogroll), h3(strings.archive_title), _archives(web, args) } end
Aqui você vê uma mistura de idiomas básicos do Lua (preenchendo uma tabela e passando para uma função concatenada) e o HTML programático do Orbit. Eles também usam o método ´web:link´, que gera links intra-aplicação. A função ´sidebar´ usa mais funções de conveniência, para fatorar melhor:
function _blogroll(web, blogroll) local res = {} for _, blog_link in ipairs(blogroll) do res[#res + 1] = li(a{ href=blog_link[1], blog_link[2] }) end return ul(res) end function _recent(web, args) local res = {} for _, post in ipairs(args.recent) do res[#res + 1] = li(a{ href=web:link("/post/" .. post.id), post.title }) end return ul(res) end function _archives(web, args) local res = {} for _, month in ipairs(args.months) do res[#res + 1] = li(a{ href=web:link("/archive/" .. month.date_str), blog.month(month) }) end return ul(res) end
Note como essas funções não chamam nada no modelo, apenas usam qualquer dado que foi passado (desde o controle).
Agora podemos ir para as funções. de visualização principal. Começaremos com a mais fácil e menor, para páginas renderizadas.
function render_page(web, args) return layout(web, args, div.blogentry(markdown(args.page.body))) end
Isto é uma chamada direta para o ´layout´, passando o corpo da página dentro de um ´div´. A única coisa importante é a sintaxe ´div.blogentryp´, que gera um ´div´ com um atributo ´class´ igual ao "blogentry", ao invés de um ´div´ direto.
Seguindo em frente, escreveremos a visualização para páginas index (e páginas de arquivo):
function render_index(web, args) if #args.posts == 0 then return layout(web, args, p(strings.no_posts)) else local res = {} local cur_time for _, post in pairs(args.posts) do local str_time = date(post.published_at) if cur_time ~= str_time then cur_time = str_time res[#res + 1] = h2(str_time) end res[#res + 1] = h3(post.title) res[#res + 1] = _post(web, post) end return layout(web, args, div.blogentry(res)) end end
Novamente misturamos Lua com gerador programático, e parte fatoral do emissor (o próprio HTML para o corpo dos posts) para outra função (poderemos reutilizar esta função para visualização de apenas um post). O único pedaço incomum de lógica é o de implementar datas especiais, o código só publica quando a data muda, portanto muitos posts do mesmo dia aparecem com a mesma data.
A ajuda do ´_post´ é bem simples:
function _post(web, post) return { markdown(post.body), p.posted{ strings.published_at .. " " .. os.date("%H:%M", post.published_at), " | ", a{ href = web:link("/post/" .. post.id .. "#comments"), strings.comments .. " (" .. (post.n_comments or "0") .. ")" } } } end
Agora podemos seguir para a piece-de-resistance, a visualização que renderiza posts únicos, junto com seus comentários, e o formulário "post a comment":
function render_post(web, args) local res = { h2(span{ style="position: relative; float:left", args.post.title } .. " "), h3(date(args.post.published_at)), _post(web, args.post) } res[#res + 1] = a{ name = "comments" } if #args.post.comments > 0 then res[#res + 1] = h2(strings.comments) for _, comment in pairs(args.post.comments) do res[#res + 1 ] = _comment(web, comment) end end res[#res + 1] = h2(strings.new_comment) local err_msg = "" if args.comment_missing then err_msg = span{ style="color: red", strings.no_comment } end res[#res + 1] = form{ method = "post", action = web:link("/post/" .. args.post.id .. "/addcomment"), p{ strings.form_name, br(), input{ type="text", name="author", value = web.input.author }, br(), br(), strings.form_email, br(), input{ type="text", name="email", value = web.input.email }, br(), br(), strings.form_url, br(), input{ type="text", name="url", value = web.input.url }, br(), br(), strings.comments .. ":", br(), err_msg, textarea{ name="comment", rows="10", cols="60", web.input.comment }, br(), em(" *" .. strings.italics .. "* "), strong(" **" .. strings.bold .. "** "), " [" .. a{ href="/url", strings.link } .. "](http://url) ", br(), br(), input.button{ type="submit", value=strings.send } } } return layout(web, args, div.blogentry(res)) end
São muitos códigos para se digerir de uma vez, então vamos aos poucos. As primeiras linhas geram o corpo do post, usando a ajuda ´post´. Depois temos a lista de comentários, novamente com o corpo de cada comentário gerado por uma ajuda ´comment´. No meio temos uma mensagem de erro que é gerada se o usuário tentar postar um comentário vazio, e então o formulário "add a comment". Um formulário precisa de muito HTML, então tem bastante código, mas é um HTML bem básico e auto-explicatório (torná-lo bonito é responsabilidade do style sheet).
A ajuda ´_comment´ é bem simples:
function _comment(web, comment) return { p(comment.body), p.posted{ strings.written_by .. " " .. comment:make_link(), " " .. strings.on_date .. " " .. time(comment.created_at) } } end
Por último, precisamos configurar todas essas funções de visualização para gerador programático de HTML:
orbit.htmlify(blog, "layout", "_.+", "render_.+")
A função ´orbit.htmlify´ pega uma tabela e uma lista de modelos, e configura todas as funções nessa tabela com nomes que casem com um dos modelos para gerar HTML. Aqui configuraremos a função ´layout´, todas as funções ´render´, e todas as ajudas (as funções começando com ´´´).
Distribuição
Para esta parte do tutorial é melhor você utilizar o diretório samples/blog
da distribuição Orbit (novamente, procure no diretório rocks
caso você tenha instalado o Orbit via Kepler ou LuaRocks). Uma aplicação Orbit é uma aplicação WSAPI, portanto a distribuição é bastante simples, basta copiar todos os arquivos (blog.lua
, blog_config.lua
, blog.db
, head.jpg
, e style.css
) para um diretório em sua raiz web (se você instalou Kepler, este seria o diretório kepler/htdocs
), e criar um script disparador neste diretório. O script disparador é bem curto (chame-o de blog.ws
):
#!/usr/bin/env wsapi.cgi require "blog" return blog
#!/usr/bin/env wsapi.cgi require "blog" return blog
Dependendo de sua configuração, você pode ter que instalar os rocks luasql-sqlite3
e markdown
antes de executar sua aplicação. Feito isso basta iniciar o Xavante, apontar o seu browser para blog.ws, e você deve ver a página inicial do blog. Se você criou um arquivo blog.db a partir do zero você não verá nenhum post. A aplicação de blog em `samples/blog' inclui um arquivo blog.db já contendo posts e comentários de exemplo.