Entidades, buscadores y repositorios

Existe una serie de formas para interactuar con los datos en XF2. En XF1 esto se orientó principalmente hacia la escritura de sentencias SQL sin procesar dentro de los archivos Model (de modelo). El enfoque en XF2 se aleja de este y se ha agregado una serie de nuevas formas en su lugar. Primero, miremos de interactuar con la base de datos directamente con sentencias de SQL lo que, por supuesto, aún es posible.

El Finder

Por todo lo dicho, el uso de consultas directas a la base de datos es la última versión

Se ha introducido un nuevo sistema "Finder" (Encontrador) que permite generar las consultas programáticamente de forma orientada a objetos para que no se necesite escribir consultas a la base de datos. El sistema Finder trabaja mano a mano con el sistema Entidad, del que se tratrará con más detalle luego. El primer argumento que se pasa al método finder es el nombre corto de clase de la Entidad con la que se desea trabajar. Vamos a convertir algunas de las consultas mencionadas en la sección anterior para usar en su lugar el sistema Finder. Por ejemplo, para acceder a un solo registro de usuario:


$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->fetchOne();

                

Una de las principales diferencias entre el enfoque de consulta directa y el uso del Finder es que la unidad base de los datos devueltos por el Finder no es una matríz. En el caso del objeto Finder que llama al método fetchOne (sólo devuelve una única fila de la base de datos), se devuelve un solo objeto Entidad.

Veamos un enfoque ligeramente diferente que devolverá varias filas:


$finder = \XF::finder('XF:User');
$users = $finder->limit(10)->fetch();

                

Éste ejemplo consultará 10 registros desde la tabla xf_user y los devolverá como un objeto ArrayCollection. Éste es un objeto especial que actúa similarmente a una matríz y que es tranversal (puede recorrerse) y tiene algunos métodos especiales que pueden indicar el número total de entradas que tiene, agruparse por ciertos valores u otra matríz con operaciones tal como filtrado, combinación, obtención de la primera o última entrada, etc.

Las consultas del Finder, generalmente recuperan todas las columnas de una tabla, por lo que no hay un equivalente específico para buscar sólo ciertos valores, ciertas columnas.

En su lugar, para obtener un simple valor, solo habría que buscar una entidad y leer el valor directamente desde ella:


$finder = \XF::finder('XF:User');
$username = $finder->where('user_id', 1)->fetchOne()->username;

                

De igual manera, para obtener una matríz de valores desde una sola columna, se puede usar el método pluckFrom:


$finder = \XF::finder('XF:User');
$usernames = $finder->limit(10)->fetch()->pluckFrom('username');

                

Hasta ahora hemos visto el Finder para aplicar algo tan simple como las restricciones where y limit. Así que vamos a mirar el Finder con más detalle, incluyendo un poco más de detalle al método where en sí mismo.

Método where

El método where soporta hasta tres argumentos. El primero es la condición en sí misma, ej. la columna que se está consultando. El segundo debe ser un operador. El tercero es el valor que se está buscando. Si solo se cumplimentan dos argumentos, como puede verse arriba, implica que el operador es automáticamente =. A continuación se detalla la lista de otros operadores válidos:

  • =
  • <>
  • !=
  • >
  • >=
  • <
  • <=
  • LIKE
  • BETWEEN

Por lo tanto, podríamos obtener una lista de los usuarios válidos que se registraron en los últimos 7 días:


$finder = \XF::finder('XF:User');
$users = $finder->where(
        'user_state', 'valid'
        )->where(
            'register_date', '>=', time() - 86400 * 7
        )->fetch();

                

Como puede verse se puede llamar al método where cuantas veces se desee y, por añadidura a ello, puede elegirse pasar una matríz con el único argumento del método y generar las condiciones en una única llamada. El método matríz soporta dos tipos de argumentos y ambos se pueden usar en la consulta que construimos anteriormente:


$finder = \XF::finder('XF:User');
$users = $finder->where(['user_state' => 'valid', ['register_date', '>=', time() - 86400 * 7] ])
->fetch();

                

