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');
    }
}
?>

Un comentario

Además de la ventaja obvia de poder ordenar por estos campos virtuales también hemos obtenido otra ventaja con respecto al método tradicional, y es que conseguimos liberar de carga a la base de datos ya que sólo se hace una petición en lugar de 1 + 2n siendo n el número de registros a mostrar.

A lo largo de las dos últimas semanas, por necesidades de un proyecto, me he visto obligado a trabajar con formularios en los que había bastantes campos de fecha.

Lo que pretendo hoy aquí es dar una serie de recetas rápidas que puedan solucionar esas preguntas “fáciles” que puedan surgir.

Cambiando el formato

Por defecto el formato es “año/mes/día”. En España el formato usual es “día/mes/año” así que para cambiarlo:

$format = '%day%/%month%/%year%';
 
$this->widgetSchema['date_field'] 
  = new sfWidgetFormDate(array('format' => $format));

Mostrando una fecha por defecto

Supongamos ahora que queremos una determinada fecha por defecto.

$default = array( 
                 'year'  => 2010, 
                 'month' => 2, 
                 'day'   => 12
              );
 
$this->widgetSchema['date_field'] 
  = new sfWidgetFormDate(array('default' => $default));

Mostrando hoy como la fecha por defecto

Si lo que queremos es que la fecha a mostrar por defecto sea el día actual

$today = array(
                'year'  => date('Y'),       
                'month' => date('n'),
                'day'   => date('j')
);
 
$this->widgetSchema['date_field'] = 
     new sfWidgetFormDate(array('default' => $today));

Eligiendo sólo unos determinados años

Supongamos que sólo nos interesa que muestre como posibles opciones de años, éste y los dos siguientes

$range  = range(date('Y'), date('Y')+2);
$years = array_combine($range,$range);
 
$this->widgetSchema['date_field'] = 
     new sfWidgetFormDate(array('years' => $years));

Validando que una fecha no es pasada

Si lo que nos interesa es que el usuario no pueda introducir una fecha pasada, por ejemplo para hacer una reserva.

$this->validatorSchema['date_field'] =
    new sfValidateDate(array('min' => date('Y-m-d'));

Validando que una fecha no es futura

O bien nos interesa que la fecha no pueda ser futura, porque se trata de que esté diciendo cuando tuvo lugar un hecho.

$this->validatorSchema['date_field'] =
    new sfValidateDate(array('max' => date('Y-m-d'));

Validando que una fecha es mayor que otra

Si tenemos dos campos de fecha, nos puede interesar que uno sea mayor que otro, por ejemplo la fecha de salida y llegada para una reserva de hotel.

      $this->validatorSchema->setPostValidator(
          new sfValidatorSchemaCompare('date_starts', '<', 'date_ends')
      );

No hay duda de que una de las mejores características de symfony es el Admin Generator, gracias a él podemos tener un panel de administración listo para usar en cuestión de minutos y además, podemos personalizarlo todo lo que queramos.

Hoy veremos como añadir nuevas acciones sobre objetos (object_actions) a la vista del listado y cómo personalizarlas desde el archivo generator.yml

Las object_actions por defecto
Por defecto, symfony incluye dos acciones para cada elemento del listado, eliminar y editar. Estas dos acciones son un tanto especiales en el sentido de que están ya están “personalizadas”, mostrando un icono distinto e incluso saltando javascript al hacer click (el caso del botón eliminar). En el archivo generator.yml:

generator:
  param:
    config:
      list:
        object_actions:
          _delete: ~
          _edit:   ~

Si no queremos mostrar una de las acciones en el listado bastará con borrarla.

Añadiendo una object_action
Como siempre es más sencillo hablar con ejemplos concretos, supongamos que el listado que estamos mostrado es de noticias y queremos añadir una acción que sea “publicar” de manera que al hacer click sobre ella se publique la noticia. Lo primero que habría que hacer sería añadir esta acción al listado, así:

generator:
  param:
    config:
      list:
        object_actions:
          _delete: ~
          _edit:   ~
          publish: ~

De esta forma, aparecerá un nuevo enlace a “modulo/:id/ListPublish” y por tanto hará falta definir la acción “ListPublish” dentro del archivo actions.class.php

// /module/moduleName/actions/actions.class.php
public function executeListPublish(sfWebRequest $request)
{
  $this->getRoute()->getObject()->publish();
  $this->redirect('@moduleName');
}

Con esto estaría el trabajo hecho, pero siempre puede quedar algo más “bonito”, ;-) .

Personalizando el texto
¿Qué ocurre si queremos que el texto que aparezca sea “Publicar Noticia” en lugar de “Publish”?, muy sencillo:

generator:
  param:
    config:
      list:
        object_actions:
          publish: { label: "Publicar Texto" }

Personalizando el HTML generado
¿Y si queremos añadirle una clase de CSS o un ID al anchor tag? Más fácil:

generator:
  param:
    config:
      list:
        object_actions:
          publish: 
            label: "Publicar Texto"
            params: { class: 'myClass',  id: 'publish-button' }

¿Y para que salte un javascript como en el caso del botón eliminar?

generator:
  param:
    config:
      list:
        object_actions:
          publish: 
            label: "Publicar Texto"
            params:
              class: 'myClass'
              id: 'publish-button'
              onclick: 'return confirm('¿Estás seguro?');'

Notar que para cambiar la imagen del icono debemos de hacerlo a través de la hoja de estilos admin.css modificando el atributo background de

#sf_admin_container ul li.sf_admin_action_publish a

Personalizando la acción que se ejecuta
Por último, si por cualquier motivo queremos que se ejecute la acción “publish” en lugar de “ListPublish” bastará con

generator:
  param:
    config:
      list:
        object_actions:
          publish: 
            action: 'publish'

I must recognize that I don´t really know how to introduce this post (and I am not really sure if the post title fits) so I just going to tell you my situation.

We have installed the sfDoctrineGuardPlugin plugin but we need to save some “extra” information for the users, for example, full name and email data. According to @jwage’s instructions we must create a new model objetc, Profile, having a 1:1 relation with sfGuardUser, like this:

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

Nothing remarkable up to now. But now, let´s suppose we want to access, let´s say full name. The code neccesary is:

  $user->Profile->full_name;

Easy, isn’t it? But deep down in our hearts we’d rather prefer just writing that:

  $user->full_name;

Can we achieve that? Let´s do it!

The Easy-peasy solution
There is a straight forward way to do it. You just need to define a new getter in the sfGuardUser class

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

but, what will happen if we want to get the email, ¿and the phone number?¿and the address?¿and …? According to this, we should define a new getter for every attribute in the Profile class. Fortunately, there is a better solution.

The “magic” solution
PHP has something really cool called magic methods. The solution below takes adventage of Doctrine_Record use of __get($fieldname) magic method.

// 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;
    }
  }
}

