Inspirado por un buen post de Asier Marqués: Desarrolo ágil con symfony, doctrine y mysql workbench en el que nos habla de como usar Mysql workbench y un plugin que nos permite exportar nuestra base de datos a formato YAML, he decidido contar mi corta experiencia con Doctrine migrations.
Hace poco comencé a usar esta opción que nos brinda a todos el ORM Doctrine. Tras dos o tres task lanzados, ya me enteraba de como funciona, es más, empecé a cogerle el gusto...
La idea parte teniendo un schema.yml inicial, una BBDD en el servidor y la necesidad de incorporar cambios en ambos. El uso es mucho más extenso, pero me voy a centrar en los siguiente:
Teniendo el schema.yml
# config/doctrine/schema.yml
Category:
columns:
id:
type: integer(11)
notnull: true
autoincrement: true
primary: true
unsigned: true
name:
type: string(255)
Y la BBDD en el servidor exactamente tal y como se declara en el schema.
Se necesita aplicar un cambio en la BBDD (añadir/eliminar columna, modificar una relación, etc.).
# config/doctrine/schema.yml
Category:
columns:
id:
type: integer(11)
notnull: true
autoincrement: true
primary: true
unsigned: true
name:
type: string(255)
Item:
columns:
id:
type: integer(11)
notnull: true
autoincrement: true
primary: true
unsigned: true
category_id:
type: integer(11)
notnull: true
unsigned: true
name:
type: string(255)
relations:
Category:
local: category_id
foreign: id
type: one
foreignAlias: Items
Cómo hacemos esto?
1. Modificar el schema.yml para aplicar el cambio.
2. Lanzar el task $ php symfony doctrine:generate-migrations-diff. Esto crea una estructura de ficheros en el directorio lib/ del proyecto:
// lib/migration/doctrine/1283537548_version1.php
class Version1 extends Doctrine_Migration_Base
{
public function up()
{
$this->createTable('item', array(
'id' =>
array(
'type' => 'integer',
'autoincrement' => '1',
'primary' => '1',
'unsigned' => '1',
'length' => '11',
),
'category_id' =>
array(
'type' => 'integer',
'notnull' => '1',
'unsigned' => '1',
'length' => '11',
),
'name' =>
array(
'type' => 'string',
'length' => '255',
),
), array(
'primary' =>
array(
0 => 'id',
),
));
}
public function down()
{
$this->dropTable('item');
}
}
// lib/migration/doctrine/1283537549_version2.phpclass Version2 extends Doctrine_Migration_Base
{
public function up()
{
$this->createForeignKey('item', 'item_category_id_category_id', array(
'name' => 'item_category_id_category_id',
'local' => 'category_id',
'foreign' => 'id',
'foreignTable' => 'category',
));
$this->addIndex('item', 'item_category_id', array(
'fields' =>
array(
0 => 'category_id',
),
));
}
public function down()
{
$this->dropForeignKey('item', 'item_category_id_category_id');
$this->removeIndex('item', 'item_category_id', array(
'fields' =>
array(
0 => 'category_id',
),
));
}
}
3. Lanzar el task $ php symfony doctrine:migrate. Aplica en la BBDD los cambios que se crearon en el paso anterior. Se le pueden pasar parámetros (entorno, up / down, etc.)
4. Por último se reconstruyen las clases del modelo: $ php symfony doctrine:build --all-classes
5. No nos olvidemos de borrar la cache..
Con esto, ya tenemos los cambios que requerimos, aplicados tanto en nuestro proyecto Symfony como en nuestra BBDD. Fácil y rápido.
Enlaces:
Proyecto de prueba creado para el post
Necesitaba crear un módulo que transformase imágenes al vuelo según parámetros pasados por la URL y almacenara una cache de éstas (módulo PIC para imágenes).
Cuando me disponía a empezarlo, justo vi sfImageTransformExtraPlugin para Symfony. Que hace todo lo que necesitaba y más.
Después de instalarlo y pelearme inicialmente con el routing para conseguir llamar al módulo de forma correcta el resultado es espléndido: rápido, gestión máginifica de la cache, fácil de usar una vez conocido, fexible, etc.
Plugin sfImageTransformExtraPlugin.
Tutorial parte 1.
Tutorial parte 2.
Cuando tenemos un módulo creado a través del "Admin Generator" se crean varias acciones automáticamente como son: edit, new, delete, list... A veces, se necesitan crear acciones diferentes a éstas y que son más propias del modelo de la aplicación como por ejemplo: "mostrar en portada", "Artículo fuera de Stock".
Para ello normalmente existen dos vías diferentes: desde el generator.yml o sobreescribiendo el partial correspondiente a las acciones del form o list.
Este sería un ejemplo para una acción que pondrá un artículo en la portada de la web:
Desde el generator.yml:
generator:
class: sfDoctrineGenerator
param:
model_class: Articulos
theme: admin
non_verbose_templates: true
with_show: false
singular: ~
plural: ~
route_prefix: articulos
with_doctrine_route: true
actions_base_class: sfActions
config:
actions: ~
list: ~
filter: ~
form:
actions:
portada:
action: 'inHome'
label: 'En portada'
confirm: 'Este artículo pasará a la portada de la web. Continuar?'
_delete: ~
_list: ~
_save_: ~
edit: ~
new: ~
Sobreescribiendo _form_actions.php
<ul class="sf_admin_actions">
<?php if ($form->isNew()): ?>
<?php // Si es un objeto nuevo no se muestra el botón ?>
<?php // Aquí irían las acciones propias del Form: edit, save... ?>
<?php else: ?>
<?php if (!$articulos->getInHome()): ?>
<li class="sf_admin_action_portada">
<?php echo link_to(__('En portada', array(), 'messages'), 'articulos/inHome?id='.$articulos->getId(), array( 'conditional' => 'portada', 'conditional_value' => false,)) ?>
</li>
<?php endif; ?>
<?php // Aquí irían las acciones propias del Form: edit, save... ?>
<?php endif; ?>
</ul>
Esta última opción da más control al programador y se pueden tener en cuenta condicionales como por ejemplo que el artículo tenga el campo home=true, en cuyo caso ya no se necesita que aparezca el botón.
Pero las opciones no se acaban aquí, existe otro método de crear nuevas acciones un poco más complejas sin necesidad de sobreescribir el elemento Partial. Cómo? Haciendo uso del Helper propio del módulo creado por el Admin Generator (Articulos en este ejemplo). Este archivo se sitúa en el directorio lib del módulo: apps/backend/modules/##moduleName/lib/##moduleNameGeneratorHelper.class.php y extiende de Base##moduleNameGeneratorHelper que ha su vez extiende de la clase sfModelGeneratorHelper. Si miramos esta clase vemos que contiene los helpers linkToNew, linkToEdit, linkToList, linkToSave y linkToSaveAndAdd que se corresponden con las acciones base y que devuelven el tag li con una llamada al helper link_to configurado con los parámetros de la acción.
Si se mira el Partial de las acciones, primero se comprueba que exista el método ##actionName dentro del helper, si existe se muestra su valor, si no existe se crea la acción parametrizada según el fichero generator.yml
cache/backend/#env/modules/autoArticulos/templates/_form_actions.php
<?php
// ...
if (method_exists($helper, 'linkToPortada')): ?>
<?php echo $helper->linkToPortada($form->getObject(), array( 'action' => 'inHome', 'label' => 'En portada', 'confirm' => 'Este artículo pasará a la portada de la web. Continuar?', 'params' => array( 'conditional' => 'portada', 'conditional_value' => false, ), 'class_suffix' => 'portada',)) ?>
<?php else: ?>
<?php echo link_to(__('En portada', array(), 'messages'), 'articulos/inHome?id='.$articulos->getId(), array( 'conditional' => 'portada', 'conditional_value' => false,)) ?>
<?php endif;
// ...
?>
Entonces, una forma de personalizar acciones es usando el generator.yml y el helper del módulo para pasárle parámetros, condicionales, etc. Siguiendo con el ejemplo:
generator.yml
generator:
class: sfDoctrineGenerator
param:
config:
form:
actions:
novedad:
action: 'inNew'
label: 'Novedad'
confirm: 'Este artículo será marcado como novedad. Continuar?'
params:
conditional: 'novedad'
conditional_value: false
El parámetro 'novedad' se usará para saber qué propiedad del objeto hay que chequear y el parámetro conditional_value para saber qué valor ha de tomar el objeto para que se muestre esta acción.
articulosGeneratorHelper.class.php
<?php
class articulosGeneratorHelper extends BaseArticulosGeneratorHelper
{
public function linkToPortada($object, $params)
{
$conditional = (isset($params['params']['conditional'])) ? 'get'.ucfirst($params['params']['conditional']) : null;
$conditional_value = (isset($params['params']['conditional_value'])) ? $params['params']['conditional_value'] : true;
if ( ($conditional == null) || ($conditional != null && $object->$conditional() == $conditional_value) ) {
return link_to(__($params['label'], array(), 'sf_admin'), 'articulos/' . $params['action'] . '?id=' . $object, array('confirm' => !empty($params['confirm']) ? __($params['confirm'], array(), 'sf_admin') : $params['confirm']));
} else {
return '';
}
}
}
Tengo que decir que no he visto nada en la documentación oficial de Symfony al respecto, por lo que no se hasta qué punto es una forma correcta de hacer uso de ésto, sobre todo con el paso de parámetros no estándares (conditional y conditional_value) desde el generator.yml al helper del módulo.
Navegando por la web de Symfony en busca de un plugin que implementase las operaciones de un "Carrito de la compra" y que fuese compatible con la versión 1.4 y el ORM Doctrine, no encontré nada interesante. Sin embargo, vi el plugin sfShoppingCartPlugin creado por Fabien Potencier para la versión 1.0 de Symfony y Propel.
Tras echarle un ojo, vi que implementa basicamente lo que necesito para mi carro de la compra y que solo requería de algunas modificaciones para hacerlo compatible con Doctrine, por lo que decidí hacer uso de él.
El plugin en si consta solamente con dos clases:
Básicamente, el uso es el siguiente:
<?php
// Se crea el objeto de la cesta
$carrito = new sfShoppingCart(sfConfig::get('app_tax'));
// Se añade un nuevo Articulo al carrito
$article = ArticlesTable::getInstance()->find($request->getParameter('id'));
$articleItem = new sfShoppingCartItem(get_class($article), $article->getId());
$articleItem->setPrice($article->getPrice());
$articleItem->setQuantity($request->getParameter('quantity'));
$carrito->addItem($articleItem);
// Recorrer el carrito y ver el precio total
foreach ($carrito->getItems() as $item) {
echo sprintf(
'%d: %f x %d = %f',
$item->getId(),
$item->getPrice(),
$item->getQuantity(),
($item->getPrice() * $item->getQuantity())
);
}
echo 'Total: ' . $carrito->getTotal();
Además de estas operaciones la clase sfShoppingCart incluye dos operaciones que devuelven el objeto Item del modelo:
sfShoppingCart::getObjects()
sfShoppingCart::getObject($class, $id)
Estos métodos hacen uso de Propel para obtener el objeto de la clase $class y con id $id.
Como lo que necesito es hacer uso de Doctrine, nada mejor que extender sfShoppingCart a sfDoctrineShoppingCart y sobreescribir ambos métodos:
public function getObject($class, $id)
{
// We must first make sure that the requested object does exist in the shopping cart
$ind = $this->getItemIndice($class, $id);
return (($ind !== null) ? call_user_func(array($class.'Table', 'find'), $id) : null);
}
public function getObjects()
{
$object_ids = array();
foreach ($this->getItems() as $item)
{
if (!array_key_exists($item->getClass(), $object_ids))
{
$object_ids[$item->getClass()] = array();
}
$object_ids[$item->getClass()][] = $item->getId();
}
$objects = array();
foreach ($object_ids as $class => $ids)
{
$objects = array_merge(
$objects,
Doctrine::getTable($class)->createQuery('c')->whereIn('c.id', $ids)->execute()->getData()
);
}
return $objects;
}
Con ésto ya se puede hacer uso de la clase sfDoctrineShoppingCart con todas las funcionalidades que ofrece sfShoppingCart pero adaptado a Doctrine.
Además he implementado algunos métodos extra para hacerlo más cómodo al uso. Por ejemplo sfDoctrineShoppingCart::getInstance() o sfDoctrineShoppingCart::save() para obtener y almacenar el carrito
Me acabo de crear una cuenta twitter con fin de seguir las ponencias de Symfony organizadas por decharlas en Castellón. No me manejo muy bien aun en esto, pero parece que me adapto rápidamente.
Voy a experimentar durante un tiempo a ver qué ocurre...
Pueden seguirme en Alberto Ramirez on Twitter
Bueno, llevo unos 3 meses trabajando con la versión 1.4 de Symfony y el ORM Doctrine. Anteriormente estuve un par de años trabajando con la version 1.0 y Propel.
El cambio a la hora de codificar es significativo, no puedo comentar nada nuevo que no se haya comentado ya al respecto de las novedades de estas versiones sobre la 1.0.
En mi caso, la integración con el subframework de formularios aun continua su proceso, me manejo mucho mejor que al principio, pero aun hay muchos aspectos que desconozco.
El sistema de enrutamiento lo veo muy cómodo, ya que permite establecer en el routing.yml los parámetros del objeto que vas a pasar por URL de modo que a la hora de llamar al helper link_to, solo con pasarle el objeto completo, éste ya se encarga de formatear la URL. Además con los comportamientos (behaviors) de Doctrine, también se pueden definir Slug de forma muy sencilla.
Ejemplo:
routing.yml
watch_show:
url: /catalogo/:brand_url/:slug
class: sfDoctrineRoute
options: { model: Watch, type: object }
param: { module: watch, action: show }
requirements:
id: \d+
sf_method: [get]
showSuccess.php
url_for('watch_show', $principalWatch)
lib/model/doctrine/Watch.class.php
public function getBrandUrl()
{
return $this->getBrand()->getSlug();
}
schema.yml
Watch:
tableName: watch
actAs:
Timestampable: ~
Sluggable:
unique: true
fields: [model_name]
canUpdate: true
apps/frontend/modules/watch/actions/actions.class.php
public function executeShow(sfWebRequest $request)
{
// 'watch_show'
$this->watch = $this->getRoute()->getObject();
}
Otro de los cambios importantes es el paso a Doctrine como ORM en defecto de Propel. Lo noto muy flexible, fácil de aprender pero por completo diferente a Propel. Los retrieves se hacen en base a la clase Doctrine_Query y vas formando query de una forma mucho más gráfica y no tan complejas como con Criteria de Propel.
Ejemplo de Query:
$q = Doctrine::getTable('Articulos')->createQuery('a')
->leftJoin('a.Familias f')
->leftJoin('a.Subfamilias sf')
->innerJoin('a.fotos ft')
->where('a.pagarias = ?',false)
->andWhere('a.agotado = ?', false)
->andWhere('a.familia = f.id')
->orderBy('a.portada DESC')
->limit(23)
;
Doctrine cuenta con una tabla que modela el objeto en si "Articulos" (en este ejemplo) y con una clase destinada a las operaciones de consulta a la tabla "ArticulosTable" (siguiendo con el ejemplo).
Existen muchas más cosas a tener en cuenta. Por lo que es recomendable leer las novedades de cada versión con el fin de tener en mente las nuevas funcionalidades que aporta cada versión de Symfony.
Yo acabo de pedir el libro Más con Symfony que está destinado a los programadores que conocen bien el entorno y quieren ser aun más productivos con Symfony.
Un complemento para Firefox que he usado recientemente y me ha facilitado la vida para debugear peticiones AJAX es FirePHP. FirePHP es un complemento para Firebug y permite hacer debug PHP en la consola de Firebug.
¿Cómo usarlo?
Hay que usar Firefox y tener instalado el complemento Firebug (que seguro que lo tienes ya), luego hay que instalar FirePHP y descargar e instalar la librería PHP.
Una vez obtenido el complemento e instalada la librería se puede usar de esta forma:
require_once('FirePHPCore/FirePHP.class.php');
$firephp = FirePHP::getInstance(true);
$firephp->log('Hello World');
Se pueden loguear variables, costantes, arrays, objetos, etc.
Para Symfony
Si se está usando Symfony como framework para el desarrollo se puede instalar el plugin sfFirePHP e integrarlo en su funcionamiento (sistema de log, barra de depuración web, etc).
Enlaces:
PHP nos brinda la posibilidad de almacenar el contenido de la salida estándar en un buffer y poder operar con él. Una de las posibilidades que tenemos con este buffer es la de simular un efecto de carga e ir mostrando contenido de la web secuencialmente (Simulando un efecto AJAX).
Este sería un pequeño script PHP de ejemplo.
ob_start();
for($x=0; $x<10; $x++){
echo printf('- Línea %d.'."\n", $x);
ob_flush();
flush();
sleep(1);
}
ob_end_flush();
Aunque es muy sencillo, lo que realiza es lo siguiente:
Cuando se comienza con un proyecto nuevo hay programadores que se ponen directamente a picar código como locos. Esto es una práctica totalmente desaconsejable para crea software y más aun para crear sistemas complejos. Las razones de ello son múltiples:
Se podrían seguir enumerando desventajas, pero no voy a seguir aquí eternamente. ¿Qué se debe hacer pues, antes de ponerse a programar una nueva aplicación?
Para solucionar estos problemas y crear una aplicación sólida, escalable y de fácil mantenimiento hay que empezar por unos cimientos sólidos, y para este propósito nada mejor que guiarnos por algún ciclo de vida del software conocido.
Las etapas varían dentro de cada ciclo de vida, pero yo me centraré en comentar las siguientes:
Este es un post perteneciente a la serie "Patrones de Diseño".
Normalmente para crear un objeto de una clase se utiliza el operador "new" que nos brinda el propio lenguaje de programación, pero a veces, la creación de un nuevo objeto requiere de una serie de pasos, configuraciones e incluso puede ser necesario crear objetos de alguna subclase. Este problema se ve incrementado si el número de objetos (de la misma clase) a crear, es elevado. El problema entonces: ¿Cómo crear objetos complejos de forma fácil, rápida y sin posibilidad de errores?
La solución a este problema puede ser la siguiente: crear un método de clase (o una función) que se encargue de crear la instancia, configurar los nuevos objetos e incluso de crear objetos dependientes de otras clases. De este modo la llamada será simple, independientemente de la complejidad que tenga esa clase en si para ser instanciada.
Esta solución es la que nos brinda el patrón "Factory", y se puede ver más fácilmente con los siguientes ejemplos: