I. Gestion des routes et des liens

Dans l'exemple choisi, nous allons développer une application qui gère des championnats sportifs et chaque équipe aura son sous-domaine.

Dans votre application Rails, après avoir généré les modèles et contrôleurs nécessaires (pensez à ajouter un champ subdomain dans votre modèle Team), il vous faut ajouter une constraint afin de gérer les sous-domaines.

 
Sélectionnez

MonApplication::Application.routes.draw do
  constraints(Subdomain) do
    match '/' => 'teams#show'  
  end
end

Ensuite, il est nécessaire de récupérer le sous-domaine afin de savoir s'il faut le prendre en compte ou non. Pour cela, une classe Subdomain doit être ajoutée dans le répertoire lib/. Cette dernière renvoie un booléen afin de savoir si oui ou non le code contenu dans la constraint est exécuté.

 
Sélectionnez

class Subdomain
  def self.matches?(request)
    request.subdomain.present? && request.subdomain != 'www'
  end
end

Il est possible d'ajouter d'autres exceptions que « www » si certains sous-domaines sont réservés à d'autres usages par exemple.

En ce qui concerne les liens dans vos vues, il est nécessaire de passer en paramètre le sous-domaine lorsque cela est nécessaire. Pour cela, un helper peut être créé :

 
Sélectionnez

module UrlHelper
  def with_subdomain(subdomain)
    subdomain ||= ""
    subdomain += "." unless subdomain.empty?
    [subdomain, request.domain, request.port_string].join
  end
end

Il suffit ensuite d'inclure ce helper dans votre ApplicationController pour qu'il soit disponible dans tous les contrôleurs du projet. Il est aussi possible de l'inclure uniquement dans certains contrôleurs si vous jugez cela plus pertinent.

Cela permet dans les vues de générer des liens de la façon suivante :

 
Sélectionnez

<%= link_to team.name, root_url(:host => with_subdomain(team.subdomain)) %>

Ce lien redirigera l'utilisateur vers la page show de l'équipe en question avec le sous-domaine correspondant.

Afin de faciliter l'usage des routes pour générer des liens, il est possible de surcharger la méthode url_for :

 
Sélectionnez

def url_for(options = nil)
  if options.kind_of?(Hash) && options.has_key?(:subdomain)
    options[:host] = with_subdomain(options.delete(:subdomain))
  end
  super
end

Grâce à cette méthode, il sera possible de passer un paramètre subdomain aux URL :

 
Sélectionnez

<%= link_to team.name, root_url(:subdomain => team.subdomain) %>

Afin de générer des URL sans aucun sous-domaine, il suffit de le préciser :

 
Sélectionnez

<%= link_to "Accueil", root_url(:subdomain => false) %>

Il se peut également qu'il y ait plusieurs niveaux de sous-domaines dans votre application. Afin de gérer cela, il suffit de modifier les méthodes créées précédemment pour spécifier quel niveau vous souhaitez (dans ce cas nous avons deux niveaux de sous-domaines) :

 
Sélectionnez

# /app/helpers/url_helper.rb
def with_subdomain(subdomain)
  subdomain ||= ""
  subdomain += "." unless subdomain.empty?
  [subdomain, request.domain(2), request.port_string].join
end
 
# /lib/subdomain.rb
class Subdomain
  def self.matches?(request)
    request.subdomain(2).present? && request.subdomain(2) != "www"
  end
end

C'est donc le premier sous-domaine qui sera modifié dans ce cas et le second restera identique pour obtenir des URL du type one.test.myapp.dev ou two.test.myapp.dev.

II. Finder et sous-domaine

Une fois que vous avez réussi à créer vos routes et vos URL en gérant les sous-domaines, il faut les récupérer dans les contrôleurs. Dans notre cas, nous voulons trouver l'équipe (Team) correspondant au sous-domaine :

 
Sélectionnez

class TeamsController < ApplicationController
  def show
    @team = Team.find_by_subdomain(request.subdomain)
  end
end

Les objets sont donc récupérés grâce à leur sous-domaine et non plus grâce à leur id et il vous est possible de créer un sous-domaine par équipe sur votre site et de récupérer l'équipe correspondante à chaque fois.

