Realizaremos un proyecto en Symfony 3.4 utilizando sus fuertes herramientas, tales como generarar código de entidades por medio de doctrine, validaciones, mapping de relaciones entre otros. Primero hablemos del proyecto:

Proyecto

Este proyecto está basado en una prueba técnica laboral que me postulé la semana pasada. La prueba tiene varios aspectos interesantes que ponen en clara evidencia el uso de los frameworks.

Dado el diagrama de clases representado en la Figura 1:

Figura 1. Diagrama de clases del proyecto

Realizar una aplicación web que cubra con los siguientes requisitos:

  1. Implementar el diagrama de clase.
  2. Sea capaz de crear Áreas de trabajo (WorkSpace), las cuales pueden contener figuras geométricas dadas en el diagrama.
  3. Las áreas de trabajo tienen un límite definido de figuras, llegado a ese límite no se puede agregar más figuras.
  4. Se necesita el área total de las figuras que contiene el área de trabajo.
  5. El apotema solo se calcula con los Hexágonos que contiene el área de trabajo.
  6. Se puede cambiar una figura geométrica por otra igual o diferente a ella.
  7. Se pueda calcular el área por tipo de figura.
  8. Cada área de trabajo se debe almacenar con un ID único.

Antes de empezar quería aclarar que intenté realizarla con la versión de Symfony 4.0 pero debido a que las funciones de generar get y set de las entidades y la función doctrine:generate:crud fueron removidos hizo muy difícil realizar esta aprueba a tiempo. No sé porque habrán quitado esas funciones tan útiles, tenía entendido que la función principal de los frameworks es reducir el tiempo de desarrollo. La versión aún no está declarada estable, así que ya veremos.

El código final puede encontrarse en mi Github personal https://github.com/arturoverbel/figuras_symfony

Instalación

Usaremos la guía de la página oficial de Symfony https://symfony.com/doc/3.4/setup.html para instalar la versión 3.4 en nuestro linux. Las líneas de comando:

sudo mkdir -p /usr/local/bin
sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
sudo chmod a+x /usr/local/bin/symfony

Con esto podemos crear entonces nuestro proyecto. Corremos el comando:

symfony new Fig

Para crear un proyecto Symfony con el nombre Fig. Nos pasamos a la carpeta del proyecto /Fig y vemos las carpetas de nuestro nuevo proyecto. Si nosotros pasamos el comando:

php bin/console

Podemos ver nuestra versión de Symfony junto con un listado de los comandos disponibles. En mi caso me aparece Symfony 3.4.3, que la carpeta del kernel se encuentra en /app, el ambiente es dev y el debug se encuentra activado.

Configuración

La configuración de la base de datos se realiza en /app/config/parameters.yml y su estructura es:

parameters:
  database_host: 127.0.0.1
  database_port: null
  database_name: fig
  database_user: root
  database_password: root
  mailer_transport: smtp
  mailer_host: 127.0.0.1
  mailer_user: null
  mailer_password: null
  secret: 6547a27126dfe22f73f2988c3e5fd9d0c0ed7abe

Aquí introducimos los datos de nuestra base de datos y luego pasamos elc omando:

php bin/console doctrine:database:create

Para crear la base de datos.

Esquema de Entidades

Utilizaremos el comando:

php bin/console doctrine:generate:entity

Para que Doctrine nos ayude a generar una nueva entidad. Les aparecerá algo como “Welcome to the Doctrine2 entity generator“. Les pedirá un nombre para la entida colocando primero el nombre del bundle (por defecto AppBundle) seguido de dos puntos y el nombre de la entidad. En nuestro caso la entidad Fig.

The Entity shortcut name: AppBundle:Fig

El formato que usaremos para mapiar la información será [annotation]. Luego nos pedirá introducir los campos con su tipo, colocamos los siguientes:

New field name (press <return> to stop adding fields): numLados
Field type [string]: integer
Is nullable [false]: 
Unique [false]:

Terminamos y nos una entidad ubicada en /src/AppBundle/Entity/Fig.php:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Figura
 *
 * @ORM\Table(name="fig")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\FigRepository")
 */
class Fig
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var int
     *
     * @ORM\Column(name="numLados", type="integer")
     */
    private $numLados;


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set numLados
     *
     * @param integer $numLados
     *
     * @return Figura
     */
    public function setNumLados($numLados)
    {
        $this->numLados = $numLados;

        return $this;
    }

    /**
     * Get numLados
     *
     * @return int
     */
    public function getNumLados()
    {
        return $this->numLados;
    }
}

