Intro

Realizaremos un proyecto backend por medio del Framework Yii 2, al final de esta entrada hay unas conclusiones personales acerca de este framework. El resultado de este proyecto se encuentra en los siguientes enlaces:

Proyecto en servidor: http://35.168.88.201/figuras_yii/web/index.php

El código de Github: https://github.com/arturoverbel/figuras_yii

Proyecto Backen en Symfony 3.4: Symfony 3.4 – Proyecto Backend

¿Qué es?

No hay mejor forma de aprender un framework que realizar el esqueleto de un proyecto. En esta entrada voy a hablarles de un frameork que lleva 10 años en la industría. Yii es un framework  orientado a objetos, software libre y de alto rendimiento. Fácil de usar y eficiente. Es el acrónimo de “Yes It Is!”, (¡Sí lo es!).

Gracias a su arquitectura basada en componentes y su sofisticado soporte de caché, es espcialmente adecuado para desarrollar aplicaciones a gran escala como CMS, e-commerce, servicios RESTful, foros, entre otros.


Yii 2.0 requiere PHP 5.4.0 o superior y funciona mejor con la última versión de PHP 7. El uso de Yii requiere conocimientos básicos de POO. Yii 2.0 también hace uso de las últimas características de PHP, como espacios de nombres y características. Comprender estos conceptos te ayudará a elegir más fácilmente Yii 2.0.

Realizaremos el ejercicio conla misma aplicación que usamos en nuestra entrada anterior de Symfony 3.4. El proyecto Figuras

Proyecto

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.

Instalación

La siguiente instalación se realizará para Windows 10.

Descargar y correr Composer-Setup.exe.

Figura 2. Instalador de Composer en Windows

Hasta aquí pueden escribir composer en el comando del sistema de Windows para que les aparezca el help de Composer.

Generemos la plantilla de nuestro proyecto Figuras pasando el siguiente comando:

composer create-project --prefer-dist yiisoft/yii2-app-basic basic

Figura 3. Projecto creado en cmd

Haste este punto podemos ver la plantilla por defecto del proyecto como lo muestra la figura 4:

Figura 4. Index estándar de Yii

La aplicación básica viene instalada con cuatro páginas:

  • Index
  • “Acerca de”
  • “Contacto”, que muestra un formulario de contacto que permite a los usuarios finales ponerse en contacto por correo electrónico.
  • Página de “Inicio de sesión”, que muestra un formulario de inicio de sesión que se puede utilizar para autenticar a los usuarios finales. Intente iniciar sesión con “admin / admin”, y verá que el ítem del menú principal “Login” cambiará a “Logout”.

Configuración

Para configurar la base de datos, nos dirijimos al archivo /config/db.php y modificamos los parámetros:

<?php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=localhost;dbname=figuras_yii',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
];

Esquema de Módulos

Generemos entonces nuestros módulos de Figura:

Con una sola consulta SQL podemos generar la tabla y simular la herencia en la Base de Datos de nuestra arquitectura. La consulta:

CREATE TABLE `Figura` (
 `id` int(11) NOT NULL,
 `numLados` int(11) NOT NULL,
 `discr` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `lado` int(11) DEFAULT NULL,
 `base` int(11) DEFAULT NULL,
 `altura` int(11) DEFAULT NULL,
 `hipotenusa` int(11) DEFAULT NULL,
 `radio` double DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

ALTER TABLE `Figura`
 ADD PRIMARY KEY (`id`);

ALTER TABLE `Figura`
 MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;COMMIT;

 

Para realizar las entidades podemos hacerlo tanto por consola como por la web. En este ejemplo lo realizaremos por al web porque es más fácil y los programadores le gusta lo fácil. Para cualquiera de las maneras usaremos un módulo llamado Gii.

Gii

Es una herramienta que viene incluido en la instalación de Yii. Es un potente generador de código que nos guiará para generar los módulos, controladores y vistas de nuestro proyecto sin escribir ninguna línea de código.

Está configurado para que este solamente habilitado en modo de desarrollo. Las configuraciones de esta herramienta se pueden editar en /config/web.php.

Al entrar con este enlace http://localhost/figuras/web/index.php?r=gii podemos ver todos los juguetes de esta herramienta:

Figura 5. Index del entorno Gii

 

Seleccionamos Model Generator para generar el ActiveRecord de nuestra tabla

Figura 6. Generando modelo Figura

Al hacer clic en generar, nos saldrá la clase Figura en /models/Figura.php. Además de el archivo generado le añadimos tres funciones más al final del archivo que son getArea( ), getPerimetro( ) y printr( ) para ser usadas en sus clases hijas (aparecen resaltadas):

<?php

namespace app\models;

use Yii;
use app\models\Cuadrado;
use app\models\Triangulo;
use app\models\Hexagono;

/**
 * This is the model class for table "figura".
 *
 * @property int $id
 * @property int $numLados
 * @property string $discr
 * @property int $lado
 * @property int $base
 * @property int $altura
 * @property int $hipotenusa
 * @property double $radio
 * @property int $workspace
 */
class Figura extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName(){
        return 'figura';
    }

    /**
     * @inheritdoc
     */
    public function rules(){
        return [
            [['numLados', 'discr'], 'required'],
            [['numLados', 'lado'], 'integer'],
            [['radio'], 'number'],
            [['discr'], 'string', 'max' => 255],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'numLados' => 'Número de lados',
            'discr' => 'Discr',
            'lado' => 'Lado',
        ];
    }

    public static function instantiate($row)
    {
        switch ($row['discr']) {
            case Cuadrado::DISCR:
                return new Cuadrado();
            case Triangulo::DISCR:
                return new Triangulo();
            case Hexagono::DISCR:
                return new Hexagono();
            default:
                return new self;
        }

        return parent::instantiate($row);
    }

    public function getArea(){ return 0; }
    public function getPerimetro(){ return 0; }
    public function printr(){ return ''; }
}

La Herencia la trabajaremos manualmente. Utilizaremos el método de herencia de tabla única. En donde las subclase junto con la súper clase se guardan en una misma tabla de la base de datos.

La función instantiate( $row ) es una función heredada de ActiveRecord el cuál nos permite clasificar una instancia en una de las subclase de nuestra clase. En nuestro caso las clasificamos para Cuadrado, Triángulo y Hexágono

¿Qué es un Active Record?

Active Record proporciona una interfaz orientada a objetos para acceder y manipular datos almacenados en bases de datos. Una clase de registro activo está asociada a una tabla de base de datos, una instancia de registro activo corresponde a una fila de esa tabla, y un atributo de una instancia de registro activo representa el valor de una columna en particular en esa fila. En lugar de escribir sentencias de SQL sin procesar, accedería a los atributos de Active Record y llamaría a los métodos de Active Record para acceder y manipular los datos almacenados en las tablas de la base de datos.

Un objeto que envuelve una fila en una tabla o vista de base de datos, encapsula el acceso a la base de datos y agrega lógica de dominio en esa información.

Martin Fowler

CRUD

CRUD es una acrónimo de “Crear, Leer, Actualizar y Borrar” (en inglés Create, Read, Update & Delete) y son las acciones básicas que podemos hacer una entidad de nuestra persistencia de datos.

Ya tenemos nuestro modelo de Figura. Ahora vamos a generar un CRUD a partir del modelo. Accedemos al CRUD Generator de Gii y escribimos lo siguiente:

Figura 7. Generador de CRUD desde un modelo

Esta acción genera varios archivos. Los cuales son:

  • generated controllers\FiguraController.php
  • generated search\FiguraSearch.php
  • generated views\figuras\_form.phpge
  • nerated views\figuras\_search.phpg
  • enerated views\figuras\create.php
  • generated views\figuras\index.phpg
  • enerated views\figuras\update.ph
  • pgenerated views\figuras\view.php