Although I am not going to explain in depth what´s going on under the hood, grosso modo, every time you ask the object for an attribute, first, it looks inside its own attributes and if it doesn´t find it then it will looks inside its Profile attributes. In case the attribute is not neither in sfGuardUser nor in Profile an expection will be thrown.

An interesting consecuence
In the list view in the admin generator you can use those attributes directly, like this:

  list:
    display: [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!

Como consecuencia de mi pequeño experimento de la semana pasada, aprendí alguna que otra cosa interesante sobre los logs de symfony.

En concreto, el núcleo de symfony viene con varios loggers interesantes, a saber:

  • sfNoLogger: Es el “anti-logger” ya que no hace nada. Es el utilizado por defecto en el entorno de producción.
  • sfVarLogger: Guarda los mensajes de log en una variable interna, llamada … logs! Se utiilza como clase padre para implementar sfWebDebugLogger
  • sfStreamLogger: Vuelca los mensajes de logs sobre un “stream” genérico. Se utiliza como clase padre para implementar sfConsoleLogger que utiliza como stream de salida php://stdout
  • sfFileLogger: Escribe los mensaje de log en un archivo que se da como parámetro a su constructor.

Además de estos, symfony añade otro más, sfAggregateLogger que es el utilizado por defecto en el entorno de desarrollo (dev). Este logger nos permite mostrar los logs de salida a través de tantos loggers como queramos simplemente añadiendo el nuevo log a lista, para ello, o bien utilizamos el método sfAggregateLogger::addLoggers($loggers) o bien los cargamos directamente mediante el archivo factories.yml:

#/app/config/factories.yml
 
logger:
  class: sfAggregateLogger
  param:
    loggers:
      sf_web_debug:
        class: sfWebDebugLogger
        param:
          level: debug
          condition:       %SF_WEB_DEBUG%
          xdebug_logging:  true
          web_debug_class: sfWebDebug
      f_twitty_logger:
        class: fTwittyLogger
        param:
          user: fTwittyLogger
          pass: symfony

En el ejemplo anterior estaríamos utilizando dos logger, sfWebDebugLogger y nuestro logger propio fTwittyLogger

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.