Como podemos ver en el archivo Fig.php, se genera automáticamente el atributo ID con la opción que autogeneré incrementalmente. El atributo numLados con sus get y set. Nosotros ahora le añadiremos en la línea 17:

abstract class Fig

Para identificarlo como una clase abstracta. También le agregaremos las siguientes funciones:

abstract protected function getArea();
abstract protected function getPerimetro();
abstract protected function printr();

Para que las clases que sean heredades de Fig() contengan las mismas funciones. El siguiente paso es generar entonces el Triangulo, el Hexagono y el Cuadrado como lo ordena la estructura de clases. La clase cuadrado por ejemplo quedará así:

Cuadrado.php

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Cuadrado
 *
 * @ORM\Entity(repositoryClass="AppBundle\Repository\CuadradoRepository")
 */
class Cuadrado extends Fig
{
    /**
     * @var int
     *
     * @ORM\Column(name="lado", type="integer")
     */
    private $lado;

    public function __construct() {
        $this->numLados = 4;
    }
    
    public function getArea() {
        return pow($this->lado, 2);
    }
    
    public function getPerimetro(){
        return $this->lado * 4;
    }

    /**
     * Set lado
     *
     * @param integer $lado
     *
     * @return Cuadrado
     */
    public function setLado($lado)
    {
        $this->lado = $lado;

        return $this;
    }

    /**
     * Get lado
     *
     * @return int
     */
    public function getLado()
    {
        return $this->lado;
    }

    public function printr(){
        return '(Lado) : (' . $this->lado . ')';
    }   
}

Tenemos entonces que crear manualmente las funciones printr( ), getPerimetro( ) y getArea( ). También agregamos el constructor para añadir el número de lado por defecto en un cuadrado. Lo más importante que podemos agregar es en la línea 12 que herede la clase Fig( ).

Herencia

Luego de tener  las demás clases. QUe pueden ser consultadas aquí: https://github.com/arturoverbel/figuras_symfony/tree/master/src/AppBundle/Entity Necesitamos decirle a Doctrine qué tipo de herencia es. Para eso nos dirigimos a Fig.php y añadimos en las anotaciones de la clase (Justo antes del nombre de la clase) lo siguiente:

/**
 * @ORM\Entity
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="discr", type="string")
 * @ORM\DiscriminatorMap({ "figura" = "Fig",
 *                         "cuadrado" = "Cuadrado",
 *                         "triangulo" = "Triangulo",
 *                         "hexagono" = "Hexagono" })
 */
abstract class Fig

Estas anotaciones al ORM nos dice que usaremos una sola tabla para todas las subclase de esta súper clase. Que usaremos una columna llamada discr, de tipo string, para diferenciar una subclase de otra subclase. Que los valores para discriminar son el listado que allí aparecen. Ojo siempre también incluir la misma súper clase así sea que sea abstracta.

Hasta aquí podemos pasar lo que hemos realizado a la base de datos. Utilizaremos el comando:

php bin/console doctrine:schema:update --force

Para migrar nuestro esquema a la base de datos. El comando nos indicará los cambios que se realizaron, si existe un error de mapeo en nuestro esquema o si simplemente no se realizaron cambios.

Debería generarles solo una tabla con la siguiente estructura:

Figura 2. Estructura de la tabla Fig. Relacionada con la entidad Fig.php

Para explicarlo mejor veamos unos datos de esta tabla:

Figura 3. Datos de la tabla Fig. Relacionada con la entidad Fig.php

Observemos que el ID 2 es un cuadrado por la columna discr, como solo posee el atributo lado entonces su columna puede tener valores, pero los atributos de base, alturahipotenusa corresponden a triángulo es por eso que el ID 4, 5 y 9 posee estas columnas con valores. La tabla de workspace la crearemos acontinuación:

Agregación

Creamos entonces de la misma manera a Workspace.php pero esta vez solo vamos a introducir el atributo nombre tipo string con una longitud de 150 y el atributo limiteFiguras de tipo entero. Al generarlas quedaría algo como la siguiente clase por defecto:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Workspace
 *
 * @ORM\Table(name="workspace")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\WorkspaceRepository")
 */