Un controllador, para gestionar las rutas y las acciones del back de cada función del CRUD. La entidad FiguraSearh extiende de un ActiveRecord Figura. El cual nos ayudará a filtrar correctamente la lista de Figuras.

El view _form lo utilizaremos tanto en Crear y Actualizar. El view _searh lo utilizaremos en Index. Todas las acciones de CRUD tiene sus respectivas vistas, excepto Eliminar, porque solo nos bastará un mensaje de conformación para eliminar. Pero si tiene su acción en el Controlador.

Figura 8. Index de Figuras

Pero hasta aquí no podemos crear nuestras respectivas figuras. Para eso tenemos que crear un ActiveRecord para cada subclase de la figura y establecer que se comporte de cierta manera. Realizamos la plantilla de cada uno primero. Comenzando con Cuadrado. Lo primero es generarlo con Gii extendiendo de Figura:

Figura 9. Generar modelo de Cuadrado heredando de Figura

Tener en cuenta que la Base de la clase es Figura, así como el nombre de la tabla. El generador de código incluirá todas las columnas de la tabla Figura como atributos de la clase, pero nosotros solo nos interesa tres de ellos, así que los quitamos del archivo y codificamos las funciones de getArea( ), getPerimetro( ) y printr( ).

<?php

namespace app\models;

use Yii;

/**
 * This is the model class for table "figura".
 *
 * @property int $id
 * @property int $lado
 */
class Cuadrado extends \app\models\Figura
{
    const DISCR = 'cuadrado';

    public function init()
    {
        parent::init();
        $this->discr = self::DISCR;
        $this->numLados = 4;
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'figura';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['lado'], 'required'],
            [['lado'], 'integer'],
        ];
    }

    public function getArea() {
       return pow($this->lado, 2);
    }

    public function getPerimetro(){
       return $this->lado * 4;
    }

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

Quitamos todos los demás atributos del conjunto de reglas para los formularios. Establecemos el atributo discr como ‘cuadrado’ y el atributo numLados como cuatro para todas instancias de esta clase.

En Yii2 no se suele usar el constructor en las clases generadas o heredadas de las clases de Yii. Para eso utilizamos la función init( ) para inicializar todo lo que necesitemos.

Ahora, generamos el CRUD de cuadrado:

Figura 10. Formulario para generar el CRUD de la clase cuadrado

Create

Con esto tenemos todo lo que necesitamos para esta clase. Primero veamos el crear, ingresemos a http://127.0.0.1/figuras/web/index.php?r=cuadrado/create. Tenemos algo así:

Figura 11. Create de la clase cuadrado

View

En hacer clic en crear nos lleva a la vista de un cuadrado, nos sale todos los atributos de la tabla como nulo, así que editamos la vista en el archivo /views/cuadrado/view.php.

<?php

use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */
/* @var $model app\models\Cuadrado */

