Una situación muy típica a la hora de desarrollar una aplicación web es ofrecer al usuario la posibilidad de subir una imagen y, de manera automática, generar un thumbnail de ésta.

Si estamos intentando implementar esta funcionalidad con symfony nos encontramos con la agradable sorpresa de que ya existe un plugin que nos facilita la tarea, sfThumbnailPlugin, pero sólo se encarga de generar el propio thumbnail, la pregunta que surge es: ¿dónde integrar la lógica para que se genere el thumbnail cada vez que se añade una imagen a un formulario?

La respuesta a la última pregunta me la dió David Vega en su post sfThumbnailPlugin y AdminGenerator: redimensionar imágenes el cual recomiendo que leáis.

Reconozco que la solución de David Vega me dejó con la boca abierta por lo bien que “encuadró” el código dentro del framework (en symfony uno no sabe muchas veces dónde poner el código), aunque en mi opinión no aprovecha bien la herencia de la clase padre, así que esta es mi versión de su sfResizedFile, con su permiso, ;) :

class sfResizedFile extends sfValidatedFile
{
	public function save($file = null, $fileMode = 0666, $create = true, $dirMode = 0777)
	{
		$file = parent::save($file, $fileMode, $create, $dirMode);
 
                $thumbFile = $this->path.DIRECTORY_SEPARATOR.'thumb_'.$file;
		$thumbnail = new sfThumbnail(100, 100, true, true, 85);
		$thumbnail->loadFile($this->getTempName());
		$thumbnail->save($thumbFile, 'image/jpeg');
 
                chmod($thumbFile, $fileMode);
 
                return $file;
	}
 
}

Consideremos que tenemos un modelo de datos en el que existen relaciones entre nuestras entidades, como siempre, un ejempo: dentro de nuestra aplicación tendremos noticias, perteneciendo cada noticia a un autor.

Ahora supongamos que generamos el panel de administración de esta aplicación mediante el admingenerator de symfony. Tal como está escrito el código del AdminGenerator nos encontraremos con que, cada vez que el usuario intente eliminar un autor al que se le haya asignado alguna noticia se producirá un error debido a una violación de la restricción de clave externa.

El usuario que utilice la aplicación no debe, ni tiene, porque saber esto, y es más que posible que intente borrar un autor que lo cumpla. Al ver la pantalla con un error 500 (a él no le aparecerá el error detallado que nosotros sí podemos ver en el entorno de desarrollo) nos llamará asustado y querrá una solución ya!

La mala solución

Una forma de proceder es mediante la famosísima solución conocida como muerto-el-perro-se-acabó-la-rabia. Si quito el botón de eliminar, el usuario no podrá borrar el registro, y si no puede borrar el registro no aparecerá el error. Fin del problema.

Bueno, no tan rápido. Qué ocurre si el usuario descubre que jugando con las URLs puede seguir borrando el registro. No pasa nada, basta con darle credenciales especiales a esa acción, por ejemplo en security.yml.

Bueno, vale, y si se ha equivocado al crear el autor y quiere borrarlo justo después de haberlo creado. En ese caso no habría problemas para eliminarlo ya que al no tener asociado ninguna noticia no surgiría tal error. Parece pues, que esta solución “hace aguas”.

La buena solución

Desde mi punto de vista la solución óptima pasa por permitirle borrar autores al usuario pero indicarle cuando no pueda. Para ello lo que hacemos es sobreescribir el método executeDelete de la acción. Quedaría algo como esto:

<?php
 
    // app/backend/modules/author/actions/actions.php
 
    public function executeDelete(sfWebRequest $request)
    {
        $request->checkCSRFProtection();
 
        $this->dispatcher->notify(new sfEvent($this, 'admin.delete_object', array('object' => $this->getRoute()->getObject())));
 
        try {
            $this->getRoute()->getObject()->delete();
            $this->getUser()->setFlash('notice', 'The item was deleted successfully.');
        }catch (Exception $e){
            $this->getUser()->setFlash('error', 'The item can not be deleted.');
        }
 
        $this->redirect('@author');
    }

Creo que debo una explicación