Vous pouvez ensuite gérer vos routes comme s'il s'agissait de simple nested resources :

 
Sélectionnez

constraints(Subdomain) do
  match '/' => 'groups#show'
 
  resources :players
  resources :games
end

En ajoutant un before_filter qui récupérera l'équipe grâce au sous-domaine, il n'y a aucune différence avec l'usage d'un id. Il faut tout de même bien vérifier que le champ subdomain est bien unique.

III. Validations

Pour notre modèle Team nous avons créé un champ subdomain afin de pouvoir utiliser un finder sur ce champ. Il peut s'avérer utile d'avoir des validations sur champ et on peut en trouver une très intéressante sur ce site.

Dans le modèle, il suffit d'ajouter la validation suivante :

 
Sélectionnez

validates :subdomain, presence: true, uniqueness: true, subdomain: true

Il y a donc un validateur de sous-domaine. Il faut maintenant créer ce dernier. Pour cela, ajoutez un fichier subdomain_validator.rb contenant le code suivant :

 
Sélectionnez

class SubdomainValidator < ActiveModel::EachValidator  
  def validate_each(object, attribute, value)
    return unless value.present?
 
    reserved_names = %w(www ftp mail pop smtp admin ssl sftp)
    reserved_names = options[:reserved] if options[:reserved]
 
    if reserved_names.include?(value)
      object.errors[attribute] << 'cannot be a reserved name'
    end                                              
 
    object.errors[attribute] << 'must have between 3 and 63 letters' unless (3..63) === value.length
    object.errors[attribute] << 'cannot start or end with a hyphen' unless value =~ /^[^-].*[^-]$/i                                                                                                    
    object.errors[attribute] << 'must be alphanumeric; A-Z, 0-9 or hyphen' unless value =~ /^[a-z0-9-]*$/i
  end
end

IV. Gestion des cookies

L'une des dernières choses à gérer avec les sous-domaines concerne les cookies. Par défaut, les cookies concernent un domaine et un sous-domaine. Si l'on veut que les cookies soient disponibles quel que soit le sous-domaine il faut modifier l'initializer qui gère ces cookies :

 
Sélectionnez

# /config/initializers/session_store.rb
 
# without subdomain
# MyApp::Application.config.session_store :cookie_store, key: '_myapp_session'
 
# with subdomain
MyApp::Application.config.session_store :cookie_store, key: '_myapp_session', domain: :all

V. Gestion des langues avec les sous-domaines

Hormis le fait de récupérer des objets grâce aux sous-domaines, il est possible de gérer les langues de ces derniers.

 
Sélectionnez

MyProject::Application.routes.draw do
  constraints(Subdomain) do
    root :to => 'home#index'
  end
end

Dans ce cas, nous allons donc avoir des URL du type fr.myapp.dev ou en.myapp.dev.

Ensuite, dans votre ApplicationController, vous pouvez récupérer la locale passée afin de définir la locale de l'application :

 
Sélectionnez

class ApplicationController < ActionController::Base
  before_filter :set_locale
 
  protected
    def set_locale
      I18n.locale = request.subdomain if request.subdomain
    end
end

Le principe est exactement le même que précédemment sauf que dans ce cas c'est la locale qui est passée en sous-domaine au lieu d'un identifiant permettant de récupérer un objet.

VI. Conclusion et remerciements

Grâce au fonctionnement des routes dans Rails, il est possible d'utiliser les sous-domaines de façon assez simple. Comme nous l'avons vu précédemment, différents usages existent suivant l'objectif visé, nous en avons vu deux ici (gestion des langues, sous-domaine par objet) mais ce n'est pas exhaustif.

Afin d'en savoir un peu plus, vous pouvez visionner le Railscast correspondant. D'autre part, la documentation concernant les routes peut vous être utile.

Si vous avez d'autres usages qui semblent intéressants concernant les sous-domaines ou des façons de faire différentes n'hésitez pas à laisser un commentaire sur cet article.

Cet article est publié avec l'aimable autorisation de Synbioz

L'article original peut être lu sur le blog de Synbioz : Gestion de sous-domaines avec Rails.

Nous tenons à remercier ClaudeLELOUP et zoom61 pour leur relecture attentive de cet article.