$this->title = $model->id;
$this->params['breadcrumbs'][] = ['label' => 'Figuras', 'url' => ['/figura/index']];
$this->params['breadcrumbs'][] = ['label' => 'Cuadrados', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="cuadrado-view">

    <h1><?= Html::encode($this->title) ?></h1>

    <p>
        <?= Html::a('Update', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?>
        <?= Html::a('Delete', ['delete', 'id' => $model->id], [
            'class' => 'btn btn-danger',
            'data' => [
                'confirm' => 'Are you sure you want to delete this item?',
                'method' => 'post',
            ],
        ]) ?>
    </p>

    <?= DetailView::widget([
        'model' => $model,
        'attributes' => [
            'id',
            'lado',
            [
                'attribute'=>'area',
                'label'=>'Area',
                'filter' => false,
                'value' => function ($model) {
                    return  $model->getArea();
                }
            ],
            [
                'attribute'=>'perimetro',
                'label'=>'Perímetro',
                'filter' => false,
                'value' => function ($model) {
                    return  $model->getPerimetro();
                }
            ],
        ],
    ]) ?>

</div>

Las líneas del 31 al 46 se agregaron para mostrar los datos de area y perímetro. Nótese que podemos ser bastantes específicos en los widgets de Yii2. Podemos especídifcar las clases del campo que contiene el valor, así como los tipos de inputs entre varias cosas más. Lo chevre de Yii2 es que si estos widgets no son lo suficiente específicos para tu proyecto, ¡Puedes crear los tuyos!

Se editaron dos cosas importantes. En la lista de breadcrumbs se agregó la lista de Figuras al lado de la lista de Cuadrado y se quitaron los demás atributos en el widget DetailView. Queda algo como:

Figura 12. Vista de un cuadrado.

En la lista y actualizar de Cuadrado también se editaron las columnas y se agregó el link de Figuras en el breadcrumbs. Así, tenemos todo lo que necesitamos de cuadrado.

Index

La página principal de cuadrado muestra la lista de todos los cuadrados que hemos creado. El código generado (con marcas de las que pusimos) es:

<?php

use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */
/* @var $searchModel app\search\CuadradoSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */

$this->params['breadcrumbs'][] = ['label' => 'Figuras', 'url' => ['/figura/index']];
$this->title = 'Cuadrados';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="cuadrado-index">

    <h1><?= Html::encode($this->title) ?></h1>
    <?php // echo $this->render('_search', ['model' => $searchModel]); ?>

    <p>
        <?= Html::a('Create Cuadrado', ['create'], ['class' => 'btn btn-success']) ?>
    </p>

    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'columns' => [
            ['class' => 'yii\grid\SerialColumn'],

            'id',
            'lado',
            [
                'attribute'=>'area',
                'label'=>'Area',
                'filter' => false,
                'value' => function ($model) {
                    return  $model->getArea();
                }
            ],
            [
                'attribute'=>'perimetro',
                'label'=>'Perímetro',
                'filter' => false,
                'value' => function ($model) {
                    return  $model->getPerimetro();
                }
            ],

            ['class' => 'yii\grid\ActionColumn'],
        ],
    ]); ?>
</div>

El widget GridView nos ayuda a pintar una tabla de información, esto se junta con un componente de búsqueda muy potente que se encuentra comentado por defecto. El resultado:

Figura 13. Página Index de cuadrado.

El GridView además en cada columna se puede filtrar haciendo la respectiva consulta en la base de datos y recargando la página otra vez. Viene con una columna de acción para Ver, Actualizar y Eliminar los registros. Esta tabla viene integrado con todo. ¡Gracias Yii2!

Index de Figuras

Para que el index de Figuras solo nos muestre lo relacionado con cada clase editaremos los enlaces de crear y editar figuras.

/**
 * Displays a single Figura model.
 * @param integer $id
 * @return mixed
 * @throws NotFoundHttpException if the model cannot be found
 */
public function actionView($id)
{
    return $this->redirect(['/'.$this->findModel($id)->discr.'/view', 'id' => $id]);

    /*
    return $this->render('view', [
        'model' => $this->findModel($id),
    ]);
    */
}

Para la función de View Figura la editamos para que reconozca el valor DISCR del modelo y redireccione al View correspondiente. Si DISCR es cuadrado entonces la url que forma sería ‘/cuadrado/view/3‘.

Lo mismo hacemos para actualizar.

En la vista de index de Figura mostraremos más especificaciones de cada figura:

<?php