No suele ser recomendable ni claro mezclar un uso como este, pero demuestra la flexibilidad del método en tanto. Ahora que las condiciones están en la matríz, se puede especificar el nombre de la columna (como clave de la matríz) y el valor para un implícito operador = o bien definir otra matríz que contenga la columna, el operador y el valor.

Método whereOr

En los ejemplos de arriba deben cumplirse ambas condiciones, ej. cada condición se empalma con la otra a través del operador AND. Sin embargo, a veces solo es necesrio satisfacer parte de la condición y esto es posible al usar el método whereOr. Por ejemplo, si se desea buscar usuarios que bien no sean válidos o bien hayan publicado cero mensajes, debe generarse como sigue:


$finder = \XF::finder('XF:User');
$users = $finder->whereOr(
    ['user_state', '<>', 'valid'],
    ['message_count', 0]
)->fetch();

                

Similar al ejemplo de la sección previa, también se pasan dos condiciones como argumentos separados, puede pasarse una matríz de condiciones en el primer argumento:


$finder = \XF::finder('XF:User');
$users = $finder->whereOr([
    ['user_state', '<>' 'valid'],
    ['message_count', 0],
    ['is_banned', 1]
])->fetch();

                

Método with

El método with esencialmente es equivalente a usar la sintaxis INNER|LEFT JOIN, aunque depende de que la Entidad haya definido sus "Relaciones". No vamos a entrar en esto hasta la siguiente página, aunque esto sólo debiera dar una comprensión de cómo funciona. Vamos a usar el Finder Thread para obtener un tema específico:


$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->where('thread_id', 123)->fetchOne();

                

Esta consulta obtendrá la Entidad Thread cuando thread_id = 123 aunque también hará una unión con la tabla xf_forum entre bastidores. En términos de controlar cómo se hace un INNER JOIN en vez de un LEFT JOIN, es para lo que está el segundo argumento. En este caso se ha establecido el argumento "debe existir" a cierto, por lo que se le da la vuelta a la sintaxis de unión para usar INNER en vez de con el predeterminado LEFT.

Veremos más detalles sobre el acceso a la extracción de datos desde esta unión en la siguiente sección.

También es posible pasar una matríz de relaciones al método with para hacer múltiples uniones.


$finder = \XF::finder('XF:Thread');
$thread = $finder->with(['Forum', 'User'], true)->where('thread_id', 123)->fetchOne();

                

Esto lo uniría a la tabla xf_user para obtener, además, al autor del tema. Sin embargo, con el segundo argumento siendo true, no es necesario hacer una unión INNER para unir al usario y así encadenar los métodos en su lugar:


$finder = \XF::finder('XF:Thread');
$thread = $finder->with('Forum', true)->with('User')->where('thread_id', 123)->fetchOne();

                

Métodos order, limit y limitByPage

Método order

Este método permite modificar la consulta por lo que se extraen los resultados en un orden específico. Tiene dos argumentos, el primero es la columna nombre y el segundo es, optionalmente, la dirección del orden. Por lo que si se desea el listado de los 10 usuarios con más mensajes, podría construirse la consulta algo así:


$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->limit(10);

Nota

Probablemente sea ahora la mejor hora de mencionar que los métodos finder pueden llamarse, en la mayoría de ocasiones, por cualquier orden. Por ejemplo: $threads = $finder->limit(10)->where('thread_id', '>', 123)->order('post_date')->with('User')->fetch(); aunque si se han escrito consultas de MySQL en ese orden, ciertamente que se encontrarán algunos problemas de sintaxis, el sistema Finder seguirá construyendo todo en el orden correcto y el código anterior, aunque parezca extraño y probablemente no recomendado, es perfectamente válido.

Como en una consulta estandar de MySQL, es posible ordenar el resultado estableciéndolo en múltiples columnas. Para hacer esto, sólo hay que llamar al método order de nuevo. También es posible pasar varias claúsulas order al método order usando una matríz.


$finder = \XF::finder('XF:User');
$users = $finder->order('message_count', 'DESC')->order('register_date')->limit(10);

                

Método limit

Ya hemos visto cómo limitar una consulta y obtener devueltos un número específico de registros:


$finder = \XF::finder('XF:User');
$users = $finder->limit(10)->fetch();

                