Creo que siempre he tenido un don (si es que se le puede llamar así) para poner nombres “rimbonbantes” a las cosas. El título de este post comienza con “… and if you just try”. Creo que el chiste (no comment) se entiende sólo si sabemos que el método executeDelete de la acción en baseAuthorActions es:

  // /cache/modules/author/actions/actions.class.php
 
  public function executeDelete(sfWebRequest $request)
  {
    $request->checkCSRFProtection();
 
    $this->dispatcher->notify(new sfEvent($this, 'admin.delete_object', array('object' => $this->getRoute()->getObject())));
 
    if ($this->getRoute()->getObject()->delete())
    {
      $this->getUser()->setFlash('notice', 'The item was deleted successfully.');
    }
 
    $this->redirect('@author');
  }

Para lo más lentos, obsérvese el cambio del “if” por el “try”. Es obvio que para entenderlo se requiere un mínimo de inglés, ;) .

La otra solución

Casi se me olvidaba! Aunque la solución expuesta aquí es ORM-independente, existe una solución para aquellos que utilizan Doctrine basada en la utilización del behaviour SoftDelete, aunque eso es ya otra historia.

Hay veces en que las reglas de validación de un formulario pueden cambiar según el valor de otros campos del mismo formulario. ¿Un ejemplo? El caso del formulario de reserva para un billete de tren, si es un viaje de ida y vuelta, la fecha de regreso será obligatoria y además deberá ser mayor que la fecha de ida, si es un viaje de ida ésta no será obligatoria.

La clase del formulario

Como siempre, trabajaremos con un ejemplo concreto, el del formulario de reserva de billetes (con una versión simplificada, claro). Para ello construimos el formulario teniendo en cuenta las reglas de validación menos restrictivas. Quedaría así:

<?php
 
class PurchaseForm extends BaseForm{
 
 
    public function configure()
    {
 
      $types = array(1 => 'ida', 2 => 'vuelta' );
      $this->setWidgets(array(
          'type'         => new sfWidgetFormChoice(array('choices' => $types),
          'go_date'      => new sfWidgetFormDate();
          'return_date'  => new sfWidgetFormDate();
      ));
 
      $this->setValidators(array(
        'type_id'       => new sfValidatorChoice(array('choices' => array_keys($types))),
        'go_date'     => new sfValidatorDate(array('min' => date('Y-m-d'))),
        'return_date'       => new sfValidatorDate(array('min' => date('Y-m-d', strtotime("tomorrow")), 'required' => false)),
      ));
 
    }
 
}
?>

Para el que no tenga ganas de pararse a leer el código, se han considerado tres campos: type (tipo de billete), go_date (fecha de ida), return_date (fecha de vuelta), con las siguientes reglas de validación:

  • type: puede valer sólo 1 ó 2 (es decir, es de ida o vuelta)
  • go_date: es una fecha obligatoria y además mayor que hoy, ya que no se puede comprar un billete de ayer
  • return_date: debe ser una fecha mayor que mañana (se ha supuesto que no se puede ir y volver en el día, aunque eso siempre es debatible)
  • Si quieres echarle un vistazo en mayor detalle a todas las posibilidades que ofrecen los widget y validators de symfony relacionados con las fechas puedes ver mi otro post, “Fechas, fechas y más fechas: Symfony, sfWidgetFormDate y sfValidatorDate“.

    Las reglas de validación con palabras

    Antes de programar entendamos un poco más qué queremos hacer. Lo que buscamos es que, en el momento en el que recibamos los valores de los campos, dependiendo del valor que tome el campo type, deberemos exigirle al campo return_date que sea obligatorio y además que sea mayor que el campo go_date

    La solución

    Quizás tan importante como la solución (que no es otra cosa sino añadir 10 líneas de código) sea entender qué me ha llevado a hacerlo así. En primer lugar, en el epígrafe anterior hemos visto que necesitamos conocer el valor del campo, esto hace que sea imposible imponer esta validación en el método configure ya que a estas alturas no se tiene conocimiento del valor de los campos. Si pensamos un poco, no es hasta que ejecutamos el método bind() del formulario en el que éste es consciente del valor de sus campos. De todas formas, no es buena idea sobreescribir este método ya que no está pensado para ello.

