Saltar a contenido

Yii Framework

Enlaces

Extensiones

Know How

Instalación

Apache

Para ocultar el script de inicio (index.php) de la URL se puede configurar un fichero .htaccess como sigue:

RewriteEngine on

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# otherwise forward it to index.php
RewriteRule . index.php

Fundamentos

Development mode

En el fichero index.php el modo DEBUG se ajusta en la linea siguiente:

<?php
defined('YII_DEBUG') or define('YII_DEBUG',true);

Ponerlo a false en Producción (fuente).

El componente log admite como una de las rutas de log la clase CWebLogRoute que muestra un log detallado de la ejecución de la página en la parte inferior de la misma. Para activarlo incluir la clase en el array de routes del componente log, por ejemplo:

<?php
'log'=>array(
        'class'=>'CLogRouter',
        'routes'=>array(
                array(
                        'class'=>'CFileLogRoute',
                        'levels'=>'error, warning',
                ),
                array(
                        'class'=>'CWebLogRoute',
                ),
        ),
),

Cuando el desarrollo esté avanzado o al menos en una fase en la que no se esperen muchos cambios en el esquema de base de datos, se puede mejorar la velocidad del componente db dando un valor mayor que 0 a la propiedad CDbConnection::schemaCachingDuration (fuente).

Desactivar el módulo gii en el fichero de configuración cuando la aplicación esté en Producción.

Poner a false el parámetro enableParamLogging del componente db en el fichero de configuración de la aplicación.

Application

Si no se especifica, el controlador predeterminado de una aplicación se llama "site" (SiteController), pero se puede especificar así en el fichero de configuración de la aplicación (fuente):

<?php
array(
    'defaultController'=>'site',
)

Los componentes de aplicación se activan por defecto si son definidos en el fichero de configuración de la aplicación, pero pueden ser desactivados asignando false al atributo enabled (fuente). Los componentes de aplicación se suelen incorporar de un paquete incluido con la instalación de Yii. La lista completa de componentes de aplicación de Yii puede consultarse aquí.

La aplicación genera un log en <AppRoot>/protected/runtime/application.log.

Controller

Si no se especifica la acción a ejecutar de un controller, se ejecuta la acción predeterminada que es index. Puede cambiarse ajustando la variable de instancia pública CController:defaultAction (fuente).

La ruta de los controllers es relativa al directorio <AppRoot>/protected/controllers (fuente).

Si la aplicación está organizada en módulos, la ruta completa de las acciones será del estilo moduleID/controllerID/actionID (fuente).

La instanciación de controllers sigue esta secuencia (fuente):

  • Si se ha especificado CWebApplication::catchAllRequest (se usa normalmente para poner la aplicación en modo mantenimiento).
  • Si el ID del controller se encuentra en el mapa definido en CWebApplication::controllerMap.
  • Finalmente se utiliza la convención comentada anteriormente que enlaza el ID del controller con las clases del directorio <AppRoot>/protected/controllers.

Las acciones se pueden escribir como clases autónomas en lugar de como métodos de un controller (fuente).

Los filtros se pueden definir como métodos de los controllers en cuyo caso deben empezar por filter o como clases independientes descendientes de CFilter (fuente).

Los filtros se pueden mapear en la función filters() de los controllers. Se pueden definir listas negras y listas blancas de acciones sobre las que se aplicará la lista de filtros (fuente).

View

La ruta de las vistas es relativa al directorio <AppRoot>/protected/views (fuente).

Dentro de las vistas se puede acceder al controller mediante $this. Adicionalmente se pueden pasar variables concretas desde el controller al invocar la vista de la siguiente manera:

<?php
$this->render('edit', array(
    'var1'=>$value1,
    'var2'=>$value2,
));

Por defecto se aplica a las vistas el layout definido en <AppRoot>/protected/views/layouts/main.php. Esto puede cambiarse a nivel de aplicación en CWebApplication::layout o a nivel de controller en CController::layout. Si se quiere renderizar una vista sin aplicar layout, en lugar del método CController::render() hay que invocar CController:renderPartial() (fuente).

Component

Los componentes se utilizan para encapsular funcionalidad. Soportan propiedades, eventos y behaviors. Estos últimos se utilizan para implementar herencia múltiple (fuente).

Module

