Por trás do site IngressoPrático - O mapa de assentos - parte 2
Eriksen Costa29.11.2011 - 14:48
Você está lendo a parte dois de Por trás do site IngressoPrático, uma série em quatro posts que mostra como nós usamos o Symfony2 para desenvolver o site.
O mapa de assentos foi um dos requisitos do projeto que nos fez rejeitar a adoção de um sistema de carrinho de compras. Precisava carregar rápido já que a maior parte da interação de venda dos ingressos seria feito através dele. Os assentos deveriam ter quatro estados diferentes:
- Disponível (verde)
- Reservado (amarelo)
- Vendido (vermelho)
- Indisponível (cinza)
Um exemplo de mapa de assentos:
Os trechos de código e diagramas de classe foram simplificados com o objetivo de ilustrar como nós organizamos o código para tornar o mapa de assentos facilmente representável em HTML. Eu adicionei referências onde você encontrará informações mais detalhadas sobre um determinado assunto.
O mapa de assentos
O diagrama de classes abaixo mostra as principais classes que formam o mapa de assentos. Um Venue
(local) por ter um ou mais SeatMap
(mapa de assentos). Um SeatMap
pode ter um ou mais relacionamentos com Image
(imagem), Label
(rótulo) e ou Seat
(assento). Os itens Seat
são objetos Plottable
que são plotados em um container usando os valores x (distância da esquerda) e y (distância do topo) usando o posicionamento absoluto do CSS.
Para fazer o SeatMap
ser facilmente representado em HTML, foi criada uma extensão Twig que provê funções que recebem um objeto Plottable
e o incorpora no tema com um bloco Twig. Extensões Twig ajudam a manter código frequentemente usado dentro de uma classe reutilizável:
Nós definimos as funções e uma tag Twig na class SeatMapExtension
. Nossas funções Twig são mapeadas para blocos Twig disponíveis em um arquivo de template base:
Como o template base exibe apenas a representação HTML para o mapa de assentos – isto é, sem exibir as cores para os diferentes estados possíveis de um assento – nós adicionamos uma tag Twig que torna possível adicionar arquivos de template Twig que podem sobrescrever os blocos padrões do template base. Dessa forma, quando nós precisamos exibir um SeatMap
para um Event
no bundle EventBundle
, sobrescrevemos os blocos Twig padrões que estão definidos no arquivo de template base no bundle VenueBundle com blocos que estão preparados para os possíveis estados de um assento:
Isso soa familiar para você? O componente Form
usa a mesma aproximação para customizar os widgets de formulário com a tag form_theme.
Como plus, fica muito fácil criar um teste de unidade do código HTML gerado pela extensão Twig usando XPath:
No final, a extensão Twig foi definida como um serviço no DIC:
Exibindo o mapa de assentos com o estado dos assentos
E onde está o dado do estado do assento? Como já foi dito, existem quatro estados possíveis para um assento: disponível, reservado, vendido e indisponível. O diagrama de classes mais completo mostra que o estado do assento está espalhado através das diferentes classes:
Clique para ver uma versão maior
Quando um Event
está sendo publicado, ele usa os dados da relação SeatMap
da classe Venue
para popular o catálogo da loja com Tickets
(ingressos) para si mesmo. Cada Ticket
tem uma relação com um Seat
e é através dessa relação que é possível recuperar o estado para um Seat
na representação HTML de um SeatMap
:
- Disponível: um
Seat
está disponível quando o seuTicket
está em umOrder
(pedido) cancelado (OrderItem -> Ticket -> Seat) ou, na ausência de qualquer outro estado, quando oTicket
está publicado; - Reservado: um
Seat
está reservado quando o seuTicket
está em umCart
(carrinho) (CartItem -> Ticket -> Seat) ou em umOrder
aberto (OrderItem -> Ticket -> Seat); - Vendido: um
Seat
está vendido quando o seuTicket
está em um Order fechado (OrderItem -> Ticket -> Seat); - Indisponível: um
Seat
está indisponível quando o seuTicket
não está publicado.
E como nós recuperamos esses dados? Nós temos uma classe EventManager
que adiciona um nível de abstração entre a aplicação e o Entity Repository do Doctrine ORM (ao qual foi baseada nas classes manager do FOSUserBundle). EventManager
tem um método que carrega uma instância Event
e que consulta o banco de dados com um SELECT SQL com join para as tabelas das entidades Cart
, Order
e Ticket
através da relação de Ticket
com CartItem
e OrderItem
.
Então EventManager
criar uma instância de EventView
que simplesmente carrega os estados das instâncias de Ticket
e cria um array associativo com os identificados de Seat
como chave e o estado relacionado de Ticket
(e a origem do valor) como valor:
Como cada código de estado para cada parte do modelo do domínio tem significados diferentes, nós criamos classes simples que se parecem com Enums Java. Para cada uma dessas classes, existe uma classe par StatusConverter
que sabe como converter o código de estado numérico para uma string que é exibida no código HTML de Seat ao qual muda a cor do mesmo dependendo do estado:
Agora que nós temos os estados para os Seat’s individuais, basta exibí-los. No template Twig de um Event
, nós configuramos um arquivo de template extra que sobrescreve alguns dos blocos do arquivo de template base de SeatMap
. Os blocos definidos nesse arquivo de template usam diretamente a instância de EventView
para acessar o estado de um assento individual.
Finalmente, o estado de um Seat
é usado para exibir o link que adiciona o Ticket
desejado no Cart
(se disponível):
<?php
// IngressoPratico/VenueBundle/Twig/SeatMapExtension.php
namespace IngressoPratico\VenueBundle\Twig;
use IngressoPratico\VenueBundle\Entity\SeatMap;
use IngressoPratico\VenueBundle\Entity\Plottable;
class SeatMapExtension extends \Twig_Extension
{
/**
* @var array
*/
protected $templates;
/**
* @var Twig_Environment
*/
protected $environment;
/**
* @var array
*/
protected $seatTypeCss = array();
/**
* Constructor.
*
* @param array|string $templates The Twig templates to use
*/
public function __construct($templates)
{
if (is_string($templates)) {
$templates = array($templates);
}
$this->templates = $templates;
}
/**
* @{inheritDoc}
*/
public function initRuntime(\Twig_Environment $environment)
{
$this->environment = $environment;
}
/**
* @{inheritDoc}
*/
public function getFunctions()
{
return array(
'seatmap_render' => new \Twig_Function_Method($this, 'renderSeatMap', array('is_safe' => array('html'))),
'seatmap_render_item' => new \Twig_Function_Method($this, 'renderSeatMapItem', array('is_safe' => array('html'))),
);
}
/**
* @{inheritDoc}
*/
public function getName()
{
return 'seatmap';
}
/**
* Returns the token parser instance to add to the existing list.
*
* @return array An array of Twig_TokenParser instances
*/
public function getTokenParsers()
{
return array(
// {% seatmap_theme "SomeBundle::widgets.twig" %}
new SeatMapThemeTokenParser(),
);
}
/**
* Add a template to look for seat map's blocks.
*
* @param string $template
*/
public function addTemplate($template)
{
$this->templates[] = $template;
}
/**
* Renders the seat map HTML representation. In your Twig template:
*
* {{ seatmap_render(seatmap) }}
* {{ seatmap_render(seatmap, { 'image_dir': 'path/to/images' }) }}
*
* @param SeatMap $seatMap
* @param array $variables
* @return string|null
*/
public function renderSeatMap(SeatMap $seatMap, array $variables = array())
{
return $this->render('seatmap', array(
'items' => $seatMap->getItems(),
'variables' => $variables,
));
}
/**
* Renders a plottable item HTML representation. In your Twig template:
*
* {{ seatmap_render_item(item) }}
* {{ seatmap_render_item(item, { 'image_dir': 'path/to/images' }) }}
*
* @param Plottable $item
* @param array $variables
* @return string|null
*/
public function renderSeatMapItem(Plottable $item, array $variables = array())
{
$variables = array_merge(
array('item' => $item, 'image_dir' => null),
$variables
);
return $this->render($item->getTypeName(), $variables);
}
/**
* Renders a template block, passing the variables to the template.
*
* @param string $section
* @param array $variables
* @return string|null
*/
private function render($section, array $variables)
{
if ($section != 'seatmap') {
$section = 'seatmap_'.$section;
}
$templates = $this->getTemplates();
if (isset($templates[$section])) {
return $templates[$section]->renderBlock($section, $variables);
}
return null;
}
/**
* Returns the availables blocks in the chosen template. You can override the default seatmap blocks using the
* seatmap_theme tag in your Twig template:
*
* {% seatmap_theme _self 'SomeBundle::seatmap-blocks.html.twig' %}
*
* @return array
*/
private function getTemplates()
{
$templates = array();
foreach ($this->templates as &$template)
{
// Load template if not already loaded
if (!$template instanceof \Twig_Template) {
$template = $this->environment->loadTemplate($template);
}
$blocks = array();
foreach ($template->getBlockNames() as $name) {
$blocks[$name] = $template;
}
$templates = array_replace($templates, $blocks);
}
return $templates;
}
}
// IngressoPratico/VenueBundle/Twig/SeatMapThemeNode.php
namespace IngressoPratico\VenueBundle\Twig;
class SeatMapThemeNode extends \Twig_Node
{
public function __construct(\Twig_NodeInterface $resources, $lineno, $tag = null)
{
parent::__construct(array('resources' => $resources), array(), $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param \Twig_Compiler $compiler A Twig_Compiler instance
*/
public function compile(\Twig_Compiler $compiler)
{
$compiler->addDebugInfo($this);
foreach ($this->getNode('resources') as $resource) {
$compiler
->write('echo $this->env->getExtension(\'seatmap\')->addTemplate(')
->subcompile($resource)
->raw(');')
;
}
}
}
// IngressoPratico/VenueBundle/Twig/SeatMapThemeTokenParser.php
namespace IngressoPratico\VenueBundle\Twig;
class SeatMapThemeTokenParser extends \Twig_TokenParser
{
/**
* Parses a token and returns a node.
*
* @param \Twig_Token $token A Twig_Token instance
* @return \Twig_NodeInterface A Twig_NodeInterface instance
*/
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$resources = array();
do {
$resources[] = $this->parser->getExpressionParser()->parseExpression();
} while (!$stream->test(\Twig_Token::BLOCK_END_TYPE));
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
return new SeatMapThemeNode(new \Twig_Node($resources), $lineno, $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'seatmap_theme';
}
}