Sin embargo, existe en la actualidad una alternativa para llamar directamente al método limit:


$finder = \XF::finder('XF:User');
$users = $finder->fetch(10);

                

Es posible pasar directamente el límite al método fetch(). También vale la pena señalar que el método limit (y fetch) tienen dos argumentos. El primero, obviamente, es el propio límite inicial y el segundo es el offset.


$finder = \XF::finder('XF:User');
$users = $finder->limit(10, 100)->fetch();

                

El offset significa aquí que se descartan los 100 primeros resultados y se devolverán los 10 siguientes tras estos. Este tipo de enfoque es útil para proporcionar resultados paginados, aunque en realidad también existe una manera más fácil de hacer esto...

Método limitByPage

Éste método es una clase de método auxiliar que en último lugar configura el límite apropiado y el límite de comienzo basandose en la "página" que actualmente se está viendo y de cuantos "por página" se requieren.


$finder = \XF::finder('XF:User');
$users = $finder->limitByPage(3, 20);

                

En este caso, el límite se establece en 20 (que es el valor por página) y el offset se establece en 40 porque comenzaremos la página 3.

Aclaración de lms


(3 (página actual) - 1 (página que aún no hemos visto)) * 20 (registros por página) = 40 registros a descartar.

                    

Ocasionalmente nos es necesario obtener más datos que sólo el límite. Over-fetching (exceso de recuperación de datos) puede ser útil para ayudar a detectar cuando se tienen datos adicionales que mostrar después de la página actual, o si existe la necesidad de filtrar los resultados iniciales basados en los permisos. Puede hacerse esto con el tercer argumento:


$finder = \XF::finder('XF:User');$users = $finder->limitByPage(3, 20, 1);

                

Esto obtendrá hasta un total de 21 usuarios (20 + 1) a partir de la página 3.

Método getQuery

Cuando inicialmente se comienza a trabajar con el finder, como intuitivo que es, ocasionalmente se podría preguntar si se está haciendo correctamente y si se va a construir la consulta como se espera. Existe un método denominado getQuery que puede indicarnos cual es la consulta actual que se está generando en el objeto finder. Por ejemplo:


$finder = \XF::finder('XF:User') ->where('user_id', 1);
\XF::dumpSimple($finder->getQuery());

                

Esto obtendrá algo similar a:

string(67) "SELECT `xf_user`.* FROM `xf_user` WHERE (`xf_user`.`user_id` = 1)" 
                

Probablemente no se necesite esto cada vez, pero puede ser útil si el finder no devuelve los resultados esperados. Léase más sobre el método dumpSimple en la sección Volcar una variable.

Métodos personalizados del Finder

Hasta ahora se ha visto que el objeto finder obtiene la configuración con un argumento similar a XF:User y XF:Thread. La mayoría de ocasiones esto identifica la clase de Entidad con la que trabaja el finder y que resolverá, por ejemplo, XF\Entity\User. Sin embargo, puede representar adicionalmente una clase finder. Las clases Finder son opcionales y sirven de vía para agregar métodos finder personalizados a tipos específicos de finder. Para ver esto en acción, vamos a mirar la clase finder relativa a XF:User que se encuentra en la clase XF\Finder\User.

Éste es un ejemplo del método finder de esa clase:


public function isRecentlyActive($days = 180)
{
    $this->where('last_activity', '>', time() - ($days * 86400));
    return$this;
}

                

Lo que nos permite hacer esto es llamar ahora al método en cualquier objeto finder User. Si se quiere un ejemplo más sencillo:

$finder = \XF::finder('XF:User');
$users = $finder->isRecentlyActive(20)->order('message_count', 'DESC')->limit(10);

                

Ésta consulta, que nos devuelve 10 usuarios más rápidamente en orden descendente de número de mensajes, ahora devolverá 10 usuarios ordenados por la última actividad en los últimos 20 días.

Aunque para muchos tipos de entidad una clase finder no existe, sigue siendo posible extender estas clases no existentes de la misma manera que las mencionadas en la sección Extender clases.

El sistema Entidad

