Blog

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:

Seat Map

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.

venue class diagram

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:

class diagram pequeno

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 seu Ticket está em um Order (pedido) cancelado (OrderItem -> Ticket -> Seat) ou, na ausência de qualquer outro estado, quando o Ticket está publicado;
  • Reservado: um Seat está reservado quando o seu Ticket está em um Cart (carrinho) (CartItem -> Ticket -> Seat) ou em um Order aberto (OrderItem -> Ticket -> Seat);
  • Vendido: um Seat está vendido quando o seu Ticket está em um Order fechado (OrderItem -> Ticket -> Seat);
  • Indisponível: um Seat está indisponível quando o seu Ticket 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 CartOrder 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';
        }
    }