use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */
/* @var $searchModel app\search\FiguraSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Figuras';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="figura-index">

    <h1><?= Html::encode($this->title) ?></h1>
    <?php // echo $this->render('_search', ['model' => $searchModel]); ?>

    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'columns' => [
            ['class' => 'yii\grid\SerialColumn'],

            'id',
            [
                'attribute'=>'printr',
                'label'=>'Resumen',
                'value' => function ($model) {
                    return  $model->printr();
                }
            ],
            [
                'attribute'=>'area',
                'label'=>'Area',
                'value' => function ($model) {
                    return  $model->getArea();
                }
            ],
            [
                'attribute'=>'perimetro',
                'label'=>'Perímetro',
                'value' => function ($model) {
                    return  $model->getPerimetro();
                }
            ],

            ['class' => 'yii\grid\ActionColumn'],
        ],
    ]); ?>

    <p>
        <h2>Crear Figura</h2>
        <?= Html::a('Create Cuadrado', ['/cuadrado/create'], ['class' => 'btn btn-success']) ?>
        <?= Html::a('Create Triángulo', ['/triangulo/create'], ['class' => 'btn btn-success']) ?>
        <?= Html::a('Create Hexágono', ['/hexagono/create'], ['class' => 'btn btn-success']) ?>
    </p>

</div>

Nuestro index de Figura entonces queda:

Figura 14. Página principal de Figuras

Ahora cuando hacemos click en la vista o actuailzación de alguna figura, irá al módulo que corresponde.

Ahora hacemos lo mismo para Triángulo y Hexágono. Para ver como van quedando su proyecto, pueden ir comparándolas con mi versión de GIT https://github.com/arturoverbel/figuras_yii, hasta aquí ya deberiamos tener cuatro controladores generados, así como sus archivos para buscar (FiguraSearch.php) y varios archivos en la carpeta de views.

Creación de Workspace

Primero es crear la tabla de Workspace con SQL y agregar la llave foránea a la tabla Figura. El script de SQL es:

CREATE TABLE `workspace` (
  `id` int(11) NOT NULL,
  `nombre` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
  `limiteFiguras` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

ALTER TABLE `figura` ADD `workspace` INT NULL AFTER `radio`;

ALTER TABLE `fig`
  ADD CONSTRAINT `FK_D4F24A958D940019` FOREIGN KEY (`workspace`) REFERENCES `workspace` (`id`);

De esta manera agregamos la tabla Workspace a la base de datos y alteramos la tabla Figura para tener la columna de Workspace con su respectiva llave foránea.

Creamos el modelo con CRUD. Importante cambiar la Clase base porque por defecto se mantiene con el último que crearon. Cambiarla a su defecto \yii\db\ActiveRecord.

Figura 15. Generar modelo de Workspace

A su vez editar la clase Figura con las siguientes lineas para trabajar con el workspace:

<?php

namespace app\models;

use Yii;
use app\models\Cuadrado;
use app\models\Triangulo;
use app\models\Hexagono;

/**
 * This is the model class for table "figura".
 *
 * @property int $id
 * @property int $numLados
 * @property string $discr
 * @property int $lado
 * @property int $base
 * @property int $altura
 * @property int $hipotenusa
 * @property double $radio
 * @property int $workspace
 */
class Figura extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName(){
        return 'figura';
    }

    /**
     * @inheritdoc
     */
    public function rules(){
        return [
            [['numLados', 'discr'], 'required'],
            [['numLados', 'lado'], 'integer'],
            [['radio'], 'number'],
            [['discr'], 'string', 'max' => 255],
            [['workspace'], 'exist', 'skipOnError' => true, 'targetClass' => Workspace::className(), 'targetAttribute' => ['workspace' => 'id']],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'numLados' => 'Número de lados',
            'discr' => 'Discr',
            'lado' => 'Lado',
            'base' => 'Base',
            'altura' => 'Altura',
            'hipotenusa' => 'Hipotenusa',
            'radio' => 'Radio',
            'workspace' => 'Workspace',
        ];
    }

    public static function instantiate($row)
    {
        switch ($row['discr']) {
            case Cuadrado::DISCR:
                return new Cuadrado();
            case Triangulo::DISCR:
                return new Triangulo();
            case Hexagono::DISCR:
                return new Hexagono();
            default:
                return new self;
        }

        return parent::instantiate($row);
    }

    public function getArea(){ return 0; }
    public function getPerimetro(){ return 0; }
    public function printr(){ return ''; }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getWorkspace0()
    {
        return $this->hasOne(Workspace::className(), ['id' => 'workspace']);
    }
}

