edge
Plus sur rubyonrails.org: Plus de Ruby on Rails

Guide classique vers Zeitwerk HOWTO

Ce guide documente comment migrer les applications Rails du mode "classique" au mode "zeitwerk".

Après avoir lu ce guide, vous saurez :

1 Quels sont les modes "classique" et "zeitwerk" ?

Depuis le début et jusqu'à Rails 5, Rails utilisait un chargeur automatique implémenté dans Active Support. Ce chargeur automatique est connu sous le nom de "classique" et est toujours disponible dans Rails 6.x. Rails 7 ne comprend plus ce chargeur automatique.

À partir de Rails 6, Rails est livré avec une nouvelle et meilleure façon de charger automatiquement, qui délègue au gem Zeitwerk. C'est le mode "zeitwerk". Par défaut, les applications chargées avec les paramètres par défaut des frameworks 6.0 et 6.1 s'exécutent en mode "zeitwerk", et c'est le seul mode disponible dans Rails 7.

2 Pourquoi passer du mode "classique" au mode "zeitwerk" ?

Le chargeur automatique "classique" a été extrêmement utile, mais il présentait un certain nombre de problèmes qui rendaient le chargement automatique un peu délicat et confus parfois. Zeitwerk a été développé pour résoudre cela, entre autres motivations.

Lors de la mise à niveau vers Rails 6.x, il est fortement recommandé de passer en mode "zeitwerk" car c'est un meilleur chargeur automatique, le mode "classique" est déprécié.

Rails 7 met fin à la période de transition et ne comprend pas le mode "classique".

3 Je suis effrayé

Ne le soyez pas :).

Zeitwerk a été conçu pour être aussi compatible que possible avec le chargeur automatique classique. Si vous avez une application fonctionnant correctement avec le chargement automatique aujourd'hui, il y a de fortes chances que le passage se fasse facilement. De nombreux projets, grands et petits, ont signalé des transitions très fluides.

Ce guide vous aidera à changer de chargeur automatique en toute confiance.

Si, pour une raison quelconque, vous rencontrez une situation que vous ne savez pas comment résoudre, n'hésitez pas à ouvrir un problème dans rails/rails et à taguer @fxn.

4 Comment activer le mode "zeitwerk" ?

4.1 Applications exécutant Rails 5.x ou moins

Dans les applications exécutant une version de Rails antérieure à 6.0, le mode "zeitwerk" n'est pas disponible. Vous devez être au moins en Rails 6.0.

4.2 Applications exécutant Rails 6.x

Dans les applications exécutant Rails 6.x, il existe deux scénarios.

Si l'application charge les paramètres par défaut du framework de Rails 6.0 ou 6.1 et s'exécute en mode "classique", elle doit être désactivée manuellement. Vous devez avoir quelque chose de similaire à ceci :

# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic # SUPPRIMEZ CETTE LIGNE

Comme indiqué, supprimez simplement la substitution, le mode "zeitwerk" est le mode par défaut.

D'autre part, si l'application charge d'anciens paramètres par défaut du framework, vous devez activer explicitement le mode "zeitwerk" :

# config/application.rb
config.load_defaults 5.2
config.autoloader = :zeitwerk

4.3 Applications exécutant Rails 7

Dans Rails 7, il n'y a que le mode "zeitwerk", vous n'avez rien à faire pour l'activer.

En effet, dans Rails 7, le setter config.autoloader= n'existe même pas. Si config/application.rb l'utilise, veuillez supprimer la ligne.

5 Comment vérifier que l'application s'exécute en mode "zeitwerk" ?

Pour vérifier que l'application s'exécute en mode "zeitwerk", exécutez la commande suivante :

bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'

Si cela affiche true, le mode "zeitwerk" est activé.

6 Mon application est-elle conforme aux conventions de Zeitwerk ?

6.1 config.eager_load_paths

Le test de conformité ne s'applique qu'aux fichiers chargés de manière anticipée. Par conséquent, afin de vérifier la conformité de Zeitwerk, il est recommandé d'avoir tous les chemins de chargement automatique dans les chemins de chargement anticipé.