Un módulo es una unidad de código autocontenida que engloba modelos, vistas, controllers y componentes. Es en cierto modo una aplicación, sólo que no puede ser desplegada por separado. Los módulos se pueden anidar en varios niveles. De la misma forma que una aplicación puede contener módulos, éstos pueden contener a su vez otros módulos. Se configuran de la misma forma con el fichero de configuración de la aplicación, indicando la propiedad modules en el módulo contenedor.

Path Alias e importación de clases

De forma predeterminada están definidos los siguientes path alias:

  • system: <YiiRoot>/framework
  • zii: <YiiRoot>/framework/zii
  • application: <AppRoot>/protected
  • webroot: <AppRoot>
  • ext: <AppRoot>/protected/extensions

Para importar clases mejor utilizar Yii:import('path_alias_de_la_clase') que el include, require o require_once de PHP (fuente). Se pueden importar todas las clases de un directorio en bloque con una orden del estilo Yii:import('path_alias_del_directorio.*'). Las clases del propio framework están pre-importadas, por lo que no es necesario hacerlo.

Convenciones

Del framework
  • Si no se especifica en la URL el ID del action se ejecutará el action predeterminada. Si no se cambia (con CController::defaultAction) se ejecutará el action index.
  • Si no se especifica en la URL el ID del controller se ejecutará el controller predeterminado. Si no se cambia (con CWebApplication::defaultController) se ejecutará el controller site.
Nomenclatura para código
  • Utilizar notación camelCase.
  • Comenzar las clases con una letra mayúscula. La letra C se usa en las clases del framework => Se recomienda utilizar otra (T por ejemplo).
  • Comenzar las variables privadas de clase con underscore.
  • Terminar las clases controller con Controller.
  • Dentro de una clase controller, los métodos de action comenzarlos con action.
  • Terminar las clases de action con Action.
Nomenclatura para bases de datos
  • Nombres de tablas y campos en minúsculas.
  • Palabras separadas con underscore (por ejemplo: direccion_cliente).
  • Nombres de tablas en singular.
  • Nombres de tablas con prefijo propio de la aplicación (por ejemplo: gestoo_clientes).

Formularios

Creando el Modelo

Validación

Las reglas de validación se definen en la función rules() del Model componiendo un array en el que cada elemento se define como sigue:

<?php
array('ListaAtributos', 'Validador', 'on'=>'ListaEscenarios', ...opciones adicionales)

donde ListaAtributos es una lista separada por comas de nombres de atributos del Model; Validador especifica el tipo de validación; el parámetro on es opcional y especifica la lista de escenarios en las que se aplica la regla; las opciones adicionales son pares clave-valor para inicializar propiedades de los validadores usados.

Hay tres tipos de validadores:

  • Validador puede ser el nombre de un método de la clase Modelo. El método tiene que tener la siguiente forma:
<?php
/**
 * @param string $attribute the name of the attribute to be validated
 * @param array $params options specified in the validation rule
 */
public function ValidatorName($attribute,$params) { ... }
  • Validador puede ser el nombre de una clase de validación. En este caso cuando la regla es aplicada, una instancia de la clase de validación se crea y se inicializa con las opciones adicionales. La clase de validación debe extender de CValidator.
  • Validador puede ser una de las clases de validación predefinidas en Yii (lista).
Asignación de atributos

Las asignaciones de atributos entre formulario y modelo se pueden hacer en bloque por medio de la función de PHP $_POST[<clase_modelo>]. Sólo se asignan los atributos considerados seguros, es decir los que tienen asociada una regla de validación. Hay que tener en cuenta que las reglas de validación se pueden asociar a escenarios por lo que se asignarán unos atributos u otros según el escenario declarado al instanciar el modelo. Por ejemplo:

<?php
// en escenario login
$model=new User('login');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];

// en escenario register
$model=new User('register');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];

Es importante recordar que las reglas de validación se usan para chequear los datos introducidos por los usuarios y no los datos generados por código (timestamps o primary keys). Por lo tanto no hay que definir reglas de validación en los atributos que no esperamos que sean introducidos por el usuario. Por ejemplo, si marcamos como seguro el atributo id de un modelo (por medio del validador safe por ejemplo), aunque inicialmente no incluyamos el campo en el formulario, un atacante podría enviarlo y la asignación en bloque sobre el modelo lo incluiría y podría permitir cambiar el ID del registro en base de datos.