Si se está familiarizado con XF1, se estrá familiarizado con algunos conceptos sobre Entidades porque derivan, en último lugar, del sistema DataWriter. Caso de no estar familiarizado con XF1, la siguiente sección ofrecerá alguna idea.

Estructura de la Entidad

El objeto Structure consta de un número de propiedades que define la estructura de la Entidad y la tablas de la base de datos es relativa a ello. El objeto estructura en sí mismo se configura dentro de la entidad con la que se relaciona. Véamos algunas de las propiedades comunes de la Entidad User:

Tabla


$structure->table = 'xf_user';

                

Esto indica a la Entidad qué tabla de la base de datos debe usar cuando se actualiza e inserta registros y también llama al Finder con la tabla a leer desde donde de generan las consultas a ejecutar. Adicionalmente, juega un papel en saber que necesitan otras tablas para unirse a su consulta.

Nombre corto


$structure->shortName = 'XF:User';

                

Este es el nombre corto de la clase tanto de la Entidad en sí misma como de la clase Finder (si es aplicable).

Tipo de contenido


$structure->contentType = 'user';

                

Esto define qué tipo de contenido representa esta Entidad. Esto no suele ser necesario en la mayoría de las estructuras de Entidad. Se usa para conectar cosas específicas usadas por el sistema "tipo de contenido" (que se cubrirá en otra sección).

Clave primaria


$structure->primaryKey = 'user_id';

                

Define la columna que representa la clave primaria en la tabla de la base de datos. Si una tabla soporta que más de una columna sea clave primaria, puede definirse esto como una matríz.

Columnas


$structure->columns = [
    'user_id' => [
        'type' => self::UINT, 
        'autoIncrement' => true, 
        'nullable' => true, 
        'changeLog' => false
    ],
    'username' => [
        'type' => self::STR, 
        'maxLength' => 50, 
        'required' => 'please_enter_valid_name'
    ]  // y muchas más columnas ...
];

                

Esta es una parte clave de la configuración de la Entidad ya que ofrece cantidad de detalles para explicar las especificidades de cada columna de la base de datos de la que es responsable la Entidad. Esto indica el tipo de datos que se esperan, cuando se requiere un valor, qué formato debe coincidir, cuando debe haber un único valor, cual es el valor predeterminado y mucho más.

Basándose en el type, el gestor de entidad sabe si codificar o decodificar un valor de una manera determinada. Esto puede ser un proceso algo simple de convertir un valor a una cadena o a un entero, o algo más complicado como el uso de json_encode() en una matríz cuando se escribe en la base de datos o usando json_decode() en una cadena de JSON al leer de la base de datos por lo que se retorna correctamente el valor al objeto Entidad como una matríz sin precisar de hacerlo manualmente. También puede soportar valores separados por comas codificados/decodificados apropiadamente.

Ocasionalmente es necesario hacer alguna verificación o modificación adicional de un valor antes de escribirlo. Como ejemplo, en la Entidad User, se ve el método verifyStyleId(). Cuando se configura un valor en el campo style_id, se comprueba automáticamente si existe un método denominado verifyStyleId(), y si lo está, se ejecuta el valor en principio.

Comportamientos

$structure->behaviors = [ 'XF:ChangeLoggable' => [] ];

                

Esto es una matríz de clases de comportamientos que pueden usarse por esta Entidad. Las clases de comportamientos constituyen la vía para permitir que ciertos códigos se reutilicen genéricamente a través de varios tipos de entidad (sólo con cámbios de la Entidad, no con lecturas). Un buen ejemplo de esto es el comportamiento XF:Likeable que es capaz de ejecutar ciertas acciones automáticamente en entidades que soportan que en el contenido se pueda hacer 'Me Gusta'. Ésto incluye el recálculo automático de los contadores cuando ocurren cambios en la visibilidad y eliminan 'Me Gusta' cuando el contenido se elimina.

Getter


$structure->getters = [
    'is_super_admin' =>true,
    'last_activity' => true
];

                

