Mirmo Dynamics

Si tu kiffes pas reunoi, t'écoutes pas et puis c'est tout.

To content | To menu | To search

Keyword - apache

Entries feed - Comments feed

Tuesday 17 March 2009

Rewrite rule to add missing www

  RewriteEngine on
  RewriteCond %{SERVER_NAME} !^www\..*$
  RewriteRule ^/(.*)$ http://www.%{SERVER_NAME}/$1 [L,QSA,R=302]

From the manual, using %1 as a back reference from the RewriteCond in the RewriteRule should work, but it didn't on my installation. Weird.

Monday 29 December 2008

Multiple domains for one symfony project, the basics

So let's say you run a car franchise, and you have several shops that you want to be able to manage through a web application. The application would be same for each shop, except you'd have to manage different sets of data and have specific assets for each shop. Fear not, this is very easy to achieve using symfony (well, it's quite easy with any framework I guess, but we will be using symfony :p)

Sample project

At the beginning, there were generate:project:

$ mkdir carshop
$ cd carshop
$ symfony generate:project carshop
$ symfony generate:app frontend
$ symfony generate:module frontend default

We will also need a simple database schema for the sake of examples. We will be using Doctrine, so let's fill in our config/doctrine/schema.yml with a very simple schema:

detect_relations: true
 
Car:
  columns:
    site_id:      { type: integer }
    name:         { type: string(255) }
    description:  { type: clob }
    image:        { type: string(255) }
 
Site:
  columns:
    name:         { type: string(255) }
    domain:       { type: string(255), unique: true }
    main:           { type: boolean, default: false }

Also, let's not forget to enable Doctrine in our project in config/ProjectConfiguration.class.php, replace the line:

$this->enableAllPluginsExcept('sfDoctrinePlugin');

with:

$this->enableAllPluginsExcept('sfPropelPlugin');

and to configure the database:

$ symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=carshop" root

Oh, we shall need some fixtures too, let's put them in data/fixtures/01_sites.yml:

Site:
  paris:
    name: Paris
    domain: paris.carshop
  auckland:
    name: Auckland
    domain: auckland.carshop

and data/fixtures/02_cars.yml:

Car:
  car_1:
    Site: paris
    name: Peugeot 307
    description: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  car_2:
    Site: paris
    name: Renault Laguna
    description: Maecenas tortor nunc, aliquam et, ultrices id, ornare consectetur, mauris.
  car_3:
    Site: auckland
    name: Subaru Impreza
    description: Ut accumsan diam et orci. Sed sit amet neque ac diam rutrum iaculis.

And now that everything is ready, let's create the tables and load the fixtures:

$ symfony doctrine:build-all-load

Vhosts

The second thing we will need is a set of vhost with differents domains all pointing to our newly generated project. It's quite easy to do, and I will assume you run some kind of linux or unix here (although it's similar under windows if I remember well). Let's start with creating fake domain names, you just have to add the following line to your /etc/hosts:

127.0.0.1 auckland.carshop paris.carshop

And the following to your apache vhost:

ServerName paris.carshop
ServerAlias *.carshop

Save and quit, reload apache, and you're all set.

Detecting the current site

First thing we need to know if we want to handle site-specific data, is which site we're currently in. This can easily be done in the project configuration. Since we will need database access, we will hook up on the context.load_factories event. So, on with coding, open your config/ProjectConfiguration.class.php and add the following line to the setup() method:

$this->dispatcher->connect('context.load_factories', array($this, 'detectSite'));

This will hook the detectSite() method to the event context.load_factories. We now need to add the detectSite() method to the ProjectConfiguration class:

/**
 * Detects the current site based on the url
 * Fallback to the main site (main = 1 in database) if we can't find a suitable entry
 *
 * @param sfEvent $event
 */
 
public function detectSite(sfEvent $event)
{
  $request = sfContext::getInstance()->getRequest();
  $domain = $request->getHost();
 
  $siteTable = Doctrine::getTable('Site');
 
  if (false !== $site = $siteTable->retrieveByDomain($domain))
  {
    $siteTable->setCurrent($site);
  }
  else
  {
    $siteTable->setCurrent($siteTable->retrieveMain());
  }
}

Nothing very hard here. We get the HTTP host from the request, then try to fetch a corresponding Site from the database, fallbacking to the main site if necessary. You may have noticed that we added three methods to the Site model class: retrieveByDomain(), retrieveMain() and setCurrent(). Your lib/model/doctrine/SiteTable.class.php should look like this now:

<?php
/**
 * This class has been auto-generated by the Doctrine ORM Framework
 */
class SiteTable extends Doctrine_Table
{
 
  /**
   * Holds the current site
   * @var Site
   */
 
  protected $current;
 
  /**
   * Retrieve a Site by its domain
   *
   * @param string $domain
   * @return Site or false if no site is found
   */
 
  public function retrieveByDomain($domain)
  {
    return $this->createQuery()->where('domain = ?', $domain)->fetchOne();
  }
 
  /**
   * Retrieve the main site
   *
   * @return Site or false if no site is found
   */
 
  public function retrieveMain()
  {
    return $this->createQuery()->where('main = 1')->fetchOne();
  }
  
 
  /**
   * Sets the current site
   *
   * @param Site $site
   */
 
  public function setCurrent(Site $site)
  {
    $this->current = $site;
  }
 
}

While we're there, let's add a getCurrent() method as well:

/**
 * Gets the current site
 *
 * @return Site
 */
 
public function getCurrent()
{
  return $this->current;
}

Handling domain-specific logic and data

Ok, so now that we know where we are, let's make the Car model aware of the current site. Open lib/model/doctrine/CarTable.class.php and add the createQuery() method:

/**
 * Creates a query, adding the site criteria automatically
 *
 * @return Doctrine_Query
 * @see Doctrine_Table::createQuery()
 */
 
public function createQuery($alias = '')
{
  $query = parent::createQuery($alias);
  $query->where('site_id = ?', Doctrine::getTable('Site')->getCurrent()->getId());
  
  return $query;
}

This is the method used internally by Doctrine_Table to create queries related to the Car model, so now, all find methods fired from the Car model will only fetch cars from the current site. We need to take care of this at save() time too, in lib/model/doctrine/Car.class.php:

/**
 * Automatically populates the site_id field if necessary
 * 
 * @see sfDoctrineRecord::save()
 */
 
public function save(Doctrine_Connection $conn = null)
{
  if (empty($this->site_id))
  {
    $this->setSite(Doctrine::getTable('Site')->getCurrent());
  }
 
  return parent::save();
}

Handling domain specific assets

On with the assets. We are going to use our good friend mod_rewrite to handle this. First, let's create a specific directory for each domain we're going to manage:

$ mkdir -p web/perhost/{paris,auckland}.carshop/{css,images,js,uploads}

Also, for testing purpose, let's create some fake js file:

$ echo 'paris.carshop' > web/perhost/paris.carshop/js/foo.js
$ echo 'auckland.carshop' > web/perhost/auckland.carshop/js/foo.js
$ echo 'fallback' > web/js/fallback.js

Then add some voodoo magic in your web/.htaccess, before the we skip all files with .something rules generated by symfony:

RewriteCond /home/ash/projects/carshop/trunk/web/perhost/%{HTTP_HOST}%{REQUEST_FILENAME} -f
RewriteRule (.*) /perhost/%{HTTP_HOST}/$1 [L]

Please note that %{REQUEST_FILENAME} includes the starting /, so that we don't need to add one after %{HTTP_HOST}.

Now check that you have mod_rewrite enabled (for example check that /etc/apache2/mods-enable/rewrite.load exists). If it's not enabled it, do it now:

$ sudo a2enmod rewrite
$ sudo /etc/init.d/apache2 reload

You can test the following urls to see if everything is going well, the results should be pretty obvious:

See you next time !

Now you have a working multi-domain architecture for your symfony project, but there are still a couple of things we could (and will) discuss:

  • uploading assets (from forms for example)
  • having a different layout for each site
  • having specific css files for each site (eg: paris.css and auckland.css)
  • cache problematic for shared assets

All these points will be addressed in the next part of this tutorial, but not before 2009 has came !

Thursday 26 June 2008

Apache and mod_rewrite to subdirectories

Naneau just poke me with a little problem he had with mod_rewrite when trying to rewrite to a subdirectory. Imagine you've got the following setup:

  • Apache's document root is /document_root/
  • You application's bootstrap is /document_root/public/index.php

You could come to the following rewrite rules quite easily:

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ public/index.php/$1 [L]

And you'd be horribly wrong.

The problem here is that when you hit / on your server, mod_rewrite will populate %{REQUEST_FILENAME} it to /document_root/, which obviously fails the !-d rewrite condition. By the magic of DirectoryIndex, you'll eventually hit /index.html (or whatever your directory index is set to), and there we go for another rewrite magic. At this point, things get a little messy, and if you're like naneau, you'll end up crying while rolling on the floor and calling for help on irc (that's quite a set of hard things to achieve at the same time).

The solution is not that simple, and actually, I've not found a fully satisfying solution yet (although naneau is satisfied with the partial solution). The quick hack is to simpy remove the !-d condition. The obvious drawback is that any existing directory will get rewritten, but you'll be able to access the files inside it. I'm still working on a more complete solution, but as it's not my main concerne for the moment, it'll wait a bit (unless someone posts a solution in the comments).

Sunday 1 October 2006

Edgy Eft, Dotdeb, php5-pdo-mysql, et moi

Bon voilà, ce n'est un secret pour personne, j'ai mis à jour ma Dapper en Edgy, et après avoir laborieusement activé l'accélération 3D de ma carte graphique, je me suis attelé à la reconstitution de mon environnement de développement favori: LAMP5. Cet enrivonnement comprend de plus l'extension PDO Mysql, très utile dans le cadre de l'utilisation du Zend Framework :-) Seulement voilà, Edgy propose un paquet php5 plus à jour que celui de dotdeb, ce qui empêche l'installation du paquet php5-pdo-mysql de dotdeb. Ayant mieux à faire qu'installer pdo_mysql via pear, j'ai décidé d'apprendre à me servir un peu d'APT, et je me propose de vous faire part de mes conclusions. Commençons par le commencement, le message d'erreur:

Les paquets suivants contiennent des dépendances non satisfaites :
  php5-pdo-mysql: Dépend: phpapi-20041225
                  Dépend: php5-common (= 5.1.6-0.dotdeb.2) mais 5.1.6-1ubuntu1 devra être installé
E: Paquets défectueux

Il suffit donc de spécifier à APT que nous souhaitons utiliser la version 5.1.6-0.dotdeb.2 du paquet php5-common. Rien de plus simple ! Cela se passe dans /etc/apt/preferences:

Package: php5-common
Pin: version 5.1.6-0.dotdeb.2
Pin-Priority: 1001

Et comme libapache2-mod-php5 dépend également de php5-common, il va nous falloir faire de même:

Package: libapache2-mod-php5
Pin: version 5.1.6-0.dotdeb.2
Pin-Priority: 1001

Voilà, désormais APT n'installera que la version 5.1.6-0.dotdeb.2 de ces deux paquets, quoiqu'il arrive. Un simple apt-get install php5-pdo-mysql suffit désormais pour que tout rentre dans l'ordre !

A noter: le tutoriel qui m'a tout appris.