Provocando la validación

Una vez que el modelo recibe los atributos (normalmente procedentes del usuario por medio de un formulario) la validación se puede desencadenar invocando el método CModel::validate(). Retornará true o false en función del resultado. En los modelos tipo CActiveRecord de forma predeterminada se desencadena también al invocar el método save(). Antes de invocar la validación se puede indicar el escenario (ajustando la propiedad scenario del modelo) para elegir el conjunto de reglas de validación que se desean.

Una vez que la validación se ha hecho, los posibles errores se almacenan en el propio modelo. Se pueden obtener con las funciones CModel::getErrors() (devuelve todos los errores) y CModel::getError() (devuelve sólo el primero).

Recogiendo varios modelos a la vez

Para recoger varios modelos o registros de un tipo simultáneamente (o entrada tabular) necesitamos crear o rellenar un array de instancias del modelo, dependiendo de si queremos insertar o modificar registros. Luego recogeremos los datos de entrada del usuario de la variable $_POST y los asignaremos a cada modelo. Cuando recogíamos un solo modelo hacíamos $_POST[<clase_modelo>], ahora haremos $_POST[<clase_modelo>][$i]. El action de modificación tendrá el siguiente aspecto:

<?php
public function actionBatchUpdate() {
    // retrieve items to be updated in a batch mode
    // assuming each item is of model class 'Item'
    $items=$this->getItemsToUpdate();
    if(isset($_POST['Item'])) {
        $valid=true;
        foreach($items as $i=>$item) {
            if(isset($_POST['Item'][$i]))
                $item->attributes=$_POST['Item'][$i];
            $valid=$item->validate() && $valid;
        }
        if($valid)  // all items are valid
            // ...do something here
    }
    // displays the view to collect tabular input
    $this->render('batchUpdate',array('items'=>$items));
}

y la vista incluirá algo como:

<div class="form">
<?php echo CHtml::beginForm(); ?>
<table>
<tr><th>Name</th><th>Price</th><th>Count</th><th>Description</th></tr>
<?php foreach($items as $i=>$item): ?>
<tr>
<td><?php echo CHtml::activeTextField($item,"[$i]name"); ?></td>
<td><?php echo CHtml::activeTextField($item,"[$i]price"); ?></td>
<td><?php echo CHtml::activeTextField($item,"[$i]count"); ?></td>
<td><?php echo CHtml::activeTextArea($item,"[$i]description"); ?></td>
</tr>
<?php endforeach; ?>
</table>

<?php echo CHtml::submitButton('Save'); ?>
<?php echo CHtml::endForm(); ?>
</div><!-- form -->

Form Builder

Yii incluye un sistema de generación de formularios dinámico. Se basa en el objeto CForm. El uso se basa en crear el objeto, pasarle un array de configuración (se suele mantener en un fichero aparte) y renderizarlo desde una vista.

Si se definen formularios anidados (como se ve en la documentación), el formulario renderizado contendrá únicamente una etiqueta form HTML dentro de la cual los campos se identificarán con nombres tipo Model[atributo], donde Model se corresponderá con uno de los modelos asociados a uno de los sub-forms.

Cuando se utiliza el Form Builder la asignación de atributos del formulario al modelo se puede gestionar desde el propio objeto CForm. Para ello existe el método loadData(). Esta carga se hace automáticamente cuando es necesario, como por ejemplo si invocamos el método validate() de CForm que valida los modelos asociados.

Documentación completa aquí.

Base de Datos

Objetos DAO