CRUD workspace

Ahora generamos el CRUD para workspace:

Figura 16. Generar CRUD para workspace

Esto nos genera todos los códigos que necesitamos para trabajar con Workspace, pero necesitamos agregar y actualizar las figuras asociadas a un workspace, así que editamos nuestro modelo:

class Workspace extends \yii\db\ActiveRecord
{

    public $figuras = [];

    public function init()
    {
        parent::init();

        $this->loadFiguras();
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'workspace';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['nombre', 'limiteFiguras'], 'required'],
            [['limiteFiguras'], 'integer'],
            [['nombre'], 'string', 'max' => 150],
            [['nombre'], 'string', 'max' => 150],
            ['figuras', 'each', 'rule' => [
                'exist', 'targetClass' => Figura::className(), 'targetAttribute' => 'id'
            ]],
            ['figuras', 'limiteFiguras'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'nombre' => 'Nombre',
            'limiteFiguras' => 'Limite Figuras',
            'figuras' => 'Figuras',
        ];
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getFiguras()
    {
        return $this->hasMany(Figura::className(), ['workspace' => 'id']);
    }

    /**
     * save Figuras
     */
    public function saveFiguras(){

        Figura::updateAll(['workspace' => null], 'workspace='.$this->id);

        if (is_array($this->figuras)) {

            foreach($this->figuras as $figura_id) {

                $fig = Figura::findOne($figura_id);
                $fig->workspace = $this->id;
                $fig->save();
            }
        }

    }

    /**
     * load Figuras
     */
    public function loadFiguras()
    {
        $this->figuras = [];
        if ( empty($this->id) ) return ;

        $this->figuras = Figura::find()
            ->where(['workspace' => $this->id])->all();


    }

    public static function getWorkspaces()
    {
        $workspaces = self::find()->all();

        $items = array();
        foreach ($workspaces as $w){
            $items[$w->id] = $w->nombre;
        }

        return $items;
    }

    public function limiteFiguras($attribute){

        if( ($this->limiteFiguras != null) && !empty($this->figuras)){

            if( intval($this->limiteFiguras) < count($this->figuras)  ){
                $this->addError($attribute, "Supera el límite de Figuras");
            }

        }

    }
}

En el modelo de Workspace es importante agregar las funciones para acceder a las Figuras desde el Workspace. Tales son como guardar y cargar las figuras. Esto es una relación OneToMany donde un Workspace puede tener de cero a muchas figuras. En UML está relación se conoce como Agregación.

En la línea 104 a 114 se agregó la validación de límite de Figuras, el cual no dejará realizar submit al formulario (sea en crear o actualizar) si el número de figuras supera el límite. El mensaje de error aparecerá en el formulario al intentar guardar. La validación se agrega en la línea 14.

Agregación

Comenzemos con Workspace, tenemos que editar tanto el controlador como el formulario. Para el controlador solo agregamos que guarde las figuras haciendo uso del método saveFiguras( ), como lo vimos en el modelo, esto actualizará la tabla de Figuras. También agregamos la función loadFiguras( ) para mostrar correctamente las figuras en el index.

public function actionCreate()
{
    $model = new Workspace();

    if ($model->load(Yii::$app->request->post()) && $model->save()) {

        $model->saveFiguras();
        return $this->redirect(['view', 'id' => $model->id]);
    }

    return $this->render('create', [
        'model' => $model,
        'figuras' => Figura::getFiguras(true),
    ]);
}

En el create.php de Workspace tenemos que pasarle las figuras correspondientes (también en el update.php):

<?= $this->render('_form', [
    'model' => $model,
    'figuras' => $figuras,
]) ?>