class Workspace
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="nombre", type="string", length=150)
     */
    private $nombre;

    /**
     * @var int
     *
     * @ORM\Column(name="limiteFiguras", type="integer")
     */
    private $limiteFiguras;
    
    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set nombre
     *
     * @param string $nombre
     *
     * @return Workspace
     */
    public function setNombre($nombre)
    {
        $this->nombre = $nombre;

        return $this;
    }

    /**
     * Get nombre
     *
     * @return string
     */
    public function getNombre()
    {
        return $this->nombre;
    }

    /**
     * Set limiteFiguras
     *
     * @param integer $limiteFiguras
     *
     * @return Workspace
     */
    public function setLimiteFiguras($limiteFiguras)
    {
        $this->limiteFiguras = $limiteFiguras;

        return $this;
    }

    /**
     * Get limiteFiguras
     *
     * @return int
     */
    public function getLimiteFiguras()
    {
        return $this->limiteFiguras;
    }    
}

Listo, ahora agreguemos el atriburo de figuras la cual tendrá una relación One-To-Many (Uno a muchas), esto queire decir que un objeto de la instancia Workspace.php tendrá varias figuras asociadas. Y viceversa las instancias de Fig.php tendrá solo un workspace asociado. Para eso realizamos la siguiente anotación en Workspace.php:

/**
 * One Product has Many Features.
 * @ORM\OneToMany(targetEntity="Fig", mappedBy="workspace")
 */
private $figuras;
public function __construct() {
    $this->figuras = new ArrayCollection();
}
use Doctrine\Common\Collections\ArrayCollection;

Para la clase de Fig.php agregar:

/**
 * Many Features have One Product.
 * @ORM\ManyToOne(targetEntity="Workspace", inversedBy="figuras")
 * @ORM\JoinColumn(name="workspace", referencedColumnName="id")
 */
private $workspace;

Para luego generar los set y get con el comando:

php bin/console doctrine:generate:entities AppBundle

Este comando buscar errores en el mapeo de nuestras entidades del bundle especificado (en nuestro caso AppBundle) y agrega los set y get correspondiente. En nuestro caso para Fig.php generó las siguientes funciones:

/**
 * Set workspace
 *
 * @param \AppBundle\Entity\Workspace $workspace
 *
 * @return Fig
 */
public function setWorkspace(\AppBundle\Entity\Workspace $workspace = null)
{
    $this->workspace = $workspace;

    return $this;
}

/**
 * Get workspace
 *
 * @return \AppBundle\Entity\Workspace
 */
public function getWorkspace()
{
    return $this->workspace;
}

El cual podemos establecer un workspace a una entidad y también podemos recuperarlo. Para Workspace.php tenemos las siguientes:

/**
 * Get limiteFiguras
 *
 * @return int
 */
public function getLimiteFiguras()
{
    return $this->limiteFiguras;
}

/**
 * Add figura
 *
 * @param \AppBundle\Entity\Fig $figura
 *
 * @return Workspace
 */
public function agregarFigura(\AppBundle\Entity\Fig $figura)
{
    $this->figuras[] = $figura;

    return $this;
}

/**
 * Remove figura
 *
 * @param \AppBundle\Entity\Fig $figura
 */
public function eliminarFigura(\AppBundle\Entity\Fig $figura)
{
    $this->figuras->removeElement($figura);
}

Se modificaron los nombres por defecto para que cumpla con los requisitos del diagrama de clases. Podemos obsercar que el get devuelve un ArrayCollection de Figuras. Que agrega y elimina de la lista que posee. Este comportamiento es porpio de una relación One-To-Many.

CRUD

Podemos decir que ya poseemos toda la arquitectura armada, ahora solo nos falta las vistas para crear, leer, actualizar y eliminar los registros de Figuras y Workspaces. Para eso pasemos los dos siguientes comandos:

php bin/console doctrine:generate:crud AppBundle:Workspace
php bin/console doctrine:generate:crud AppBundle:Cuadrado
php bin/console doctrine:generate:crud AppBundle:Triangulo
php bin/console doctrine:generate:crud AppBundle:Hexagono

Estos comandos no generará una lista de controladores y vistas por cada entidad. Por ejemplo podemos ver en la carpeta de controladores todos los contrladores generados:

Figura 4. Lista de controladores del proyecto

En la carpeta de Views observamos carpeta para cada entidad. En una de ellas, por ejemplo Cuadrado observamos un archivo .html.twig por cada acción de la entidad (edit, index, new y show):

