marzo
17

Supongamos que tenemos un sitio que originalmente no fue preparado para múltiples idiomas y el cliente ahora nos pide una versión en inglés. Para simplificar vamos a suponer que tenemos un único modelo llamado Product que tiene un campo name, el cual ahora va a tener que ser guardado en dos idiomas.

CakePHP ya viene preparado para una situación así, sólo basta con crear la tabla i18n, adjuntar el TranslateBehavior al modelo, con el siguiente código:

var $actsAs = array('Translate' => array('name'));

Y listo, todo todo estará funcionando.

Sin embargo tenemos un problema: si el modelo ya tenía datos, veremos que desde el momento en que el behavior se agrega, estos datos ya no se verán cuando se haga un find(). Esto sucede porque la consulta SQL que realiza el Translate Behavior hace un JOIN entre la tabla del modelo (en este caso products) con la tabla i18n, y como la tabla i18n está vacía porque recién la creamos, la consulta no devuelve resultados.

La solución sería inicializar la tabla i18n con los datos que ya tiene la tabla products, lo cual se podría hacer mediante programación, pero es mucho más fácil escribiendo código SQL puro.

La siguiente consulta sirve para copiar los datos del campo name de la tabla products a la tabla i18n de modo que quede funcional para el Translate Behavior. Suponemos que los contenidos originalmente estaban publicados en español.

insert into i18n (select null, 'spa', 'Product', Product.id, 'name', Product.name from products as Product)

Este procedimiento hay que repetirlo para cada campo de cada modelo que queremos traducir.

No es una solución muy elegante ni tampoco muy automática, pero resuelve el problema de manera bastante rápida.

noviembre
26

Tomemos como ejemplo un modelo Page que cumple con las siguientes condiciones:

  • Tiene asociado el TreeBehavior, es decir, cada Page del sistema podría ser hija de otra Page.
  • Tiene un campo created en donde se guarda la fecha en que la página fue creada.
  • Al momento de mostrar el árbol de páginas quiero que se ordenen por fecha de creación y, si hay dos páginas con la misma fecha de creación, que se ordenen por número de id.

Esto podría conseguirse mediante el siguiente código

$pages = $this->Page->find(
   'threaded',
   array('order' => array('Page.created ASC, Page.id ASC'))
);

Lo cual genera la siguiente consulta

SELECT `Page`.`id`, `Page`.`parent_id`, `Page`.`lft`, `Page`.`rght`, `Page`.`title`, `Page`.`text`, `Page`.`created` FROM `pages` AS `Page` WHERE 1 = 1 ORDER BY `Page`.`created` ASC, `Page`.`id` ASC

Hasta acá todo perfecto, pero si necesitamos generar un listado en forma de árbol para poder usar en un select generado por el Form helper, escribiríamos lo siguiente

$this->Page->generateTreeList();

Que genera la siguiente consulta

SELECT `Page`.`id`, `Page`.`title`, `Page`.`lft`, `Page`.`rght` FROM `pages` AS `Page` WHERE 1 = 1 ORDER BY `Page`.`lft` asc

Como podemos ver el problema es que generateTreeList siempre ordena a través del campo lft y no hay posibilidad de definir un ordenamiento diferente (puedes ver el código aquí)

Intento de solución

Ya que no podemos reordenar los datos al momento de hacer la consulta, intentaremos reordenarlos cuando guardamos las páginas. Para eso, el treeBehavior tiene un método llamado reorder al que podemos pasarle por parámetros cómo queremos que se reordene nuestro árbol. Cada vez que llamamos a reorder, los campos lft y rght del modelo se actualizan automáticamente, de modo que la próxima vez que hagamos un generateTreeList recibiremos los datos ordenados de la forma que queríamos.

El problema con esta solución es que reorder sólo nos permite reordenar a través de un único campo, con lo cual no nos sirve para nuestro ejemplo. Hemos abierto un ticket sugiriendo una pequeña modificación para que nos permita reordenar a través de consultas más complejas, puedes verlo aquí: http://cakephp.lighthouseapp.com/projects/42648/tickets/1263-complex-sort-on-tree-reorder

Solución final

Podemos crear un behavior propio que extienda al treeBehavior oficial y que sobreescriba el método reorder, con las modificaciones que se sugieren en el ticket. De este modo no necesitamos modificar el núcleo de CakePHP.

marzo
11

Uno de los problemas que tienen los UUID es que pueden resultar demasiado largos para algunas aplicaciones. Aquí veremos un método para reducir los clásicos UUID de 36 caracteres a 25.

Supongamos que a cada usuario registrado en nuestro sistema le asignamos un UUID, el cual tenemos que incluir en la url para visualizar su perfil. Esta URL quedaría algo más o menos así:
http://example.com/users/profile/4a3009bd-43f4-4535-ae3e-08a0650df841

Si observamos la composición de un UUID vemos que todos sus caracteres son números hexadecimales (excepto los guiones). Hay muchos caracteres que no están siendo utilizados en esta cadena y si convertimos los números hexadecimales a una base mayor que utilice más caracteres, podríamos reducir la cadena.

PHP tiene una función llamada base_convert que toma tres argumentos: el número a convertir, la base en la que se encuentra dicho número y la base a la cual lo queremos convertir. Si a esta función le pasamos nuestro UUID generado por CakePHP para convertirlo a base 36 (la máxima base que soporta dicha función) y lo pasamos a mayúsculas (sólo para que sea más prolijo), nos queda una cadena de 25 caracteres similar a esta: 4FPTDWER5VQ4G4S408C8O8WO0. La cadena generada sigue siendo única porque el número que representa es el mismo que el UUID original, sólo que está convertido a otra base mayor.

Ahora, para hacer un poco más sencillo el proceso, sería ideal que CakePHP genere automáticamente estos UUID cortos, del mismo modo que los hace con los largos. Si el campo id de una tabla es una cadena de 36 caracteres CHAR(36) o VARCHAR(36), CakePHP genera automáticamente un UUID por cada insert que se hace en la tabla. Con este behavior logramos el mismo comportamiento pero para aquellos campos id que sean cadenas de 25 caracteres. Veamos el código.

<?php

Class ShortUuidBehavior extends ModelBehavior {

	function setup(&$Model, $settings) {
	}

	function beforeSave(&$Model) {
		if(
			empty($Model->data[$Model->alias]['id'])
			and $Model->_schema['id']['length'] == 25
			and $Model->_schema['id']['key'] == 'primary'
			and $Model->_schema['id']['type'] == 'string'
		) {
			$Model->data[$Model->alias]['id'] = $this->_generateShortUUID();
		}
	}

	function _generateShortUUID() {
		$uuid = str_replace('-', '', String::uuid());
		return strtoupper(base_convert($uuid, 16, 36));
	}

}

?>

Este behavior se puede ubicar dentro de app_model.php, ya que sólo afectará a aquellos modelos cuyo id sea una cadena de 25 caracteres.