El modelo de acceso a bases de datos de Yii se basa en los siguientes objetos:

  • CDbConnection: Representa una conexión a una base de datos. Normalmente se configura como componente de la aplicación. Es interesante aprovechar la funcionalidad de prefijo de tablas por medio de la propiedad CDbConnection::tablePrefix. Si se utiliza, luego en las consultas hay que indicar las tablas sin el prefijo (que queda configurado en esta propiedad) y entre dobles llaves (por ejemplo: {{usuario}}).
  • CDbCommand: Representa una sentencia SQL para ejecutar contra una base de datos. Se puede crear mediante el método createCommand() del objeto CDbConnection. Se ejecuta con los métodos query() para selects y execute() para el resto. query() devuelve un CDbDataReader. Existen cuatro variantes de query() que devuelven datos directamente:
    • queryAll(): Devuelve un array con todos los registros del resultado de la consulta.
    • queryRow(): Devuelve el primer registro del resultado de la consulta.
    • queryColumn(): Devuelve un array con el valor de la primera columna del resultado de la consulta.
    • queryScalar(): Devuelve el primer campo del primer registro del resultado de la consulta. Útil para una consulta tipo count.
  • CDbDataReader: Representa un conjunto de registros que se lee sólo hacia adelante resultado de una consulta. Normalmente el objeto se recorre ejecutando sucesivamente el método read() hasta que devuelve false. A continuación se muestran tres formas de extraer los datos de CDbDataReader:
    <?php
    $dataReader=$command->query();
    // calling read() repeatedly until it returns false
    while(($row=$dataReader->read())!==false) { ... }
    // using foreach to traverse through every row of data
    foreach($dataReader as $row) { ... }
    // retrieving all rows at once in a single array
    $rows=$dataReader->readAll();
    
  • CDbTransaction: Representa una transacción en la base de datos. A continuación se muestra una estructura típica transaccional con este objeto:
    <?php
    $transaction=$connection->beginTransaction();
    try {
        $connection->createCommand($sql1)->execute();
        $connection->createCommand($sql2)->execute();
        //.... other SQL executions
        $transaction->commit();
    }
    catch(Exception $e) { // an exception is raised if a query fails
        $transaction->rollBack();
    }
    
Binding de parámetros

Para evitar ataques de inyección de SQL y para mejorar el rendimiento de consultas que se repiten habitualmente, se recomienda parametrizar las consultas con los métodos CDbCommand::bindParam() y CDbCommand::bindValue(). Ejemplo:

<?php
// una query con dos placeholders ":username" and ":email"
$sql="INSERT INTO tbl_user (username, email) VALUES(:username,:email)";
$command=$connection->createCommand($sql);
$command->bindParam(":username",$username,PDO::PARAM_STR);  // reemplaza el placeholder ":username" con el valor de la variable $username
$command->bindParam(":email",$email,PDO::PARAM_STR);        // reemplaza el placeholder ":email" con el valor de la variable $email
$command->execute();
// inserta otra fila con un nuevo conjunto de valores
$command->bindParam(":username",$username2,PDO::PARAM_STR);
$command->bindParam(":email",$email2,PDO::PARAM_STR);
$command->execute();
Binding de variables

Se pueden asociar variables PHP a columnas de una query de manera que tomen valor automáticamente cada vez que leamos el siguiente registro. Por ejemplo:

<?php
$sql="SELECT username, email FROM tbl_user";
$dataReader=$connection->createCommand($sql)->query();
$dataReader->bindColumn(1,$username);  // asocia la primera columna (username) con la variable $username
$dataReader->bindColumn(2,$email);     // asocia la segunda columna (email) con la variable $email
while($dataReader->read()!==false)
{
    // $username y $email contienen los valores correspondientes a la fila en curso
}

Query Builder

Existe un conjunto de herramientas para construir sentencias SQL de una manera orientada a objeto. Como ventajas, permite construir sentencias de forma programática, evita problemas de sintaxis o ambigüedades, previene ataques de inyección de SQL al usar binding de parámetros y ofrece abstracción de base de datos lo que facilitaría una migración más adelante.

Consultar la documentación completa aquí.

Active Record

Active Record (AR) es una popular técnica de mapeo Objeto-Relacional (ORM) que permite manipular una tabla de manera orientada a objeto. De forma dinámica se genera una entidad modelo a partir de una tabla con prácticamente nada de código. Se recomienda que todos los nombres de los campos de las tablas estén en minúsculas para no tener problemas con el cambio de gestor de base de datos, ya que algunos son sensibles a mayúsculas y otros no.

Creando registros

Simplemente creando un objeto AR, asignando valores a sus propiedades y llamando al método CActiveRecord::save().

Para asignar expresiones SQL a algunos de los campos o propiedades de los objetos AR, hay que utilizar el objero CDbExpression, por ejemplo:

<?php
$post=new Post;
$post->create_time=new CDbExpression('NOW()');  // $post->create_time='NOW()'; no funcionaría porque 'NOW()' sería tratado como cadena
$post->save();
Recuperando registros

Para recuperar un solo registro tenemos los métodos:

<?php
$post=Post::model()->find($condition,$params);                          // encuentra la primera fila que satisface la condición especificada
$post=Post::model()->findByPk($postID,$condition,$params);              // encuentra la fila con el primary key especificado
$post=Post::model()->findByAttributes($attributes,$condition,$params);  // encuentra la fila con los valores de los atributos especificados
$post=Post::model()->findBySql($sql,$params);                           // encuentra la primera fila usando la SQL especificada

Si no se encuentra ningún registro, el resultado será null.

Para recuperar varios registros tenemos los métodos:

<?php
$posts=Post::model()->findAll($condition,$params);                          // encuentra todas las filas que satisfacen la condición especificada
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);             // encuentra todas las filas con el primary key especificado
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);  // encuentra todas las filas con los valores de los atributos especificados
$posts=Post::model()->findAllBySql($sql,$params);                           // encuentra todas las filas usando la SQL especificada

Adicionalmente existen los siguientes métodos:

<?php
$n=Post::model()->count($condition,$params);        // obtiene el número de filas que satisfacen la condición especificada
$n=Post::model()->countBySql($sql,$params);         // obtiene el número de filas que se obtendrían con las SQL especificada
$exists=Post::model()->exists($condition,$params);  // comprueba si existe al menos una fila que satisface la condición especificada

En todos los casos, en lugar de la pareja de strings $condition y $params, podemos entregar un objeto de tipo CDbCriteria más cosas además de una cláusula WHERE. Alternativamente a pasar un CDbCriteria podemos pasar un array con las propiedades de permitan construir este objeto dinámicamente.

Actualizando registros

Modificando un AR recién obtenido y guardando se producirá una actualización en base de datos:

<?php
$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // guarda el cambio en la base de datos

Cuando se guarda (método save()) se invoca automáticamente la validación (a no ser que se pase el argumento false).

Si el registro es nuevo se hará un insert en lugar de un update. Se puede interrogar al modelo si estamos en uno u otro caso por medio del método CActiveRecord::isNewRecord().

Se puede actualizar un AR que no se haya cargado previamente con los siguientes métodos:

<?php
Post::model()->updateAll($attributes,$condition,$params);       // actualiza las filas que satisfacen la condición especificada
Post::model()->updateByPk($pk,$attributes,$condition,$params);  // actualiza las filas que satisfacen la condición y la primary key especificadas
Post::model()->updateCounters($counters,$condition,$params);    // actualiza las columnas tipo counter en las filas que satisfacen la condición especificada
Borrando registros

Para borrar un solo registro utilizar el método delete() sobre una instancia AR obtenida previamente:

<?php
$post=Post::model()->findByPk(10); // localizamos el registro
$post->delete(); // lo borramos. El objeto $post seguirá existiendo, pero el registro en base de datos no

También se pueden borrar registros sin cargarlos previamente con los siguientes métodos:

<?php
Post::model()->deleteAll($condition,$params);       // borra las filas que se ajusten a la condición especificada
Post::model()->deleteByPk($pk,$condition,$params);  // borra las filas que se ajusten a la condición especificada y a la primary key
Comparando registros

Usar el método CActiveRecord::equals().

Adaptación de datos

CActiveRecord proporciona los siguientes métodos para adaptar los datos que se intercambian entre modelo y base de datos en uno y otro sentido:

  • beforeValidate y afterValidate: invocados antes y después de que se efectúe la validación.
  • beforeSave y afterSave: invocados antes y después de que se guarde una instancia AR.
  • beforeDelete y afterDelete: invocados antes y después de que se borre una instancia AR.
  • afterConstruct: invocado en cada instancia AR creada con el operador new.
  • beforeFind: invocado antes de que un método de búsqueda de AR se use para efectuar una consulta (por ejemplo find(), findAll()).
  • afterFind: invocado después de que cada instancia AR es creada como resultado de una búsqueda.
Usando transacciones

Cada instancia AR contiene una propiedad llamada dbConnection que soporta transacciones de la siguiente forma:

<?php
$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try {
    // find y save son dos pasos entre los que puede interferir otra petición
    // encerraremos el intervalo entre ambas órdenes en una transacción para asegurar consistencia e integridad
    $post=$model->findByPk(10);
    $post->title='new post title';
    $post->save();
    $transaction->commit();
}
catch(Exception $e) {
    $transaction->rollBack();
}
Named scopes