Y en el formulario se agrega el campo como listBox para cargar todas las figuras y que el atributo del modelo nos ayude a cargar las ya selccionada si está editando:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $model app\models\Workspace */
/* @var $form yii\widgets\ActiveForm */
?>

<div class="workspace-form">

    <?php $form = ActiveForm::begin(); ?>

    <?= $form->field($model, 'nombre')->textInput(['maxlength' => true]) ?>

    <?= $form->field($model, 'limiteFiguras')->textInput() ?>

    <?= $form->field($model, 'figuras')->listBox( $figuras,['multiple' => true] ) ?>

    <div class="form-group">
        <?= Html::submitButton('Save', ['class' => 'btn btn-success']) ?>
    </div>

    <?php ActiveForm::end(); ?>

</div>

En el index de Workspace agregamos el resto de valores como el Area total, perímetro total y las figuras asociadas:

<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        'id',
        'nombre',
        'limiteFiguras',

        [
            'attribute' => 'figuras',
            'filter' => false,
            'format' => 'raw',
            'value' => function ($model) {
                $model->init();
                $print = '<ul>';
                foreach ($model->figuras as $fig){
                    $print .= '<li>'.$fig->print.'</li>';
                }
                $print .= '</ul>';
                return  $print;
            },
        ],
        [
            'label' => 'Área Total',
            'filter' => false,
            'format' => 'raw',
            'value' => function ($model) {
                $area = 0;
                foreach ($model->figuras as $fig){
                    $area += $fig->getArea();
                }
                return  $area;
            },
        ],
        [
            'label' => 'Apotema Total',
            'filter' => false,
            'format' => 'raw',
            'value' => function ($model) {
                $apotema = 0;
                foreach ($model->figuras as $fig){
                    if( $fig->discr == 'hexagono')
                        $apotema += $fig->apotema;
                }
                return  $apotema;
            },
        ],

        ['class' => 'yii\grid\ActionColumn'],
    ],
]); ?>

Con esto ya tenemos un excelente vistazo para el Workspace:

Figura 17. Página principal para el Workspace. Con todos los datos de las figuras

 

Figura 18. Creación de un Workspace. Disponible para elegir una figura

Para las figuras también debemos agregar la lista de workspace, pero no es necesario realizar funciones de guardar. Se efectúa igual que en Workspace.

Conclusiones

Yii2 tiene mucho potencial, se pueden hacer varias cosas con un excelente nivel de detalle y existen además muchos módulos gracias a composer.

Viene con JQuery, Boostrap entre otras cosas que facilita el inicio de un proyecto.

Los generadores de código con realmente útil, tanto para crear los ActiveRecord como el CRUD de cada módulo. Sin embargo hay que programar un poco a la hora de realizar relaciones bidireccionales. Lo que me parece un poco ineficiente, en este aspecto creo que Symfony 3.4 es mucho mejor.

Es más ligero en información. Si nosotros imprimimos con un var_dump una entidad en Yii 2 nos aparecerá sus atributos, pero en Symfony 3.4 hasta el navegador se congela por la cantidad de información. Esto lo hace mucho más rápido.

Un aspecto que no me gusta es el orm ActiveRecord. El orm de Symfony 3.4 es Doctrine y estan bueno que ni siquiera tienes que ver la base de datos.

Yii 2 te muchas facilidades para trabajar con Front, sobre todo cuando de formulario se trata. Pero para un proyecto grande de back con relaciones bastante pesadas yo sugiero Symfony 3.4

Este mismo proyecto se realizó con Symfony 3.4 en el siguiente enlace: Symfony 3.4 – Proyecto Backend

Resultado

Pueden ver el resultado de este proyecto en nuestro servidor de prueba:

http://35.168.88.201/figuras_yii/web/index.php

El código fuente final en nuestro Github:

https://github.com/arturoverbel/figuras_yii

Fuentes