C'est déjà le cas par défaut, mais si le projet a des chemins de chargement automatique personnalisés configurés comme ceci :

config.autoload_paths << "#{Rails.root}/extras"

ceux-ci ne sont pas chargés de manière anticipée et ne seront pas vérifiés. Les ajouter aux chemins de chargement anticipé est facile :

config.autoload_paths << "#{Rails.root}/extras"
config.eager_load_paths << "#{Rails.root}/extras"

6.2 zeitwerk:check

Une fois le mode "zeitwerk" activé et la configuration des chemins de chargement anticipé vérifiée, veuillez exécuter :

bin/rails zeitwerk:check

Un test réussi ressemble à ceci :

% bin/rails zeitwerk:check
Attendez, je charge l'application de manière anticipée.
Tout va bien !

Il peut y avoir une sortie supplémentaire en fonction de la configuration de l'application, mais le dernier "Tout va bien !" est ce que vous recherchez. Si la double vérification expliquée dans la section précédente a déterminé qu'il doit effectivement y avoir des chemins d'autoload personnalisés en dehors des chemins de chargement anticipé, la tâche les détectera et vous avertira à leur sujet. Cependant, si la suite de tests charge ces fichiers avec succès, tout va bien.

Maintenant, si un fichier ne définit pas la constante attendue, la tâche vous le signalera. Elle le fait un fichier à la fois, car si elle passait à autre chose, l'échec de chargement d'un fichier pourrait entraîner d'autres échecs sans rapport avec la vérification que nous voulons effectuer et le rapport d'erreur serait confus.

S'il y a une constante signalée, corrigez celle-ci en particulier et exécutez à nouveau la tâche. Répétez jusqu'à obtenir "Tout va bien !".

Prenons par exemple :

% bin/rails zeitwerk:check
Attendez, je charge l'application avec impatience.
Le fichier app/models/vat.rb était censé définir la constante Vat

La TVA est une taxe européenne. Le fichier app/models/vat.rb définit VAT, mais le chargeur automatique s'attend à Vat, pourquoi ?

6.3 Acronymes

Il s'agit du type de divergence le plus courant que vous pouvez rencontrer, cela concerne les acronymes. Commençons par comprendre pourquoi nous obtenons ce message d'erreur.

Le chargeur automatique classique est capable de charger automatiquement VAT car son entrée est le nom de la constante manquante, VAT, il invoque underscore sur celui-ci, ce qui donne vat, et recherche un fichier appelé vat.rb. Cela fonctionne.

L'entrée du nouveau chargeur automatique est le système de fichiers. Étant donné le fichier vat.rb, Zeitwerk invoque camelize sur vat, ce qui donne Vat, et s'attend à ce que le fichier définisse la constante Vat. C'est ce que dit le message d'erreur.

La correction est facile, il vous suffit d'indiquer à l'inflecteur cet acronyme :

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym "VAT"
end

Cela affecte la façon dont Active Support inflecte globalement. Cela peut être bien, mais si vous préférez, vous pouvez également passer des remplacements aux inflecteurs utilisés par les chargeurs automatiques :

# config/initializers/zeitwerk.rb
Rails.autoloaders.main.inflector.inflect("vat" => "VAT")

Avec cette option, vous avez plus de contrôle, car seuls les fichiers appelés exactement vat.rb ou les répertoires appelés exactement vat seront inflectés en tant que VAT. Un fichier appelé vat_rules.rb n'est pas affecté par cela et peut très bien définir VatRules. Cela peut être pratique si le projet présente ce genre d'incohérences de dénomination.

Avec cela en place, la vérification réussit !

% bin/rails zeitwerk:check
Attendez, je charge l'application avec impatience.
Tout va bien !

Une fois que tout va bien, il est recommandé de continuer à valider le projet dans la suite de tests. La section Vérifier la conformité de Zeitwerk dans la suite de tests explique comment faire cela.