Los métodos getters se llaman automáticamente cuando se llama a campos nominales. Por ejemplo, si se solicita el método is_super_admin desde una Entidad User, esto lo comprobará automáticamente y usa el método getIsSuperAdmin(). Cosa interesante sobre esto es advertir que la tabla xf_user no tiene en la actualidad un campo nominal is_super_admin. Esto existe en la actualidad en la Entidad Admin y se ha agregado un método getter como atajo para acceder al valor. Los métodos getters también pueden usarse para sobreescribir directamente valores de campos existentes, que es aquí el caso del valor last_activity. last_activity Es un valor en caché en la actualidad que usualmente se actualiza cuando un usuario cierra sesión. Sin embargo, se almacena la fecha de última actividad del usuario en la tabla xf_session_activity, por lo que se puede usar el método getLastActivity para devolver ese valor en lugar del valor en caché última actividad. Si alguna vez se tiene la necesidad de evitar el método getters por completo por acabar de obtener de la entidad el verdadero valor, hay que colocar un subrayado como sufijo del nombre de la columna, ej. $user->last_activity_.

Porque una Entidad es como cualquier otro objeto de PHP, pueden agregarseles más métodos. UIn caso de uso común de esto es agregar cosas como métodos de comprobación de permisos que pueden llamarse por la propia Entidad sobre sí misma.

Relaciones

$structure->relations = [
    'Admin' => [
        'entity' => 'XF:Admin', 
        'type' => self::TO_ONE, 
        'conditions' => 'user_id',
        'primary' => true
    ]
];

                

Aquí se definen las relaciones. ¿Qué son las relaciones? Se define como la relación entre entidades que se puede usar para realizar sobre la marcha consultas de unión a otras tablas o recuperar registros asociados a una entidad. Si se recuerda, en el método with del Finder se busca extraer un usuario específico y obtener de forma preventiva el registro de administrador del usuario (si es que existe) y sería algo así:


$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('Admin')->fetchOne();

                

Esto usará la información definida en la Entidad User para la relación Admin y los detalles de la estructura Entidad XF:Admin para conocer si esta consulta de usuario puede realizar una unión LEFT JOIN en la tabla xf_admin y en la columna user_id. Para acceder a la fecha del último acceso admin de la Entidad User:


$lastLogin = $user->Admin->last_login;  // devuelve la marca de fecha del último acceso admin

                

Sin embargo, no siempre es necesario hacer una unión en un Finder para obtener información relativa de una Entidad. Por ejemplo, si tomamos el ejemplo de arriba sin el método with pondrá:


$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->fetchOne();
$lastLogin = $user->Admin->last_login;  // devuelve la marca de fecha del último acceso admin

                

Se seguirá obteniendo aquí el último valor last_login. Se hace realizando una consulta adicional para obtener sobre la marcha la Entidad Admin.

El ejemplo de arriba usa el tipo TO_ONE y esta relación, por lo tanto, relaciona una Entidad con otra. También está el tipo TO_MANY.

Si no es posible extraer una relación TO_MANY entera (ej. con una unión en el método with en el Finder), lo es con el coste de una consulta extra a la base de datos si es posible leer en cualquier momento sobre la marcha, como last_login en el ejemplo final anterior.

Una de estas relaciones que se define en la entidad User es la relación ConnectedAccounts:


$structure->relations = [ 
    'ConnectedAccounts' => [ 
        'entity' => 'XF:UserConnectedAccount', 
        'type' => self::TO_MANY, 
        'conditions' => 'user_id', 
        'key' => 'provider'
    ]
];

                

Esta relación es capaz de devolver los registros desde la tabla xf_user_connected_account que coincidan con el actual ID de usuario como una FinderCollection. Esto es similar al objeto ArrayCollection mencionado en la sección de arriba El Finder. La definición de la relación especifica en qué colección debe ponerse la clave por el proveedor del campo.

A pesar de no ser posible extraer varios registros cuando el Finder ejecuta una consulta, es posible usar una relación TO_MANY para extrer un solo registro de esa relación. Como ejemplo, si se desea ver si un usuario está asociado con una cuenta de proveedor específica, al menos, se puede buscar eso mientras se consulta:


$finder = \XF::finder('XF:User');
$user = $finder->where('user_id', 1)->with('ConnectedAccounts|facebook')->fetchOne();

                

Opciones


