Yii2, agregando un atributo (virtual) que no está en la base de datos, a un modelo ActiveRecord

Para este ejemplo asumiré que existe una tabla en la BD llamada cliente, con los campos (siendo id el PK): id, nombre, apellido, fecha_nacimiento, email. La SQL necesaria para crear esta tabla sería:

CREATE TABLE `yii2`.`cliente` ( 
    `id` INT NOT NULL AUTO_INCREMENT COMMENT 'ID' , 
    `nombre` VARCHAR(128) NOT NULL COMMENT 'Nombre' , 
    `apellido` VARCHAR(128) NOT NULL COMMENT 'Apellido' , 
    `fecha_nacimiento` DATE NOT NULL COMMENT 'Fecha de Nacimiento' , 
    `email` VARCHAR(128) NOT NULL COMMENT 'Email' , 
    PRIMARY KEY (`id`)
) ENGINE = InnoDB;

Por supuesto existirá también un modelo Cliente que la represente en Yii2 y que contendrá dichos campos como atributos.

El reto se presenta, por ejemplo, al tratar de mostrar los datos de este modelo (tabla) en una vista con el widget GridView, el cual podría verse más o menos así:

<?php GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'nombre',
        'apellido',
        'fecha_nacimiento',
        'email',
        ['class' => 'yii\grid\ActionColumn'],
    ],
    'responsive' => true,
    'hover' => true
]); ?>

Nada extraordinario hasta aquí, verdad? … Bueno, pueden surgir un par de necesidades:

  1. Presentar el nombre y apellido juntos en una sola columna, en lugar de cada uno en una columna separada
  2. Presentar la edad de la persona, en lugar de la fecha de nacimiento.

Pero es claro que estos nuevos atributos no son parte del modelo pues no son parte de los campos de la tabla en la BD, pero Yii2 ofrece la flexibilidad necesaria para implementarlos, incluyendo también cierta lógica que pudiera necesitarse (como la unión de los nombres-apellidos y el cálculo de la edad).

A estos atributos anexos, se les suele llamar atributos o variables virtuales, y se agregan al modelo existente de la siguiente forma:

class Cliente extends \yii\db\ActiveRecord {

    public $nombre_completo; // Defino un atributo virtual
    public $edad; // Defino un atributo virtual

    public function rules() {
        return [
            // Otras reglas pre-existentes ...
            [['nombre_completo', 'edad'], 'safe'],
        ];
    }

    // Este método se invoca después de usarse Cliente::find()
    // Aquí se pueden establecer valores para los atributos virtuales
    public function afterFind() {
        parent::afterFind();
        // Concateno el nombre y apellido en el nuevo atributo virtual
        $this->nombre_completo = "{$this->nombre} {$this->apellido}";
        // Calculo y asigno la edad en años en el nuevo atributo virtual
        $nacimiento = new \DateTime($this->fecha_nacimiento);
        $hoy = new \DateTime();
        $edad = $hoy->diff($nacimiento);
        $this->edad = $edad->y; // Se puede usar también $edad->m (meses), $edad->d (días), etc.
    }

De esta manera, se podrán usar estos atributos virtuales directamente en el GridView:

<?php GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'nombre_completo',
        'edad',
        'email',
        ['class' => 'yii\grid\ActionColumn'],
    ],
    'responsive' => true,
    'hover' => true
]); ?>

Finalmente, aunque esto no es necesario, se puede agregar al modelo, las etiquetas de los nuevos atributos:

class Cliente extends \yii\db\ActiveRecord {

    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'nombre' => 'Nombre',
            'apellido' => 'Apellido',
            'fecha_nacimiento' => 'Fecha de Nacimiento',
            'email' => 'Correo',
            'nombre_completo' => 'Nombre completo',
            'edad' => 'Edad',
        ];
    }

Con esto se logra el listado de registros de la tabla de la BD incluyendo estos campos virtuales nuevos, que además manejan información combinada y/o calculada de la existente:

save image

El siguiente paso sería lograr que se pueda filtrar/ordenar el listado por estos campos, lo cual se describe en detalle en este otro artículo.