    En general, los desarrolladores de symfony implementan métodos del tipo doXXXX() que son los que les gustaría que sobreescribiésemos, para nuestra suerte sfForm implementa el método doBind().

    Ahora sí, la solución

    <?php
    class PurchaseForm extends BaseForm{
     
        // resto de código
     
        public function  doBind(array $values)
        {
            if($values['type']==2)
            {
                $this->validatorSchema['date_to']->setOption('required', true);
                $this->validatorSchema->setPostValidator(
                    new sfValidatorSchemaCompare('date_from', '<', 'date_to', array('throw_global_error' => true), array('invalid' => 'La fecha de comienzo debe ser menor que la de fin'))
                );
            }
     
            return parent::doBind($values);
        }

Si alguna vez os habéis parado a ver el código generado por el modelo, podréis observar que la clase Base es más o menos así:

<?php 
abstract class BaseItem extends sfDoctrineRecord
{
    public function setTableDefinition()
    {
       // Aquí el código de definición de la tabla
    }
 
    public function setUp()
    {
       // Aquí las relaciones y los behaviours
    }
}
?>

Algo que siempre me había llamado la atención de este código es, ¿por qué la clase base extiende de sfDoctrineRecord en lugar de hacerlo directamente de Doctrine_Record?

Esta pregunta no tiene una única respuesta, ya que son varias funcionalidades las que añade este wrapper:

  • Funcionalidades específicas para objetos I18n. Añade un proxy al objeto Translate de manera que si al acceder a un atributo del objeto no se encuentra, intentará buscarlo entre los de su objeto Translate asociado (para la cultura definida en ese momento) antes de lanzar una exception (algo parecido a esto)
  • Getter y Setter para las relaciones de objeto, de manera que podamos tratar “igual” tanto a las relaciones como los atributos de las tablas, una funcionalidad al más estilo Propel, :)
  • Hidratación específica para atributos Date y Timestamp. sfDoctrineRecord añade un método, sfDoctrineRecord::getDateTimeObject, que permite obtener los campos de fecha y timestamp no como una cadena de texto, sino como un objeto DateTime. De manera análoga, sfDoctrineRecord::setDateTimeObject() permite fijar el valor de un campo date o timestamp a partir de un campo DateTime en lugar de una cadena de texto correctamente formateada, algo muy útil en aplicaciones internacionalizadas donde las fechas se pueden insertar de diferentes maneras y requieren un trato previo en función de la cultura del usuario.

Pongamonos en situación, tenemos un listado generado por el AdminGenerator. Decidimos añadir una nueva columna a la lista, algo tremendamente fácil con sólo añadir un nuevo getter al objeto del modelo pero …. (siempre hay un pero) no podemos ordenar por ese nuevo campo, :( .

Bueno, esa situación era hasta hoy, ya que intentaré exponer lo tremendamente sencillo que es conseguir ordenar por una columna virtual.

En primer lugar, y para trabajar sobre un ejemplo “real” consideremos el archi-conocido caso en el que tenemos un modelo de datos formado por usuarios (sfGuardUser) y sus correspondientes perfiles (Profile) guardando una relación de 1:1 y teniendo la tabla Profile la siguiente estructura:

Profile:
  columns:
    full_name: { type: string(255) }
    email:     { type: string(255) }

Cómo era la situación hasta hoy

Consideremos el caso hipotético de que para el desarrollo de una aplicación tenemos que mostrar un listado con todos los usuarios con las siguientes columnas: nombre de usuario, nombre real y email. Hasta hoy, el proceso a seguir era,

1) creamos dos nuevos getter en el objeto sfGuardUser para las dos nuevas columnas virtuales:

// lib/model/sfGuardUser.class.php
<?
class sfGuardUser extends BasesfGuardUser{
 
  public function getFullName()
  {
    $this->Profile->full_name;
  }
 