Un named scope representa un criterio de búsqueda al que le asignamos un nombre y que puede ser combinado con otros named scopes para refinar una búsqueda de AR's. Se declaran en el método CActiveRecord::scopes() como pares nombre-criterio de filtrado. Por ejemplo:

<?php
class Post extends CActiveRecord {
    //......
    public function scopes() {
        return array(
            'published'=>array(
                'condition'=>'status=1',
            ),
            'recently'=>array(
                'order'=>'create_time DESC',
                'limit'=>5,
            ),
        );
    }
}

La declaración de cada named scope se corresponde con un array que sirve para inicializar un objeto CDbCriteria. Una vez definidos se pueden combinar en las sentencias de búsqueda como por ejemplo:

<?php
$posts=Post::model()->published()->recently()->findAll();

Deben usarse siempre sobre métodos class-level, es decir partiendo de ModelName::model()->, no directamente de ModelName::. Si se quieren parametrizar hay que definirlos como métodos class-level autónomos en lugar de configurarlos en el array de CActiveRecord::scopes. Por ejemplo para parametrizar el named scope recently definido previamente se haría:

<?php
public function recently($limit=5) {
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'create_time DESC',
        'limit'=>$limit,
    ));
    return $this;
}

Luego podríamos llamar:

<?php
$posts=Post::model()->published()->recently(3)->findAll();

Se puede definir un named scope predeterminado. De esta forma será aplicado automáticamente en todas las consultas. Para ello se define en el método CActiveRecord::defaultScope sin darle nombre como sí había que hacer en CActiveRecord::scopes. Por ejemplo:

<?php
public function defaultScope() {
    return array(
        'condition'=>"language='".Yii::app()->language."'",
    );
}

ActiveRecord relacional

Declaración de relaciones

Para ilustrar los ejemplos de esta sección se utilizará el siguiente modelo de datos: er.png

Las relaciones entre dos clases AR tienen que ver con la relación entre las tablas en base de datos. En base de datos tendremos las relaciones:

  • one-to-many: entre tbl_user y tbl_post por ejemplo.
  • ono-to-one: entre tbl_user y tbl_profile por ejemplo.
  • many-to-many: entre tbl_category y tbl_post por ejemplo.

En el lado AR hay cuatro tipos posibles:

  • BELONGS_TO: Si la relación entre las tablas A y B es one-to-many, entonces B belongs_to A. En el ejemplo A=tbl_user y B=tbl_post.
  • HAS_MANY: Si la relación entre las tablas A y B es one-to-many, entonces A has_many B. En el ejemplo A=tbl_user y B=tbl_post.
  • HAS_ONE: Si la relación entre las tablas A y B es ono-to-one. Es un caso especial de HAS_MANY en el que A tiene al menos un B. En el ejemplo A=tbl_user y B=tbl_profile.
  • MANY_MANY: Si la relación entre las tablas A y B es many-to-many. Se necesita una tabla intermedia que descomponga la relación en dos relaciones 1:n con la tabla intermedia (C). En el ejemplo A=tbl_post, B=tbl_category y C=tbl_post_category.

Estas relaciones se declaran en el método CActiveRecord::relations() definiendo un array en el que cada elemento representa una relación de la siguiente forma:

<?php
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...opciones adicionales)

donde VarName es el nombre de la relación, RelationType indica el tipo de relación (se pueden aprovechar las siguientes constantes: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY y self::MANY_MANY), ClassName es el nombre de la clase AR con que se está estableciendo la relación y ForeignKey indica la foreign key implicada en la relación. Las opciones adicionales se explicarán en la sección Opciones de consultas relacionales.

A continuación se definen las relaciones en los modelos User y Post:

<?php
class Post extends CActiveRecord {
    //......

    public function relations() {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
            'categories'=>array(self::MANY_MANY, 'Category',
                'tbl_post_category(post_id, category_id)'),
        );
    }
}

class User extends CActiveRecord {
    //......

