Gestion de sous-domaines avec Rails

| Synbioz | Bovino |

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éveloppements en local j'utilise Pow, ce qui me permet d'avoir des URL du type mon-projet.dev et je peux donc avoir des sous-domaines sans avoir à modifier mon fichier /etc/hosts.

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.

Commentez

- Titre : Gestion de sous-domaines avec Rails

- Auteurs : Synbioz, Bovino

- Parution : 16 décembre 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Synbioz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Gestion des routes et des liens

- II. Finder et sous-domaine

- III. Validations

- IV. Gestion des cookies

- V. Gestion des langues avec les sous-domaines

- VI. Conclusion et remerciements

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.

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

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

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 :

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

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

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

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

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 :

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 :

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

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 :

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

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 :

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.