  public function getEmail()
  {
    $this->Profile->email;
  }
?>

Te podías haber ahorrado definir estos getters si en lugar de esto hubieses creado directamente un proxy del objeto Profile, aunque eso es otra historia

2) Modificamos el archivo generator.yml para que muestre estos dos nuevas columnas virtuales:

      list:
        title:   Listado de Usuarios
        display: [=username, full_name, email]

3) El
Listado de Usuarios con columnas virtuales
pero no podríamos ordenar ni por full_name ni email … hasta hoy! Vamos allá!

Desvirtualizando los campos virtuales

Lo primero que debemos hacer es desvirtualizar los campos que eran virtuales, para ello cambiamos el método que utiliza el AdminGenerator para recoger el listado de elementos:

  list:
    title:   Listado de Usuarios
    display: [=username, full_name, email]
    table_method: doSelectAdmin

debiendo de crear este método en el objeto de la tabla correspondiente

// lib/model/sfGuardUserTable.class.php
<?php
class sfGuardUserTable extends Doctrine_Table{
 
  public static function doSelectAdmin()
  {
    return Doctrine_Query::create()
      ->select('u.username as username, p.full_name as full_name, p.email as email')
      ->from('sfGuardUser u')
      ->leftJoin('u.Profile p');
  }
}
?>

Notemos que el nombre de los campos de la sentencia select coincide con el nombre de los campos en list.display.

Después de esto tendríamos el mismo resultado que en el caso anterior, sin poder seguir ordenando por estas dos nuevas columnas.

Ya no es virtual, ahora es real

Para que symfony sustituya el texto de la cabecera de la tabla por un enlace debemos de indicarle de manera explícita que estos campos son reales y no virtuales. Esto se lo indicamos en el generator.yml:

#generator.yml
fields:
  full_name: { is_real: true }
  email:       { is_real: true }

Resulta curioso que este parámetro no venga en ningún sitio en la documentación de symfony (o al menos, yo no lo he sabido encontrar).

Con esto conseguiremos que el texto de la cabecera (i.e: “Email”, “Nombre”) sea ahora un enlace, aunque si haces click verás que no ocurre nada, esto se debe a que symfony pone una traba más que hay que sortear.

Sí, mis campos son reales y además son “ordenables”

Finalmente hay que indicarle a symfony que sí que queremos poder ordenar por esa columna, por lo que será necesario reescribir un método:

<?php
// /modules/sfGuardUser/actions/actions.class.php
 
class sfGuardUserActions extendes autosfGuardUserActions{
 
    protected function isValidSortColumn($column)
    {
      return in_array($column, array('username', 'full_name', 'email');
    }
}
?>

Puesto que no se muy bien como justificar una introducción a este post (de hecho tampoco sé si el título es el más adecuado), voy a poneros simplemente en situación.

Tenemos instalado el plugin sfDoctrineGuardPlugin y necesitamos guardar información “extra” sobre nuestros usuarios, por ejemplo, el email y el nombre. En este caso, y de acuerdo con las indicaciones de @jwage, crearíamos una nueva tabla Profile que guarda una relación 1:1 con sfGuardUser, tal que así:

Profile:
  columns:
    sf_guard_user_id: integer(4)
    full_name: string(255)
    email_address: string(255)
  relations:
    User:
      class: sfGuardUser
      local: sf_guard_user_id
      foreign: id
      foreignType: one

Hasta aquí, nada nuevo bajo el sol. Ahora, supongamos que queremos acceder al nombre de un usuario, el código para esto sería:

  $user->Profile->full_name;

Sencillo, ¿no? Pero, puesto que la relación es de 1:1, en el fondo de nuestro corazón nos gustaría que esto,

  $user->full_name;

bastase para obtener ese valor. ¿Cómo lo conseguimos?

Solución rápida y para toda la familia
La forma más rápida y obvia de conseguir lo anterior es también la más sencilla, basta con esto

// lib/model/doctrine/sfDoctrineGuardPlugin/sfGuardUser.class.php
 