    public function relations() {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}
Consultas relacionales

Por defecto se aplica el sistema lazy loading en los campos de un modelo que se refieren a una relación. Para forzar un eager loading hay que preceder los métodos de búsqueda con el método with(). Por ejemplo:

<?php
$posts=Post::model()->with('author')->findAll();

Se pueden especificar varios nombres de relaciones separados por comas en el método with() o pasarle un CDbCriteria::with al método de búsqueda. Por ejemplo:

<?php
$criteria=new CDbCriteria;
$criteria->with=array(
    'author.profile',
    'author.posts',
    'categories',
);
$posts=Post::model()->findAll($criteria);
Consultas relacionales sin obtener los modelos relacionados

En ocasiones nos interesa hacer una consulta usando una relación pero no queremos los datos de los propios modelos relacionados. Por ejemplo si quisiéramos los usuarios que tienen post's en estado published=1, pero no los post's en sí, podríamos hacer lo siguiente:

<?php
$users=User::model()->with(array(
    'posts'=>array(
        'select'=>false,
        'joinType'=>'INNER JOIN',
        'condition'=>'posts.published=1',
    ),
))->findAll();
Opciones de consultas relacionales

Las opciones adicionales que se pueden indicar cuando se definen las relaciones que se mencionaban en el apartado Declaración de relaciones son las siguientes (consultar su funcionamiento en la documentación oficial):

  • select
  • condition
  • params
  • on
  • order
  • with
  • joinType
  • alias
  • together
  • join
  • group
  • having
  • index
  • scopes

Adicionalmente las siguientes opciones están disponibles para ciertas relaciones durante el lazy loading (consultar su funcionamiento en la documentación oficial):

  • limit
  • offset
  • through

Ejemplo de uso:

<?php
class User extends CActiveRecord {
    public function relations() {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                            'order'=>'posts.create_time DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}
Desambiguación de columnas

En una consulta relacional con AR, el alias para la tabla primaria es t, mientras que el alias de la tabla relacionada se corresponde con el nombre de la relación. Por ejemplo:

<?php
$posts=Post::model()->with('comments')->findAll(array(
    'order'=>'t.create_time, comments.create_time'
));
Opciones dinámicas en consultas relacionales

Se pueden sobreescribir las opciones que se definen en las relaciones indicando otras mediante el método with() o con la opción adicional with.

Rendimiento de consultas relacionales

Existe una opción que permite elegir entre dos métodos para realizar consultas eager loading. Se trata de la opción together que determina si se realizará una única consulta con el join de las tablas relacionadas (más rendimiento) o una consulta por cada tabla (menos información redundante). Consultar documentación completa.

Consulta estadística

Existe un tipo de relación especializado en obtener datos estadísticos entre tablas. Sólo se puede aplicar sobre tablas relacionadas con HAS_MANY y MANY_MANY y el tipo de relación se designa con self::STAT. Por ejemplo:

<?php
class Post extends CActiveRecord {
    public function relations() {
        return array(
            'commentCount'=>array(self::STAT, 'Comment', 'post_id'),
            'categoryCount'=>array(
                self::STAT, 'Category', 'post_category(post_id, category_id)'
            ),
        );
    }
}

Por defecto la consulta estadística consistirá en un COUNT, pero podemos realizar ajustes con las siguientes opciones adicionales al definir la relación:

  • select: la expresión estadística. Por defecto es COUNT(*), lo que realiza un conteo de los objetos hijos.
  • defaultValue: el valor que se asignará a los registros que no reciban un resultado de la consulta estadística. Por ejemplo, si un post no tiene comentarios, commentCount recibirá este valor. Por defecto vale 0.
  • condition: la cláusula WHERE. Su valor por defecto es vacío.
  • params: los parámetros a asociar a la sentencia SQL generada. Debe ser un array de parejas clave-valor.
  • order: la cláusula ORDER. Su valor por defecto es vacío.
  • group: la cláusula GROUP BY. Su valor por defecto es vacío.
  • having: la cláusula HAVING. Su valor por defecto es vacío.
Consulta relacional con named scopes

Se pueden combinar las consultas relacionales con los named scopes aplicando estos últimos tanto sobre la tabla principal como sobre las relacionadas. En el primer caso se haría combinando named scopes y consulta relacional en cadena de la siguiente forma:

<?php
$posts=Post::model()->published()->recently()->with('comments')->findAll();

Aplicar named scopes a los modelos relacionados se hace indicandolos detrás de la relación separándolos con ":". Por ejemplo:

<?php
$posts=Post::model()->with('comments:recently:approved')->findAll();

Los named scopes pueden forzarse en la propia declaración de la relación del siguiente modo:

<?php
class User extends CActiveRecord {
    public function relations() {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                'with'=>'comments:approved'),
        );
    }
}

Si los named scopes del objeto relacionados admiten parámetros, pueden pasarse en la consulta relacional de la siguiente forma:

<?php
$users=User::model()->findAll(array(
    'with'=>array(
        'posts'=>array(
            'scopes'=>array(
                'rated'=>5,
            ),
        ),
    ),
));
Consulta relacional con through

Se definen así:

<?php
'comments'=>array(self::HAS_MANY,'Comment',array('key1'=>'key2'),'through'=>'posts')

donde en array('key1'=>'key2'):