6.4 Concerns

Vous pouvez charger automatiquement et charger avec impatience à partir d'une structure standard avec des sous-répertoires concerns comme

app/models
app/models/concerns

Par défaut, app/models/concerns appartient aux chemins d'autoload et il est donc supposé être un répertoire racine. Ainsi, par défaut, app/models/concerns/foo.rb doit définir Foo, et non Concerns::Foo.

Si votre application utilise Concerns comme espace de noms, vous avez deux options :

  1. Supprimez l'espace de noms Concerns de ces classes et modules et mettez à jour le code client.
  2. Laissez les choses telles quelles en supprimant app/models/concerns des chemins d'autoload :
  # config/initializers/zeitwerk.rb
  ActiveSupport::Dependencies.
    autoload_paths.
    delete("#{Rails.root}/app/models/concerns")

6.5 Présence de app dans les chemins d'autoload

Certains projets veulent que quelque chose comme app/api/base.rb définisse API::Base, et ajoutent app aux chemins d'autoload pour y parvenir.

Étant donné que Rails ajoute automatiquement tous les sous-répertoires de app aux chemins d'autoload (avec quelques exceptions), nous avons une autre situation dans laquelle il y a des répertoires racines imbriqués, similaire à ce qui se passe avec app/models/concerns. Cette configuration ne fonctionne plus telle quelle.

Cependant, vous pouvez conserver cette structure, il vous suffit de supprimer app/api des chemins d'autoload dans un initialiseur :

# config/initializers/zeitwerk.rb
ActiveSupport::Dependencies.
  autoload_paths.
  delete("#{Rails.root}/app/api")

Attention aux sous-répertoires qui n'ont pas de fichiers à charger avec impatience. Par exemple, si l'application a app/admin avec des ressources pour ActiveAdmin, vous devez les ignorer. De même pour assets et ses amis :

# config/initializers/zeitwerk.rb
Rails.autoloaders.main.ignore(
  "app/admin",
  "app/assets",
  "app/javascripts",
  "app/views"
)

Sans cette configuration, l'application chargerait ces arbres avec impatience. Elle échouerait sur app/admin car ses fichiers ne définissent pas de constantes, et elle définirait un module Views, par exemple, en tant qu'effet secondaire indésirable.

Comme vous pouvez le voir, avoir app dans les chemins d'autoload est techniquement possible, mais un peu délicat.

6.6 Constantes chargées automatiquement et espaces de noms explicites

Si un espace de noms est défini dans un fichier, comme Hotel ici : app/models/hotel.rb # Définit Hotel. app/models/hotel/pricing.rb # Définit Hotel::Pricing.

la constante Hotel doit être définie à l'aide des mots-clés class ou module. Par exemple :

class Hotel
end

est correct.

Des alternatives comme

Hotel = Class.new

ou

Hotel = Struct.new

ne fonctionneront pas, les objets enfants comme Hotel::Pricing ne seront pas trouvés.

Cette restriction s'applique uniquement aux espaces de noms explicites. Les classes et modules qui ne définissent pas d'espace de noms peuvent être définis en utilisant ces idiomes.

6.7 Un fichier, une constante (au même niveau supérieur)

En mode classic, vous pouvez techniquement définir plusieurs constantes au même niveau supérieur et les recharger toutes. Par exemple, étant donné

# app/models/foo.rb

class Foo
end

class Bar
end

alors que Bar ne pourrait pas être chargé automatiquement, le chargement automatique de Foo marquerait également Bar comme chargé automatiquement.

Ce n'est pas le cas en mode zeitwerk, vous devez déplacer Bar dans son propre fichier bar.rb. Un fichier, une constante de niveau supérieur.

Cela ne concerne que les constantes au même niveau supérieur que dans l'exemple ci-dessus. Les classes et modules internes sont corrects. Par exemple, considérez

# app/models/foo.rb

class Foo
  class InnerClass
  end
end