Figura 5. Lista de vistas por entidad.

En el controlador de WorkspaceController.php tenemos todas funciones de cada acción de la entidad. La primera que vemos es el index:

<?php

namespace AppBundle\Controller;

use AppBundle\Entity\Fig;
use AppBundle\Entity\Workspace;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;

/**
 * Workspace controller.
 *
 * @Route("workspace")
 */
class WorkspaceController extends Controller
{
    /**
     * Lists all workspace entities.
     *
     * @Route("/", name="workspace_index")
     * @Method("GET")
     */
    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $workspaces = $em->getRepository('AppBundle:Workspace')->findAll();

        return $this->render('workspace/index.html.twig', array(
            'workspaces' => $workspaces,
        ));
    }

Primero, en el comentario @Route(“workspace”) nos dice  que todas las rutas de las siguientes funciones comienzan por /workspace”. Como el index indica “/” entonces la ruta princila para Workspace sería: “http://127.0.0.1/Fig/web/app_dev.php/workspace/” para nuestra ruta de prueba. La función index instancia de la clase Doctrine, luego realiza una búsqueda para recojer todos los workspace y los renderiza en el index ubicado en /app/Resources/views/workspace/index.html.twig.

Al cargar la ruta tenemos:

Figura 6. Página index por defecto de Workspace.

Corregir problema de insertar en One-To-Many

Gracias a Doctrine podemos tener buenas funciones para trabajar con el mapeo de las entidades. Pero el CRUD de Symplifica aún no está del todo ligado con estas funciones. Podemos darnos cuenta al crear un nuevo workspace en la dirección “http://127.0.0.1/Fig/web/app_dev.php/workspace/new“:

Figura 7. Página de crear workspace por defecto.

Al crear un Workspace podemos ingresar el “Nombre” y el “Límite de Figuras” pero no podemos agregar las Figuras. Para que salgan un listado de las figuras primero necesitamos modificar el archivo que organiza todos los formularios. Tanto en index, edit, create y show. El archivo se encuentra en “src/AppBundle/Form/WorkspaceType.php” y le agregamos al constructor las figuras relacionada con la entidad:

/**
 * {@inheritdoc}
 */
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('nombre')
            ->add('limiteFiguras')
            ->add('figuras', EntityType::class, array(
                'class' => Fig::class,
                'required' => false,
                'choice_label' => 'whereis',
                'multiple' => true
            ))
    ;
}

Vemos que ya se encuentra nombre y figura por defecto (el mismo nombre que s enecuentra en la entidad), ahora agregamos el atributo de figuras. Le establecemos que es una clase Entidad y marcamos sus opciones. En la opción de choice_label podemos agregar un atributo o un método de la clase. En este caso creamos un método nuevo en Fig.php para identificarlo mejor en la lista. La función:

public function whereis(){
    $hasWorkspace = ( empty($this->getWorkspace()) ) ? '' : $this->getWorkspace()->getId() . '*';
    return $hasWorkspace . "  |  " . $this->printr();
}

Aquí lo que hacemos es identificar si tiene o no tiene workspace y imprime printr que es una función que se herada en las subclases y son diferentes para cada una.

Con esto nos debería aparecer la lista múltiple. Pero eso no es todo, debemos modificar las funciónes que editan y guardan así:

/**
 * Creates a new workspace entity.
 *
 * @Route("/new", name="workspace_new")
 * @Method({"GET", "POST"})
 */
public function newAction(Request $request)
{
    $workspace = new Workspace();
    $form = $this->createForm('AppBundle\Form\WorkspaceType', $workspace);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();

        /** @var Fig $figura */
        foreach ($workspace->getFiguras() as $figura) {
            $figura->setWorkspace($workspace);
            $em->persist($figura);
        }

        $em->persist($workspace);
        $em->flush();

        return $this->redirectToRoute('workspace_show', array('id' => $workspace->getId()));
    }

    return $this->render('workspace/new.html.twig', array(
        'workspace' => $workspace,
        'form' => $form->createView(),
    ));
}

Además de modificar la instancia de $workspace, también debemos modificar las $figuras que corraspondan a este $workspace y  persistirlas. Lo mismo hacemos en edit.php:

/**
 * Displays a form to edit an existing workspace entity.
 *
 * @Route("/{id}/edit", name="workspace_edit")
 * @Method({"GET", "POST"})
 */