  public function getFullName()
  {
    return $this->Profile->full_name;
  }

pero, ¿qué ocurre si también queremos recoger el email?¿y una dirección?¿y el teléfono?¿y …? De acuerdo a este planteamiento tendremos que crear un nuevo getter en el objeto sfGuardUser por cada atributo en el objeto Profile. Por suerte, existe una solución “mejor”.

La solución “mágica”
PHP dispone de algo muy interesante llamado métodos mágicos. La solución que se muestra más abajo está basada en la utilización que hace Doctrine, en su objeto Doctrine_Record, del método mágico __get($fieldName).

// lib/model/sfGuardUser.class.php
class sfGuardUser extends sfDoctrineRecord
{
  public function get($fieldName, $type = true)
  {
    try{
      parent::get($fieldName, $type = true);
    }catch(exception $e){
      $this->Profile->$fieldName;
    }
  }
}

Aunque no entraré a explicar en detalle que es todo lo que ocurre, grosso modo, cada vez que se pide un atributo al objeto sfGuardUser busca entre los suyos propios y en caso de que no lo encuentre, lo intentará buscar entre los del su Profile. En caso de que tampoco los encuentre, saltará una excepción.

Algunas ventajas
Aparte de las ventajas obvias de poder obtener el valor de los atributos del objeto Profile de manera transparente, también podemos utilizar en el admin generator para mostrar estos campos, por ejemplo, en el listado:

  list:
    display: [username, full_name, email]

Last week I read an interesting post about logging your messages into a Mongo database (I must admite that I didn’t know what a Mongo DB was until that day). This made me think about an experiment, and this is it.

The experiment
According to the post intro and the post title, you don’t need to be a genius to guess what the experiment is about. Yes, as you should probably notice, this is about developing a logger which update a twitter account with the symfony log messages. The twitter account is @fTwittyLogger (wich is the nickname I gave it).

sfLogger class
First off, some theory. Every logger you develop must inherit from the abstract class sfLogger. As clients of this code we are only concerned about implementing the sfLogger::doLog($message, $priority) method which takes two arguments, the message to log and the priority of this message, which can be one of the next constants: EMERG, ALERT, CRIT, ERR, WARNING, NOTICE, INFO and DEBUG.

The methods sfLogger:initialize() and sfLogger::shutdown() are executed at the begining and the end of the process, and may be useful to set some variables, close database connections, etc.

Our logger, fTwittyLogger

// lib/fTwittyLogger.class.php
<?php
 
class fTwittyLogger extends sfLogger
{
  protected
    $format     = '[%priority%] %message%';
 
  public function initialize(sfEventDispatcher $dispatcher, $options = array())
  {
 
    parent::initialize($dispatcher, $options);
 
    if (!$this->hasOption('user'))
    {
      throw new sfInitializationException(sprintf('You must provide the twitter username account'));
    }
 
    if (!$this->hasOption('pass'))
    {
      throw new sfInitializationException(sprintf('You must provide the twitter password account'));
    }
 
    $this->user = $this->getOption('user');
    $this->pass = $this->getOption('pass');
 
    return true;
  }
 
  protected function doLog($message, $priority)
  {
    $this->tweet(strtr($this->format, array(
      '%message%'  => $message,
      '%priority%' => $this->getPriority($priority))
    ));
 
  }
 
  protected function getPriority($priority)
  {
    return sfLogger::getPriorityName($priority);
  }
 
  public function hasOption($option)
  {
    return array_key_exists($option, $this->options);
  }
 
  private function tweet($message)
  {
    $context = stream_context_create(array(
      'http' => array(
      'method'  => 'POST',
      'header'  => sprintf("Authorization: Basic %s\r\n", base64_encode($this->user.':'.$this->pass)).
                   "Content-type: application/x-www-form-urlencoded\r\n",
      'content' => http_build_query(array('status' => $message)),
      'timeout' => 5,
      ),
    ));
 
    $ret = file_get_contents('http://twitter.com/statuses/update.xml', false, $context);
 
    return false !== $ret;
  }
}

From top to bottom:

  • initialize() check there is an username and password for the Twitter account
  • doLog() twits the message.tweet() method is a copy of this post

Using fTwittyLogger, modifying factories.yml
The last thing to do is to notify symfony that we are no longer using the default logger and we want to use our new one:

// app/config/factories.yml
prod:
  logger:
    class:   fTwittyLogger
    param:
      loggers: ~
      user: fTwittyLogger
      pass: symfony

NOTE: As you can see, the password is there. It's not a slip. I did it so you don't have to create "zombie" accounts just in case you want to give it a try.

... this is nonsense
Before someone attack me without mercy, I would like to say I am aware about how useless and unpractical this is. Twitter just let you write 140 characters and the data is public domain, but this post is about learning something new rather than something practical. Hope you enjoy it!

Hace poco leía un interesante artículo en el que explicaban como desarrollar un logger que guardase la información en una base de datos mongo (reconozco que no sabía qué era Mongo hasta que lo leí). Este post me “iluminó” para hacer un experimento simple y de paso aprender algo sobre los logs de symfony.

El experimento
Después de la mini-introducción y el título de este post no habrá que ser muy listo para darse cuenta de que el experimento consiste en conectar el logger de symfony con una cuenta de Twitter de manera que cada vez que hay un mensaje de log se haga un update en una cuenta de twitter, en este caso, esta cuenta es @fTwittyLogger, que es el nombre cariñoso que le he dado a la criatura.

La clase sfLogger
Antes de empezar, un poco de teoría. Cualquier logger en symfony deberá heredar de una otra manera de la clase abstracta sfLogger. Como clientes de esta clase nuestra única preocupación será implementar el método sfLogger::doLog($message, $priority) que recibe dos parámetros, el mensaje que queremos que aparezca en el log y la prioridad del mensaje que puede ser una de las siguientes constantes: EMERG, ALERT, CRIT, ERR, WARNING, NOTICE, INFO y DEBUG.

Además, existen dos métodos que también nos pueden resultar útiles, sfLogger:initialize() y sfLogger::shutdown(), que se ejecutan al principio y el fin del proceso.

Nuestra logger, fTwittyLogger

// lib/fTwittyLogger.class.php
<?php
 
class fTwittyLogger extends sfLogger
{
  protected
    $format     = '[%priority%] %message%';
 
  public function initialize(sfEventDispatcher $dispatcher, $options = array())
  {
 
    parent::initialize($dispatcher, $options);
 
    if (!$this->hasOption('user'))
    {
      throw new sfInitializationException(sprintf('You must provide the twitter username account'));
    }
 
    if (!$this->hasOption('pass'))
    {
      throw new sfInitializationException(sprintf('You must provide the twitter password account'));
    }
 
    $this->user = $this->getOption('user');
    $this->pass = $this->getOption('pass');
 
    return true;
  }
 
  protected function doLog($message, $priority)
  {
    $this->tweet(strtr($this->format, array(
      '%message%'  => $message,
      '%priority%' => $this->getPriority($priority))
    ));
 
  }
 
  protected function getPriority($priority)
  {
    return sfLogger::getPriorityName($priority);
  }
 
  public function hasOption($option)
  {
    return array_key_exists($option, $this->options);
  }
 
  private function tweet($message)
  {
    $context = stream_context_create(array(
      'http' => array(
      'method'  => 'POST',
      'header'  => sprintf("Authorization: Basic %s\r\n", base64_encode($this->user.':'.$this->pass)).
                   "Content-type: application/x-www-form-urlencoded\r\n",
      'content' => http_build_query(array('status' => $message)),
      'timeout' => 5,
      ),
    ));
 
    $ret = file_get_contents('http://twitter.com/statuses/update.xml', false, $context);
 
    return false !== $ret;
  }
}

De arriba a abajo:

  • El método initialize() asegurá que se han pasado el nombre de usuario y la contraseña de la cuenta de twitter
  • El método doLog() twittea el mensaje. El método tweet() es una copia de lo que se hizo en este post

Utilizando fTwittyLogger, modificando factories.yml
Lo último que quedaría sería informar a symfony de que ya no queremos utilizar su logger por defecto y en su lugar queremos utilizar nuestra nueva creación:

// app/config/factories.yml
prod:
  logger:
    class:   fTwittyLogger
    param:
      loggers: ~
      user: fTwittyLogger
      pass: symfony

NOTA: No ha sido un descuido dejar la contraseña, lo he puesto adrede por si alguien quiere hacer alguna prueba que no tenga que andar creando cuentas “zombie” en Twitter.

… pues menuda tontería
Antes de que alguien me ataque de forma despiada, soy consciente de que mostrar tus logs en una cuenta de twitter no tiene ninguna utilidad práctica. Twitter sólo te deja escribir 140 caracteres y además es público pero no se trata de eso sino de explorar y hacer cosas nuevas con el único fin de aprender algo nuevo.

Supongamos que tenemos un modelo de datos que queremos internacionalizar, por ejemplo, ciudades (para hacerlo más simple). El nombre de la ciudad variará en función del idioma, así, Londres para el español y London para el inglés.

El archivo schema.yml en este caso es tremendamente simple:

City:
  actAs:
    I18n: { fields: [name] }
  columns:
    name: { type: string(255) }

Algo que no he sido capaz de encontrar ni en la documentación de Doctrine ni en la de symfony es cómo construir el archivo fixtures.yml para cargar estos datos. La solución a continuación:

City:
  city_01:
    # aqui irían el resto de campos que no fuesen internacionalizados
    Translation:
      en: { name: "London" }
      es: { name: "Londres" }

Después del post de ayer, ya tenemos Apache, PHP y MySQL corriendo en nuestro PC, pero esto no es suficiente.

El principal problema con el que nos encontramos es que nuestras web deberían colgar de la carpeta /var/www/, en la cual no tenemos permisos de escritura y sólo es accesible para root.

Para solventar lo anterior sin tener que acceder como root, la solución sería que los archivos de las webs estuviesen en /home/miusuario/www/. Es más, supongamos que estoy desarrollando dos webs, a saber: mycoolapp y wtfapp (se me acaban de ocurrir los nombres, :-) ). Lo ideal sería que los archivos de cada una estuviesen en:

  • /home/miusuario/www/mycoolapp/
  • /home/miusuario/www/wtfapp/

y que para verlas en el navegador, las URL’s fuesen de la forma

  • http://mycoolapp.host
  • http://wtfapp.host

Pues bien, veamos como conseguimos esto.

Modificando el archivo hosts
El primer paso será informarle a nuestro PC que los dos dominios anteriores (mycoolapp.host y wtfapp.host) no necesitan ser resueltos en una tabla de DNS externas, sino que lo hará el mismo. Para esto editamos el archivo hosts

sudo gedit /etc/hosts

y añadimos las dos siguientes líneas

127.0.1.1   mycoolapp.host
127.0.1.1   wtfapp.host

Creando los Virtual Hosts
Para conseguir alojar más de un dominio en nuestro PC necesitamos configurar un virtual host por cada uno de ellos, ¿y cómo se hace esto? Muy sencillo.

Creamos un nuevo archivo llamado como el dominio que queremos configurar (en este caso wftapp.host) en la carpeta sites-availables de apache, así:

sudo gedit /etc/apache/sites-availables/wtfapp.host

Cuyo contenido será lo siguiente:

<VirtualHost *:80>
	ServerName wtfapp.host
	ServerAdmin webmaster@dominio.com
	DocumentRoot /home/miusuario/www/wtfapp/web
	DirectoryIndex index.php
 
	<Directory "/home/miusuario/www/wtfapp/web">
	  AllowOverride All
	  Allow from All
	</Directory>
</VirtualHost>

Nótese que tanto en DocumentRoot como en Directory se ha añadido “/web” ya que ésta es realmente la carpeta pública de la web.

Una vez hecho esto, habilitamos el nuevo virtual host

sudo a2ensite wtfapp.host

y reiniciamos apache para que los cambios surjan efecto

sudo /etc/init.d/apache2 restart

Después de esto, todo debería funcionar correctamente y deberíamos acceder a nuestra web de desarrollo en la URL http://wtfapp.host

Symfony y Desarrollo Web © Copyright 2009, All Rights Reserved.