Si l'application recharge Foo, elle rechargera également Foo::InnerClass.

6.8 Globs dans config.autoload_paths

Attention aux configurations qui utilisent des caractères génériques comme

config.autoload_paths += Dir["#{config.root}/extras/**/"]

Chaque élément de config.autoload_paths doit représenter l'espace de noms de niveau supérieur (Object). Cela ne fonctionnera pas.

Pour corriger cela, supprimez simplement les caractères génériques :

config.autoload_paths << "#{config.root}/extras"

6.9 Décoration de classes et modules provenant de moteurs

Si votre application décore des classes ou des modules provenant d'un moteur, il est probable qu'elle fasse quelque chose comme ceci quelque part :

config.to_prepare do
  Dir.glob("#{Rails.root}/app/overrides/**/*_override.rb").sort.each do |override|
    require_dependency override
  end
end

Cela doit être mis à jour : vous devez indiquer au chargeur automatique main d'ignorer le répertoire des remplacements, et vous devez les charger avec load à la place. Quelque chose comme ceci :

overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
  Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override|
    load override
  end
end

6.10 before_remove_const

Rails 3.1 a ajouté la prise en charge d'un rappel appelé before_remove_const qui était invoqué si une classe ou un module répondait à cette méthode et allait être rechargé. Ce rappel est resté autrement non documenté et il est peu probable que votre code l'utilise.

Cependant, au cas où il le ferait, vous pouvez réécrire quelque chose comme

class Country < ActiveRecord::Base
  def self.before_remove_const
    expire_redis_cache
  end
end

comme

# config/initializers/country.rb
if Rails.application.config.reloading_enabled?
  Rails.autoloaders.main.on_unload("Country") do |klass, _abspath|
    klass.expire_redis_cache
  end
end

6.11 Spring et l'environnement test

Spring recharge le code de l'application si quelque chose change. Dans l'environnement test, vous devez activer le rechargement pour que cela fonctionne :

# config/environments/test.rb
config.cache_classes = false

ou, depuis Rails 7.1 :

# config/environments/test.rb
config.enable_reloading = true

Sinon, vous obtiendrez :

reloading is disabled because config.cache_classes is true

ou

reloading is disabled because config.enable_reloading is false

Cela n'a pas d'impact sur les performances.

6.12 Bootsnap

Assurez-vous de dépendre au moins de Bootsnap 1.4.4.

7 Vérifier la conformité de Zeitwerk dans la suite de tests

La tâche zeitwerk:check est pratique lors de la migration. Une fois que le projet est conforme, il est recommandé d'automatiser cette vérification. Pour ce faire, il suffit de charger l'application de manière anticipée, ce que fait précisément zeitwerk:check.

7.1 Intégration continue

Si votre projet dispose d'une intégration continue, il est conseillé de charger l'application de manière anticipée lorsque la suite s'exécute. Si l'application ne peut pas être chargée de manière anticipée pour une raison quelconque, vous voulez le savoir en CI, mieux qu'en production, n'est-ce pas ?

Les CI définissent généralement une variable d'environnement pour indiquer que la suite de tests s'exécute là-bas. Par exemple, cela pourrait être CI :

# config/environments/test.rb
config.eager_load = ENV["CI"].present?

À partir de Rails 7, les applications nouvellement générées sont configurées de cette manière par défaut.

7.2 Suites de tests minimales

Si votre projet n'a pas d'intégration continue, vous pouvez toujours charger de manière anticipée dans la suite de tests en appelant Rails.application.eager_load! :

7.2.1 Minitest

require "test_helper"

class ZeitwerkComplianceTest < ActiveSupport::TestCase
  test "eager loads all files without errors" do
    assert_nothing_raised { Rails.application.eager_load! }
  end
end

7.2.2 RSpec

require "rails_helper"

RSpec.describe "Zeitwerk compliance" do
  it "eager loads all files without errors" do
    expect { Rails.application.eager_load! }.not_to raise_error
  end
end

8 Supprimez tous les appels require