public function editAction(Request $request, Workspace $workspace)
{
    $deleteForm = $this->createDeleteForm($workspace);
    $editForm = $this->createForm('AppBundle\Form\WorkspaceType', $workspace);
    $editForm->handleRequest($request);

    if ($editForm->isSubmitted() && $editForm->isValid()) {

        $em = $this->getDoctrine()->getManager();

        $oldfigs=$em->getRepository('AppBundle:Fig')->findby(array(
            "workspace"=>$workspace
        ));

        /** @var Fig $oldfig */
        foreach($oldfigs as $oldfig){
            $oldfig->setWorkspace(null);
            $em->persist($oldfig);
        }

        /** @var Fig $figura */
        foreach ($workspace->getFiguras() as $figura) {
            $figura->setWorkspace($workspace);
            $em->persist($figura);
        }

        $em->persist($workspace);
        $em->flush();

        return $this->redirectToRoute('workspace_edit', array('id' => $workspace->getId()));
    }

    return $this->render('workspace/edit.html.twig', array(
        'workspace' => $workspace,
        'edit_form' => $editForm->createView(),
        'delete_form' => $deleteForm->createView(),
    ));
}

Mejora página index

Mejoremos un poco esta página. Primeo modifiquemos la base de estas plantillas en “/app/Resources/views/base.html.twig”, vamos agregarle bootstrap a nuestra plantilla:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
        <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    </head>
    <body>
        <div class="container">
            {% block body %}{% endblock %}
        </div>
        {% block javascripts %}{% endblock %}
    </body>
</html>

Y ahora modifiquemos el index.html.twig para mostrar todas las figuras que hacen parte de cada workspace. El código sería:

{% extends 'base.html.twig' %}

{% block body %}
    <h1>Workspaces list</h1>

    <table class="table">
        <thead>
            <tr>
                <th>Id</th>
                <th>Nombre</th>
                <th>Límite</th>
                <th>Figura</th>
                <th>Area Total</th>
                <th>Apotema total</th>
                <th>#</th>
            </tr>
        </thead>
        <tbody>
        {% for workspace in workspaces %}
            <tr>
                <td>{{ workspace.id }}</td>
                <td><a href="{{ path('workspace_show', { 'id': workspace.id }) }}">{{ workspace.nombre }}</a></td>
                <td>{{ workspace.limiteFiguras }}</td>
                <td>
                    <ul>
                        {% for figura in workspace.figuras %}
                        <li>
                            <a href="{{ path('fig_show', { 'id': figura.id }) }}">
                                Lados: {{ figura.numLados }}, {{  figura.printr }}
                            </a>
                        </li>
                        {%  endfor %}
                    </ul>
                </td>
                <td>
                    {{ workspace.getAreaTotal }}
                </td>
                <td>
                    {{ workspace.apotemaTotal }}
                </td>
                <td>
                    <a href="{{ path('workspace_edit', { 'id': workspace.id }) }}">edit</a>
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    <ul>
        <hr>
        <li>
            <a href="{{ path('workspace_new') }}">Crear un Workspace</a>
        </li>
        <li>
            <a href="{{ path('fig_index') }}">Lista de figuras</a>
        </li>
    </ul>
{% endblock %}

Tenemos entonces una vista como:

Figura 8. Página index mejorado de Workspace

Podemos hacer así con las demás vistas. Podemos seguir agregando más de estos.

Conclusión

Con Symfony 3.4 podemos crear un fuerte proyecto backend, en menos de medio día. Lástima que muchas funciones aún necesiten ser editadas como el CRUD de mapeo de entidades que pueden ser un dolor de cabeza para muchos programadores.

A mi parecer el hechode no tener que actualizar los módulos después de cargarlos con los comandos CRUD es un error muy grave, teniendo en cuenta que Symfony posee, más que una arquitectura MVC, una arquitectura FCE (Front-Control-Entity) donde las vistas están ligadas con las entidades. Si se modifican atributos, o se agregan en las entitades deberían actualizarse en las vista y correspondiente sin ningún cambio de código. Espero que estas mejoras se vean mejor en Symfony 4.0 en su versión estable.

El código se encuentra en mi Github como:

https://github.com/arturoverbel/figuras_symfony

El proyecto se encuentra “despeblado” en mi servidor de prueba:

http://35.168.88.201/figuras/web/app.php/workspace/

Fuentes