  • key1 es una clave definida en la relación especificada en through (posts en este caso)
  • key2 es una clave definida en el modelo al que esta relación apunta (Comment en este caso)

through puede utilizarse en relaciones HAS_ONE y HAS_MANY.

Para ilustrar el caso HAS_MANY se utilizará el siguiente modelo de datos: has_many_through.png

Por ejemplo, para obtener los usuarios de un grupo en concreto:

<?php
class Group extends CActiveRecord {
    //...
    public function relations() {
        return array(
            'roles'=>array(self::HAS_MANY, 'Role', 'group_id'),
            'users'=>array(self::HAS_MANY, 'User', array('user_id'=>'id'), 'through'=>'roles'
            ),
        );
    }
}

Un caso más complicado sería obtener todos los comentarios de todos los usuarios de un grupo en concreto. Se lograría añadiendo a las relaciones anteriores la siguiente:

<?php
'comments'=>array(
    self::HAS_MANY, 'Comment', array('id'=>'user_id'), 'through'=>'users'
),

Una variante del HAS_MANY through es cuando el modelo origen y destino es el mismo. Por ejemplo, dado el siguiente modelo de datos: through_self.png

Podríamos definir las siguientes relaciones:

<?php
class User extends CActiveRecord {
    //...
    public function relations() {
        return array(
            'mentorships'=>array(
                self::HAS_MANY,'Mentorship','teacher_id','joinType'=>'INNER JOIN'
            ),
            'students'=>array(
                self::HAS_MANY,'User',array('student_id'=>'id'),
                    'through'=>'mentorships','joinType'=>'INNER JOIN'
            ),
        );
    }
}

De manera que podríamos obtener los alumnos que son enseñados por un profesor (alumnos y profesor son ambos usuarios):

<?php
$teacher=User::model()->findByPk(1);
$students=$teacher->students;

Para ilustrar el caso de HAS_ONE through nos basaremos en: has_one_through.png

Por ejemplo, obtener la dirección de un usuario cuando ésta está relacionada con el usuario por medio de un profile:

<?php
class User extends CActiveRecord {
    //...
    public function relations() {
        return array(
            'profile'=>array(self::HAS_ONE, 'Profile', 'user_id'),
            'address'=>array(self::HAS_ONE, 'Address', array('id'=>'profile_id'), 'through'=>'profile'
            ),
        );
    }
}
Tabla de relación MANY_MANY con datos adicionales

Para el caso en que la tabla intermedia que desarrolla una relación MANY_MANY entre otras dos tablas contenga campos con información adicional a los dos foreign keys que apuntan a las dos tablas, seguir este artículo del wiki y éste del foro (ignorar las respuestas más viejas que son de cuando no existía el mecanismo through).

Cosas interesantes

Application Singleton

Yii:app() Sirve como almacén central de ajustes de configuración a nivel de aplicación (fuente).

Añadir jQuery

Añadir jQuery a una página. Código visto en un action, pero también se podría insertar directamente en la vista en que vaya a ser necesario:

<?php
// Adding jQuery to the view page.
Yii::app()->getClientScript()->registerCoreScript('jquery');
Filtrado referido en BootGridView o CGridView

Imaginemos el modelo empleado asociado en n:1 al modelo departamento. En el listado de empleados queremos mostrar el departamento al que pertenece y poder filtrar por el nombre del mismo (no por el ID de departamento como ocurriría si indicáramos el campo departamento_id que pertenece a empleado). Ver ejemplo en el filtrado del campo xproduct.product en admin.php del modelo Product en R135 del repositorio svn+ssh://edumoreno@192.168.2.250/var/lib/subversion/gestoo/trunk.