Dans mon expérience, les projets ne le font généralement pas. Mais j'en ai vu quelques-uns, et j'en ai entendu parler pour quelques autres. Dans une application Rails, vous utilisez require exclusivement pour charger du code depuis lib ou depuis des dépendances tierces telles que des gems ou la bibliothèque standard. Ne chargez jamais de code d'application pouvant être chargé automatiquement avec require. Vous pouvez voir pourquoi c'était une mauvaise idée dans le mode classic ici.

require "nokogiri" # BIEN
require "net/http" # BIEN
require "user"     # MAUVAIS, SUPPRIMEZ CELA (en supposant que app/models/user.rb existe)

Veuillez supprimer tous les appels require de ce type.

9 Nouvelles fonctionnalités que vous pouvez exploiter

9.1 Supprimez les appels à require_dependency

Tous les cas d'utilisation connus de require_dependency ont été éliminés avec Zeitwerk. Vous devriez rechercher ces appels dans le projet et les supprimer.

Si votre application utilise l'héritage de table unique, veuillez consulter la section Héritage de table unique du guide Autoloading and Reloading Constants (Zeitwerk Mode).

9.2 Les noms qualifiés dans les définitions de classes et de modules sont maintenant possibles

Vous pouvez maintenant utiliser de manière robuste des chemins de constantes dans les définitions de classes et de modules :

# Le chargement automatique dans le corps de cette classe correspond maintenant à la sémantique de Ruby.
class Admin::UsersController < ApplicationController
  # ...
end

Un point à noter est que, selon l'ordre d'exécution, le chargeur automatique classique pouvait parfois charger automatiquement Foo::Wadus dans :

class Foo::Bar
  Wadus
end

Cela ne correspond pas à la sémantique de Ruby car Foo n'est pas dans l'imbrication, et cela ne fonctionnera pas du tout en mode zeitwerk. Si vous rencontrez un tel cas particulier, vous pouvez utiliser le nom qualifié Foo::Wadus :

class Foo::Bar
  Foo::Wadus
end

ou ajouter Foo à l'imbrication :

module Foo
  class Bar
    Wadus
  end
end

9.3 La sécurité des threads partout

En mode classic, le chargement automatique des constantes n'est pas sûr pour les threads, bien que Rails dispose de verrous pour rendre par exemple les requêtes web sûres pour les threads.

Le chargement automatique des constantes est sûr pour les threads en mode zeitwerk. Par exemple, vous pouvez maintenant charger automatiquement dans des scripts multi-thread exécutés par la commande runner.

9.4 Le chargement anticipé et le chargement automatique sont cohérents

En mode classic, si app/models/foo.rb définit Bar, vous ne pourrez pas charger automatiquement ce fichier, mais le chargement anticipé fonctionnera car il charge les fichiers de manière récursive sans discernement. Cela peut être une source d'erreurs si vous testez d'abord le chargement anticipé, l'exécution peut échouer plus tard lors du chargement automatique.

En mode zeitwerk, les deux modes de chargement sont cohérents, ils échouent et génèrent des erreurs dans les mêmes fichiers.

Retour d'information

Vous êtes encouragé à contribuer à l'amélioration de la qualité de ce guide.

Veuillez contribuer si vous trouvez des fautes de frappe ou des erreurs factuelles. Pour commencer, vous pouvez lire notre contribution à la documentation section.

Vous pouvez également trouver du contenu incomplet ou des informations qui ne sont pas à jour. Veuillez ajouter toute documentation manquante pour la version principale. Assurez-vous de vérifier Edge Guides d'abord pour vérifier si les problèmes ont déjà été résolus ou non sur la branche principale. Consultez les Directives des guides Ruby on Rails pour le style et les conventions.

Si pour une raison quelconque vous repérez quelque chose à corriger mais ne pouvez pas le faire vous-même, veuillez ouvrir un problème.

Et enfin, toute discussion concernant la documentation de Ruby on Rails est la bienvenue sur le Forum officiel de Ruby on Rails.