$structure->options = [
    'custom_title_disallowed' => preg_split('/\r?\n/', $options->disallowedCustomTitles), 
    'admin_edit' => false, 
    'skip_email_confirm' => false
];

                

Las opciones de la Entidad son la vía para modificar el comportamiento de la Entidad bajo ciertas condiciones. Por ejemplo, si se configura admin_edit a cierto (que es el caso de cuando se edita a un usuario en el PC Admin), se omitirán ciertas comprobaciones como permitir que esté vacía la dirección de email del usuario.

Ciclo de vida de la Entidad

La Entidad juega un trabajo significativo en términos de gestión del ciclo de vida de un registro en el interior de la base de datos. Además de leer y escribir valores de ella, puede usarse la Entidad para eliminar registros y lanzar ciertos eventos cuando ocurren todas estas acciones y realizar ciertas tareas o actualizar, además, ciertos registros asociados. Estos son algunos de esos eventos que ocurren cuando la Entidad está guardando:

  • _preSave() - Este evento ocurre antes de iniciar el proceso de guardar y se usa principalmente para realizar cualquier validación o configuración de datos adicionales antes de guardar (pre-save).
  • _postSave() - Se llama a este evento después de haber guardado y antes de realizar cualquier transacción y se usa para realizar cualquier trabajo adicional que deba lanzarse tras el guardado por la Entidad.

Adicionalmente existen _preDelete() y _postDelete() que funcionan de similar manera, pero cuando sucede una eliminación.

La Entidad también es capaz de obtener información de su propio estado actual. Por ejemplo, existen los métodos isInsert() y isUpdate() con los que se puede detectar si se está insertando un nuevo registro o si se está actualizando un registro existente. Existe también el método isChanged() al que se puede llamar si cambia un campo específico desde el último guardado.

Éstos son algunos ejemplos reales de la acción de éstos métodos, en la Entidad User.


protectedfunction _preSave()

{
    if ($this->isChanged('user_group_id') || $this->isChanged('secondary_group_ids')) {

        $groupRepo = $this->getUserGroupRepo();

        $this->display_style_group_id = $groupRepo->getDisplayGroupIdForUser($this);

    }

    // ...

}

protectedfunction _postSave()

{
    // ...

    if ($this->isUpdate() && $this->isChanged('username') && $this->getExistingValue('username') != null)

        {

            $this->app()->jobManager()->enqueue(
                'XF:UserRenameCleanUp', [
                    'originalUserId' => $this->user_id, 
                    'originalUserName' => $this->getExistingValue('username'), 
                    'newUserName' => $this->username 

                ]

            );

        }

        // ...

}

                

En el ejemplo _preSave() se extrae y se pone en caché el nuevo ID de grupo a mostrar de usuario basado en los cambios habidos en sus grupos. En el ejemplo _postSave(), se lanza una trabajo para ejecutarse tras haber cambiado el nombre de usuario.

Repositorios

Los repositorios son un nuevo concepto en XF2 aunque no hay que sentirse culpable si se comparan con los objetos "Modelo" de XF1. No existen los objetos modelo de XF1 en XF2 porque existen mucho mejores sitios y vías de extracción y escritura de datos en la base de datos. Por ello, en lugar de tener una clase masiva que contenga todas las consultas que necesitan los complementos y todas las diferentes vías de manipulación de estas consultas, se tiene el Finder que agrega un montón de flexibilidad.

También vale la pena tener en cuenta que los objetos Modelo en XF1 eran un poco el "vertedero" de muchas cosas y muchos de los cuales ahora son redundantes. Por ejemplo, en XF1 todo el código de reconstrucción de los permisos está en el modelo de permisos. En XF2 se tienen servicios y objetos específicos que manejan esto.

Por tanto, ¿qué son los Repositorios? Se corresponden con una Entidad y un Finder y mantienen métodos que generalmente devuelven un objeto Finder para un propósito específico. ¿Por qué no solo devolver el resultado de una consulta Finder? Bien, si se devuelve en sí mismo el objeto Finder serviría como un punto muy útil de expansión para extender los complementos y modificar el objeto Finder antes de la devolución de la Entidad o colección.

Los repositorios contienen algunos métodos específicos para cosas como para la reconstrucción de cachés.