Blog tech

Gestion de sous-domaines avec Rails

Rédigé par Alexandre Salaun | 10 octobre 2012

Lors du développement d’un site ou d’une application web il est possible que vous ayez à gérer des sous-domaines. En effet, ces derniers peuvent servir à séparer différentes parties de votre site pour plus de clarté mais aussi pour des besoins de référencement par exemple.

Lors de mes développement en local j’utilise Pow, ce qui me permet d’avoir des urls du type mon-projet.dev et je peux donc avoir des sous-domaines sans avoir à modifier mon fichier /etc/hosts.

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érer les models et controllers nécessaires (pensez à ajouter un champ subdomain dans votre model Team), il vous faut ajouter une constraint afin de gérer les sous-domaines.

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 si 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é.

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

Il est possible d’ajouter d’autres exception 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éé :

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 cet helper dans votre ApplicationController pour qu’il soit disponible dans tous les controllers du projet. Il est aussi possible de l’inclure uniquement dans certains controllers si vous jugez cela plus pertinent.

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

<%= 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 :

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 urls :

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

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

<%= 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écedemment pour spécifier à quel niveau vous souhaitez (dans ce cas nous avons deux niveaux de sous-domaines) :

# /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-domaines qui sera modifié dans ce cas et le second restera identique pour obtenir des urls du type one.test.myapp.dev ou two.test.myapp.dev.

Finder et sous-domaine

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

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 si il s’agissait de simple nested resources :

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 aucune différence avec l’usage d’un id. Il faut tout de même bien vérifier que le champ subdomain soit bien unique.

Validations

Pour notre model 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 model, il suffit d’ajouter la validation suivante :

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 :

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

Il y a donc une validation sur le nombre de caractères du sous-domaine ou sur les noms réservés. De plus, il est possible d’ajouter facilement des sous-domaines réservés :

validates :subdomain, presence: true, uniqueness: true, subdomain: { :reserved => %w(www test) }

Gestion des cookies

L’une des dernières choses à gérer avec les sous-domaines concerne les cookies. Par défaut, les cookies concerne 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 :

# /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

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 ces derniers.

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

Dans ce cas, nous allons donc avoir des urls 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 :

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édement sauf 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.

Conclusion

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.

L’équipe Synbioz.

Libres d’être ensemble.