Empezando con Motores

En esta guía aprenderás sobre motores y cómo pueden ser utilizados para proporcionar funcionalidad adicional a sus aplicaciones anfitrionas a través de una interfaz limpia y muy fácil de usar.

Después de leer esta guía, sabrás:


1 ¿Qué son los Motores?

Los motores pueden considerarse aplicaciones en miniatura que proporcionan funcionalidad a sus aplicaciones anfitrionas. Una aplicación de Rails es en realidad solo un motor "supercargado", con la clase Rails::Application heredando mucho de su comportamiento de Rails::Engine.

Por lo tanto, los motores y las aplicaciones pueden considerarse casi lo mismo, solo con diferencias sutiles, como verás a lo largo de esta guía. Los motores y las aplicaciones también comparten una estructura común.

Los motores también están estrechamente relacionados con los plugins. Ambos comparten una estructura de directorio lib común y se generan utilizando el generador rails plugin new. La diferencia es que un motor se considera un "plugin completo" por Rails (como lo indica la opción --full que se pasa al comando generador). En realidad, usaremos la opción --mountable aquí, que incluye todas las características de --full, y algo más. Esta guía se referirá a estos "plugins completos" simplemente como "motores" a lo largo de la guía. Un motor puede ser un plugin, y un plugin puede ser un motor.

El motor que se creará en esta guía se llamará "blorgh". Este motor proporcionará funcionalidad de blogging a sus aplicaciones anfitrionas, permitiendo que se creen nuevos artículos y comentarios. Al comienzo de esta guía, trabajarás únicamente dentro del propio motor, pero en secciones posteriores verás cómo conectarlo a una aplicación.

Los motores también pueden estar aislados de sus aplicaciones anfitrionas. Esto significa que una aplicación puede tener una ruta proporcionada por un helper de enrutamiento como articles_path y usar un motor que también proporciona una ruta llamada articles_path, y los dos no entrarían en conflicto. Junto con esto, los controladores, modelos y nombres de tablas también están en un espacio de nombres. Verás cómo hacer esto más adelante en esta guía.

Es importante tener en cuenta en todo momento que la aplicación debe siempre tener prioridad sobre sus motores. Una aplicación es el objeto que tiene la última palabra en lo que sucede en su entorno. El motor solo debe estar mejorándolo, en lugar de cambiarlo drásticamente.

Para ver demostraciones de otros motores, revisa Devise, un motor que proporciona autenticación para sus aplicaciones principales, o Thredded, un motor que proporciona funcionalidad de foro. También está Spree que proporciona una plataforma de comercio electrónico, y Refinery CMS, un motor CMS.

Finalmente, los motores no habrían sido posibles sin el trabajo de James Adam, Piotr Sarnacki, el Equipo Central de Rails y varias otras personas. Si alguna vez los encuentras, ¡no olvides decir gracias!

2 Generando un Motor

Para generar un motor, necesitarás ejecutar el generador de plugins y pasarle opciones según sea apropiado para la necesidad. Para el ejemplo "blorgh", necesitarás crear un motor "montable", ejecutando este comando en una terminal:

$ rails plugin new blorgh --mountable

La lista completa de opciones para el generador de plugins puede verse escribiendo:

$ rails plugin --help

La opción --mountable le dice al generador que deseas crear un motor "montable" y aislado por espacio de nombres. Este generador proporcionará la misma estructura esqueleto que lo haría la opción --full. La opción --full le dice al generador que deseas crear un motor, incluyendo una estructura esqueleto que proporciona lo siguiente:

  • Un árbol de directorios app
  • Un archivo config/routes.rb:

    Rails.application.routes.draw do
    end
    
  • Un archivo en lib/blorgh/engine.rb, que es idéntico en función a un archivo config/application.rb estándar de Rails:

    module Blorgh
      class Engine < ::Rails::Engine
      end
    end
    

La opción --mountable añadirá a la opción --full:

  • Archivos de manifiesto de activos (blorgh_manifest.js y application.css)
  • Un stub de ApplicationController con espacio de nombres
  • Un stub de ApplicationHelper con espacio de nombres
  • Una plantilla de vista de diseño para el motor
  • Aislamiento de espacio de nombres a config/routes.rb:

    Blorgh::Engine.routes.draw do
    end
    
  • Aislamiento de espacio de nombres a lib/blorgh/engine.rb:

    module Blorgh
      class Engine < ::Rails::Engine
        isolate_namespace Blorgh
      end
    end
    

Además, la opción --mountable le indica al generador que monte el motor dentro de la aplicación de prueba dummy ubicada en test/dummy añadiendo lo siguiente al archivo de rutas de la aplicación dummy en test/dummy/config/routes.rb:

mount Blorgh::Engine => "/blorgh"

2.1 Dentro de un Motor

2.1.1 Archivos Críticos

En la raíz del directorio de este nuevo motor vive un archivo blorgh.gemspec. Cuando incluyas el motor en una aplicación más adelante, lo harás con esta línea en el Gemfile de la aplicación de Rails:

gem "blorgh", path: "engines/blorgh"

No olvides ejecutar bundle install como de costumbre. Al especificarlo como un gem dentro del Gemfile, Bundler lo cargará como tal, analizando este archivo blorgh.gemspec y requiriendo un archivo dentro del directorio lib llamado lib/blorgh.rb. Este archivo requiere el archivo blorgh/engine.rb (ubicado en lib/blorgh/engine.rb) y define un módulo base llamado Blorgh.

require "blorgh/engine"

module Blorgh
end

CONSEJO: Algunos motores eligen usar este archivo para poner opciones de configuración globales para su motor. Es una idea relativamente buena, así que si deseas ofrecer opciones de configuración, el archivo donde se define el module de tu motor es perfecto para eso. Coloca los métodos dentro del módulo y estarás listo.

Dentro de lib/blorgh/engine.rb está la clase base para el motor:

module Blorgh
  class Engine < ::Rails::Engine
    isolate_namespace Blorgh
  end
end

Al heredar de la clase Rails::Engine, este gem notifica a Rails que hay un motor en la ruta especificada, y montará correctamente el motor dentro de la aplicación, realizando tareas como agregar el directorio app del motor a la ruta de carga para modelos, mailers, controladores y vistas.

El método isolate_namespace aquí merece una mención especial. Esta llamada es responsable de aislar los controladores, modelos, rutas y otras cosas en su propio espacio de nombres, lejos de componentes similares dentro de la aplicación. Sin esto, existe la posibilidad de que los componentes del motor puedan "filtrarse" en la aplicación, causando interrupciones no deseadas, o que componentes importantes del motor puedan ser sobrescritos por cosas con nombres similares dentro de la aplicación. Uno de los ejemplos de tales conflictos son los helpers. Sin llamar a isolate_namespace, los helpers del motor se incluirían en los controladores de una aplicación.

NOTA: Se recomienda altamente que la línea isolate_namespace se deje dentro de la definición de la clase Engine. Sin ella, las clases generadas en un motor pueden entrar en conflicto con una aplicación.

Lo que significa este aislamiento del espacio de nombres es que un modelo generado por una llamada a bin/rails generate model, como bin/rails generate model article, no se llamará Article, sino que estará en un espacio de nombres y se llamará Blorgh::Article. Además, la tabla para el modelo está en un espacio de nombres, convirtiéndose en blorgh_articles, en lugar de simplemente articles. Similar al espacio de nombres del modelo, un controlador llamado ArticlesController se convierte en Blorgh::ArticlesController y las vistas para ese controlador no estarán en app/views/articles, sino en app/views/blorgh/articles. Los mailers, trabajos y helpers también están en un espacio de nombres.

Finalmente, las rutas también estarán aisladas dentro del motor. Esta es una de las partes más importantes sobre el espacio de nombres, y se discute más adelante en la sección Rutas de esta guía.

2.1.2 Directorio app

Dentro del directorio app están los directorios estándar assets, controllers, helpers, jobs, mailers, models y views con los que deberías estar familiarizado por una aplicación. Veremos más sobre modelos en una sección futura, cuando estemos escribiendo el motor.

Dentro del directorio app/assets, están los directorios images y stylesheets que, nuevamente, deberías estar familiarizado debido a su similitud con una aplicación. Una diferencia aquí, sin embargo, es que cada directorio contiene un subdirectorio con el nombre del motor. Debido a que este motor va a estar en un espacio de nombres, sus activos también deberían estarlo.

Dentro del directorio app/controllers hay un directorio blorgh que contiene un archivo llamado application_controller.rb. Este archivo proporcionará cualquier funcionalidad común para los controladores del motor. El directorio blorgh es donde irán los otros controladores para el motor. Al colocarlos dentro de este directorio con espacio de nombres, evitas que entren en conflicto con controladores con nombres idénticos dentro de otros motores o incluso dentro de la aplicación.

NOTA: La clase ApplicationController dentro de un motor se nombra igual que una aplicación de Rails para facilitar la conversión de tus aplicaciones en motores.

Al igual que para app/controllers, encontrarás un subdirectorio blorgh bajo los directorios app/helpers, app/jobs, app/mailers y app/models que contiene el archivo application_*.rb asociado para reunir funcionalidades comunes. Al colocar tus archivos bajo este subdirectorio y poner tus objetos en un espacio de nombres, evitas que entren en conflicto con elementos con nombres idénticos dentro de otros motores o incluso dentro de la aplicación.

Por último, el directorio app/views contiene una carpeta layouts, que contiene un archivo en blorgh/application.html.erb. Este archivo te permite especificar un diseño para el motor. Si este motor se va a usar como un motor independiente, entonces agregarías cualquier personalización a su diseño en este archivo, en lugar del archivo app/views/layouts/application.html.erb de la aplicación.

Si no deseas imponer un diseño a los usuarios del motor, entonces puedes eliminar este archivo y hacer referencia a un diseño diferente en los controladores de tu motor.

2.1.3 Directorio bin

Este directorio contiene un archivo, bin/rails, que te permite usar los subcomandos y generadores de rails tal como lo harías dentro de una aplicación. Esto significa que podrás generar nuevos controladores y modelos para este motor muy fácilmente ejecutando comandos como este:

$ bin/rails generate model

Ten en cuenta, por supuesto, que cualquier cosa generada con estos comandos dentro de un motor que tenga isolate_namespace en la clase Engine estará en un espacio de nombres.

2.1.4 Directorio test

El directorio test es donde irán las pruebas para el motor. Para probar el motor, hay una versión reducida de una aplicación de Rails incrustada dentro de él en test/dummy. Esta aplicación montará el motor en el archivo test/dummy/config/routes.rb:

Rails.application.routes.draw do
  mount Blorgh::Engine => "/blorgh"
end

Esta línea monta el motor en la ruta /blorgh, lo que lo hará accesible a través de la aplicación solo en esa ruta.

Dentro del directorio de prueba está el directorio test/integration, donde deben colocarse las pruebas de integración para el motor. También se pueden crear otros directorios en el directorio test. Por ejemplo, es posible que desees crear un directorio test/models para tus pruebas de modelo.

3 Proporcionando Funcionalidad de Motor

El motor que cubre esta guía proporciona funcionalidad para enviar artículos y comentar, y sigue un hilo similar a la Guía de Inicio Rápido, con algunos giros nuevos.

NOTA: Para esta sección, asegúrate de ejecutar los comandos en la raíz del directorio del motor blorgh.

3.1 Generando un Recurso de Artículo

Lo primero que se debe generar para un motor de blog es el modelo Article y el controlador relacionado. Para generar esto rápidamente, puedes usar el generador de scaffold de Rails.

$ bin/rails generate scaffold article title:string text:text

Este comando mostrará esta información:

invoke  active_record
create    db/migrate/[timestamp]_create_blorgh_articles.rb
create    app/models/blorgh/article.rb
invoke    test_unit
create      test/models/blorgh/article_test.rb
create      test/fixtures/blorgh/articles.yml
invoke  resource_route
 route    resources :articles
invoke  scaffold_controller
create    app/controllers/blorgh/articles_controller.rb
invoke    erb
create      app/views/blorgh/articles
create      app/views/blorgh/articles/index.html.erb
create      app/views/blorgh/articles/edit.html.erb
create      app/views/blorgh/articles/show.html.erb
create      app/views/blorgh/articles/new.html.erb
create      app/views/blorgh/articles/_form.html.erb
create      app/views/blorgh/articles/_article.html.erb
invoke    resource_route
invoke    test_unit
create      test/controllers/blorgh/articles_controller_test.rb
create      test/system/blorgh/articles_test.rb
invoke    helper
create      app/helpers/blorgh/articles_helper.rb
invoke      test_unit

Lo primero que hace el generador de scaffold es invocar el generador active_record, que genera una migración y un modelo para el recurso. Observa aquí, sin embargo, que la migración se llama create_blorgh_articles en lugar del habitual create_articles. Esto se debe al método isolate_namespace llamado en la definición de la clase Blorgh::Engine. El modelo aquí también está en un espacio de nombres, colocándose en app/models/blorgh/article.rb en lugar de app/models/article.rb debido a la llamada a isolate_namespace dentro de la clase Engine.

A continuación, el generador test_unit se invoca para este modelo, generando una prueba de modelo en test/models/blorgh/article_test.rb (en lugar de test/models/article_test.rb) y un fixture en test/fixtures/blorgh/articles.yml (en lugar de test/fixtures/articles.yml).

Después de eso, se inserta una línea para el recurso en el archivo config/routes.rb del motor. Esta línea es simplemente resources :articles, convirtiendo el archivo config/routes.rb del motor en esto:

Blorgh::Engine.routes.draw do
  resources :articles
end

Observa aquí que las rutas se dibujan sobre el objeto Blorgh::Engine en lugar de la clase YourApp::Application. Esto es para que las rutas del motor estén confinadas al propio motor y puedan montarse en un punto específico como se muestra en la sección Directorio de Prueba. También hace que las rutas del motor estén aisladas de aquellas rutas que están dentro de la aplicación. La sección Rutas de esta guía lo describe en detalle.

A continuación, el generador scaffold_controller se invoca, generando un controlador llamado Blorgh::ArticlesController (en app/controllers/blorgh/articles_controller.rb) y sus vistas relacionadas en app/views/blorgh/articles. Este generador también genera pruebas para el controlador (test/controllers/blorgh/articles_controller_test.rb y test/system/blorgh/articles_test.rb) y un helper (app/helpers/blorgh/articles_helper.rb).

Todo lo que este generador ha creado está ordenadamente en un espacio de nombres. La clase del controlador está definida dentro del módulo Blorgh:

module Blorgh
  class ArticlesController < ApplicationController
    # ...
  end
end

NOTA: La clase ArticlesController hereda de Blorgh::ApplicationController, no del ApplicationController de la aplicación.

El helper dentro de app/helpers/blorgh/articles_helper.rb también está en un espacio de nombres:

module Blorgh
  module ArticlesHelper
    # ...
  end
end

Esto ayuda a prevenir conflictos con cualquier otro motor o aplicación que pueda tener un recurso de artículo también.

Puedes ver lo que el motor tiene hasta ahora ejecutando bin/rails db:migrate en la raíz de nuestro motor para ejecutar la migración generada por el generador de scaffold, y luego ejecutando bin/rails server en test/dummy. Cuando abras http://localhost:3000/blorgh/articles verás el scaffold predeterminado que se ha generado. ¡Haz clic! Acabas de generar las primeras funciones de tu primer motor.

Si prefieres jugar en la consola, bin/rails console también funcionará igual que una aplicación de Rails. Recuerda: el modelo Article está en un espacio de nombres, así que para referenciarlo debes llamarlo como Blorgh::Article.

irb> Blorgh::Article.find(1)
=> #<Blorgh::Article id: 1 ...>

Una última cosa es que el recurso articles para este motor debería ser la raíz del motor. Siempre que alguien vaya a la ruta raíz donde se monta el motor, deberían ver una lista de artículos. Esto se puede hacer si se inserta esta línea en el archivo config/routes.rb dentro del motor:

root to: "articles#index"

Ahora la gente solo necesitará ir a la raíz del motor para ver todos los artículos, en lugar de visitar /articles. Esto significa que en lugar de http://localhost:3000/blorgh/articles, solo necesitas ir a http://localhost:3000/blorgh ahora.

3.2 Generando un Recurso de Comentarios

Ahora que el motor puede crear nuevos artículos, solo tiene sentido agregar funcionalidad de comentarios también. Para hacer esto, necesitarás generar un modelo de comentario, un controlador de comentario y luego modificar el scaffold de artículos para mostrar comentarios y permitir que la gente cree nuevos.

Desde la raíz del motor, ejecuta el generador de modelos. Dile que genere un modelo Comment, con la tabla relacionada teniendo dos columnas: una columna de article_id integer y una columna de text text.

$ bin/rails generate model Comment article_id:integer text:text

Esto mostrará lo siguiente:

invoke  active_record
create    db/migrate/[timestamp]_create_blorgh_comments.rb
create    app/models/blorgh/comment.rb
invoke    test_unit
create      test/models/blorgh/comment_test.rb
create      test/fixtures/blorgh/comments.yml

Esta llamada al generador generará solo los archivos de modelo necesarios, poniendo los archivos bajo un directorio blorgh y creando una clase de modelo llamada Blorgh::Comment. Ahora ejecuta la migración para crear nuestra tabla blorgh_comments:

$ bin/rails db:migrate

Para mostrar los comentarios en un artículo, edita app/views/blorgh/articles/show.html.erb y agrega esta línea antes del enlace "Edit":

<h3>Comments</h3>
<%= render @article.comments %>

Esta línea requerirá que haya una asociación has_many para comentarios definida en el modelo Blorgh::Article, que no existe en este momento. Para definir una, abre app/models/blorgh/article.rb y agrega esta línea en el modelo:

has_many :comments

Convirtiendo el modelo en esto:

module Blorgh
  class Article < ApplicationRecord
    has_many :comments
  end
end

NOTA: Debido a que el has_many está definido dentro de una clase que está dentro del módulo Blorgh, Rails sabrá que deseas usar el modelo Blorgh::Comment para estos objetos, por lo que no hay necesidad de especificar eso usando la opción :class_name aquí.

A continuación, debe haber un formulario para que se puedan crear comentarios en un artículo. Para agregar esto, pon esta línea debajo de la llamada a render @article.comments en app/views/blorgh/articles/show.html.erb:

<%= render "blorgh/comments/form" %>

A continuación, el parcial que esta línea renderizará necesita existir. Crea un nuevo directorio en app/views/blorgh/comments y en él un nuevo archivo llamado _form.html.erb que tenga este contenido para crear el parcial requerido:

<h3>New comment</h3>
<%= form_with model: [@article, @article.comments.build] do |form| %>
  <p>
    <%= form.label :text %><br>
    <%= form.text_area :text %>
  </p>
  <%= form.submit %>
<% end %>

Cuando se envíe este formulario, intentará realizar una solicitud POST a una ruta de /articles/:article_id/comments dentro del motor. Esta ruta no existe en este momento, pero se puede crear cambiando la línea resources :articles dentro de config/routes.rb en estas líneas:

resources :articles do
  resources :comments
end

Esto crea una ruta anidada para los comentarios, que es lo que requiere el formulario.

La ruta ahora existe, pero el controlador al que va esta ruta no. Para crearlo, ejecuta este comando desde la raíz del motor:

$ bin/rails generate controller comments

Esto generará las siguientes cosas:

create  app/controllers/blorgh/comments_controller.rb
invoke  erb
 exist    app/views/blorgh/comments
invoke  test_unit
create    test/controllers/blorgh/comments_controller_test.rb
invoke  helper
create    app/helpers/blorgh/comments_helper.rb
invoke    test_unit

El formulario estará haciendo una solicitud POST a /articles/:article_id/comments, que corresponderá con la acción create en Blorgh::CommentsController. Esta acción necesita ser creada, lo que se puede hacer poniendo las siguientes líneas dentro de la definición de clase en app/controllers/blorgh/comments_controller.rb:

def create
  @article = Article.find(params[:article_id])
  @comment = @article.comments.create(comment_params)
  flash[:notice] = "Comment has been created!"
  redirect_to articles_path
end

private
  def comment_params
    params.require(:comment).permit(:text)
  end

Este es el paso final requerido para que el nuevo formulario de comentarios funcione. Sin embargo, mostrar los comentarios no está del todo correcto todavía. Si fueras a crear un comentario ahora mismo, verías este error:

Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder],
:formats=>[:html], :locale=>[:en, :en]}. Searched in:   *
"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views"   *
"/Users/ryan/Sites/side_projects/blorgh/app/views"

El motor no puede encontrar el parcial requerido para renderizar los comentarios. Rails busca primero en el directorio app/views de la aplicación (test/dummy) y luego en el directorio app/views del motor. Cuando no puede encontrarlo, lanzará este error. El motor sabe buscar blorgh/comments/_comment porque el objeto de modelo que está recibiendo es de la clase Blorgh::Comment.

Este parcial será responsable de renderizar solo el texto del comentario, por ahora. Crea un nuevo archivo en app/views/blorgh/comments/_comment.html.erb y pon esta línea dentro:

<%= comment_counter + 1 %>. <%= comment.text %>

La variable local comment_counter nos la proporciona la llamada <%= render @article.comments %>, que la definirá automáticamente e incrementará el contador a medida que itera a través de cada comentario. Se usa en este ejemplo para mostrar un pequeño número junto a cada comentario cuando se crea.

Eso completa la función de comentarios del motor de blogging. Ahora es el momento de usarlo dentro de una aplicación.

4 Conectando a una Aplicación

Usar un motor dentro de una aplicación es muy fácil. Esta sección cubre cómo montar el motor en una aplicación y la configuración inicial requerida, así como vincular el motor a una clase User proporcionada por la aplicación para proporcionar propiedad para artículos y comentarios dentro del motor.

4.1 Montando el Motor

Primero, el motor necesita ser especificado dentro del Gemfile de la aplicación. Si no hay una aplicación a mano para probar esto, genera una usando el comando rails new fuera del directorio del motor así:

$ rails new unicorn

Por lo general, especificarías el motor dentro del Gemfile como un gem normal y corriente.

gem "devise"

Sin embargo, debido a que estás desarrollando el motor blorgh en tu máquina local, necesitarás especificar la opción :path en tu Gemfile:

gem "blorgh", path: "engines/blorgh"

Luego ejecuta bundle para instalar el gem.

Como se describió anteriormente, al colocar el gem en el Gemfile se cargará cuando Rails se cargue. Primero requerirá lib/blorgh.rb del motor, luego lib/blorgh/engine.rb, que es el archivo que define las piezas principales de funcionalidad para el motor.

Para hacer que la funcionalidad del motor sea accesible desde dentro de una aplicación, necesita ser montado en el archivo config/routes.rb de esa aplicación:

mount Blorgh::Engine, at: "/blog"

Esta línea montará el motor en /blog en la aplicación. Haciéndolo accesible en http://localhost:3000/blog cuando la aplicación se ejecuta con bin/rails server.

NOTA: Otros motores, como Devise, manejan esto de manera un poco diferente al hacerte especificar helpers personalizados (como devise_for) en las rutas. Estos helpers hacen exactamente lo mismo, montando piezas de la funcionalidad del motor en una ruta predefinida que puede ser personalizable.

4.2 Configuración del Motor

El motor contiene migraciones para las tablas blorgh_articles y blorgh_comments que necesitan ser creadas en la base de datos de la aplicación para que los modelos del motor puedan consultarlas correctamente. Para copiar estas migraciones en la aplicación ejecuta el siguiente comando desde la raíz de la aplicación:

$ bin/rails blorgh:install:migrations

Si tienes múltiples motores que necesitan migraciones copiadas, usa railties:install:migrations en su lugar:

$ bin/rails railties:install:migrations

Puedes especificar una ruta personalizada en el motor de origen para las migraciones especificando MIGRATIONS_PATH.

$ bin/rails railties:install:migrations MIGRATIONS_PATH=db_blourgh

Si tienes múltiples bases de datos también puedes especificar la base de datos de destino especificando DATABASE.

$ bin/rails railties:install:migrations DATABASE=animals

Este comando, cuando se ejecuta por primera vez, copiará todas las migraciones del motor. Cuando se ejecute la próxima vez, solo copiará las migraciones que no se hayan copiado ya. La primera ejecución de este comando mostrará algo como esto:

Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh

La primera marca de tiempo ([timestamp_1]) será la hora actual, y la segunda marca de tiempo ([timestamp_2]) será la hora actual más un segundo. La razón de esto es para que las migraciones para el motor se ejecuten después de cualquier migración existente en la aplicación.

Para ejecutar estas migraciones dentro del contexto de la aplicación, simplemente ejecuta bin/rails db:migrate. Al acceder al motor a través de http://localhost:3000/blog, los artículos estarán vacíos. Esto se debe a que la tabla creada dentro de la aplicación es diferente de la creada dentro del motor. Adelante, juega con el motor recién montado. Encontrarás que es lo mismo que cuando era solo un motor.

Si deseas ejecutar migraciones solo desde un motor, puedes hacerlo especificando SCOPE:

$ bin/rails db:migrate SCOPE=blorgh

Esto puede ser útil si deseas revertir las migraciones del motor antes de eliminarlo. Para revertir todas las migraciones del motor blorgh puedes ejecutar un código como:

$ bin/rails db:migrate SCOPE=blorgh VERSION=0

4.3 Usando una Clase Proporcionada por la Aplicación

4.3.1 Usando un Modelo Proporcionado por la Aplicación

Cuando se crea un motor, puede querer usar clases específicas de una aplicación para proporcionar enlaces entre las piezas del motor y las piezas de la aplicación. En el caso del motor blorgh, hacer que los artículos y los comentarios tengan autores tendría mucho sentido.

Una aplicación típica podría tener una clase User que se usaría para representar autores para un artículo o un comentario. Pero podría haber un caso en el que la aplicación llame a esta clase algo diferente, como Person. Por esta razón, el motor no debería codificar específicamente las asociaciones para una clase User.

Para mantenerlo simple en este caso, la aplicación tendrá una clase llamada User que representa a los usuarios de la aplicación (entraremos en hacer esto configurable más adelante). Puede generarse usando este comando dentro de la aplicación:

$ bin/rails generate model user name:string

El comando bin/rails db:migrate necesita ejecutarse aquí para asegurar que nuestra aplicación tenga la tabla users para uso futuro.

Además, para mantenerlo simple, el formulario de artículos tendrá un nuevo campo de texto llamado author_name, donde los usuarios pueden elegir poner su nombre. El motor luego tomará este nombre y creará un nuevo objeto User a partir de él o encontrará uno que ya tenga ese nombre. Luego, el motor asociará el artículo con el objeto User encontrado o creado.

Primero, el campo de texto author_name necesita ser agregado al parcial app/views/blorgh/articles/_form.html.erb dentro del motor. Esto se puede agregar sobre el campo title con este código:

<div class="field">
  <%= form.label :author_name %><br>
  <%= form.text_field :author_name %>
</div>

A continuación, necesitamos actualizar nuestro método Blorgh::ArticlesController#article_params para permitir el nuevo parámetro del formulario:

def article_params
  params.require(:article).permit(:title, :text, :author_name)
end

El modelo Blorgh::Article debería tener entonces algún código para convertir el campo author_name en un objeto User real y asociarlo como el author de ese artículo antes de que el artículo se guarde. También necesitará tener un attr_accessor configurado para este campo, para que los métodos setter y getter estén definidos para él.

Para hacer todo esto, deberás agregar el attr_accessor para author_name, la asociación para el autor y la llamada before_validation en app/models/blorgh/article.rb. La asociación author se codificará para la clase User por el momento.

attr_accessor :author_name
belongs_to :author, class_name: "User"

before_validation :set_author

private
  def set_author
    self.author = User.find_or_create_by(name: author_name)
  end

Al representar el objeto de la asociación author con la clase User, se establece un enlace entre el motor y la aplicación. Debe haber una forma de asociar los registros en la tabla blorgh_articles con los registros en la tabla users. Debido a que la asociación se llama author, debería añadirse una columna author_id a la tabla blorgh_articles.

Para generar esta nueva columna, ejecuta este comando dentro del motor:

$ bin/rails generate migration add_author_id_to_blorgh_articles author_id:integer

NOTA: Debido al nombre de la migración y la especificación de la columna después de ella, Rails sabrá automáticamente que deseas agregar una columna a una tabla específica y escribirá eso en la migración por ti. No necesitas decirle más que esto.

Esta migración necesitará ejecutarse en la aplicación. Para hacerlo, primero debe copiarse usando este comando:

$ bin/rails blorgh:install:migrations

Observa que solo una migración se copió aquí. Esto se debe a que las dos primeras migraciones se copiaron la primera vez que se ejecutó este comando.

NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh

Ejecuta la migración usando:

$ bin/rails db:migrate

Ahora con todas las piezas en su lugar, se llevará a cabo una acción que asociará un autor - representado por un registro en la tabla users - con un artículo, representado por la tabla blorgh_articles del motor.

Finalmente, el nombre del autor debería mostrarse en la página del artículo. Agrega este código sobre la salida "Title" dentro de app/views/blorgh/articles/_article.html.erb:

<p>
  <strong>Author:</strong>
  <%= article.author.name %>
</p>

4.3.2 Usando un Controlador Proporcionado por la Aplicación

Debido a que los controladores de Rails generalmente comparten código para cosas como autenticación y acceso a variables de sesión, heredan de ApplicationController por defecto. Sin embargo, los motores de Rails están diseñados para ejecutarse independientemente de la aplicación principal, por lo que cada motor obtiene un ApplicationController con espacio de nombres. Este espacio de nombres previene colisiones de código, pero a menudo los controladores del motor necesitan acceder a métodos en el ApplicationController de la aplicación principal. Una forma fácil de proporcionar este acceso es cambiar el ApplicationController con espacio de nombres del motor para heredar del ApplicationController de la aplicación principal. Para nuestro motor Blorgh esto se haría cambiando app/controllers/blorgh/application_controller.rb para que luzca así:

module Blorgh
  class ApplicationController < ::ApplicationController
  end
end

Por defecto, los controladores del motor heredan de Blorgh::ApplicationController. Así que, después de hacer este cambio, tendrán acceso al ApplicationController de la aplicación principal, como si fueran parte de la aplicación principal.

Este cambio requiere que el motor se ejecute desde una aplicación de Rails que tenga un ApplicationController.

4.4 Configurando un Motor

Esta sección cubre cómo hacer que la clase User sea configurable, seguida de consejos generales de configuración para el motor.

4.4.1 Estableciendo Configuraciones en la Aplicación

El siguiente paso es hacer que la clase que representa a un User en la aplicación sea personalizable para el motor. Esto se debe a que esa clase no siempre puede ser User, como se explicó anteriormente. Para hacer que esta configuración sea personalizable, el motor tendrá una configuración llamada author_class que se usará para especificar qué clase representa a los usuarios dentro de la aplicación.

Para definir esta configuración, debes usar un mattr_accessor dentro del módulo Blorgh para el motor. Agrega esta línea a lib/blorgh.rb dentro del motor:

mattr_accessor :author_class

Este método funciona como sus hermanos, attr_accessor y cattr_accessor, pero proporciona un método setter y getter en el módulo con el nombre especificado. Para usarlo, debe referenciarse usando Blorgh.author_class.

El siguiente paso es cambiar el modelo Blorgh::Article a esta nueva configuración. Cambia la asociación belongs_to dentro de este modelo (app/models/blorgh/article.rb) a esto:

belongs_to :author, class_name: Blorgh.author_class

El método set_author en el modelo Blorgh::Article también debería usar esta clase:

self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)

Para ahorrar tener que llamar a constantize en el resultado de author_class todo el tiempo, podrías simplemente sobrescribir el método getter author_class dentro del módulo Blorgh en el archivo lib/blorgh.rb para siempre llamar a constantize en el valor guardado antes de devolver el resultado:

def self.author_class
  @@author_class.constantize
end

Esto luego convertiría el código anterior para set_author en esto:

self.author = Blorgh.author_class.find_or_create_by(name: author_name)

Resultando en algo un poco más corto y más implícito en su comportamiento. El método author_class siempre debería devolver un objeto Class.

Dado que cambiamos el método author_class para devolver una Class en lugar de una String, también debemos modificar nuestra definición belongs_to en el modelo Blorgh::Article:

belongs_to :author, class_name: Blorgh.author_class.to_s

Para establecer esta configuración dentro de la aplicación, se debe usar un inicializador. Al usar un inicializador, la configuración se establecerá antes de que la aplicación comience y llame a los modelos del motor, que pueden depender de que esta configuración exista.

Crea un nuevo inicializador en config/initializers/blorgh.rb dentro de la aplicación donde está instalado el motor blorgh y pon este contenido en él:

Blorgh.author_class = "User"

ADVERTENCIA: Es muy importante aquí usar la versión String de la clase, en lugar de la clase en sí. Si usaras la clase, Rails intentaría cargar esa clase y luego referenciar la tabla relacionada. Esto podría llevar a problemas si la tabla no existiera ya. Por lo tanto, se debe usar una String y luego convertirla en una clase usando constantize en el motor más adelante.

Adelante, intenta crear un nuevo artículo. Verás que funciona exactamente de la misma manera que antes, excepto que esta vez el motor está usando la configuración en config/initializers/blorgh.rb para saber qué clase es.

Ahora no hay dependencias estrictas sobre qué clase es, solo sobre cuál debe ser la API para la clase. El motor simplemente requiere que esta clase defina un método find_or_create_by que devuelva un objeto de esa clase, para ser asociado con un artículo cuando se crea. Este objeto, por supuesto, debería tener algún tipo de identificador por el cual pueda ser referenciado.

4.4.2 Configuración General del Motor

Dentro de un motor, puede llegar un momento en el que desees usar cosas como inicializadores, internacionalización u otras opciones de configuración. La buena noticia es que estas cosas son completamente posibles, porque un motor de Rails comparte gran parte de la funcionalidad de una aplicación de Rails. De hecho, la funcionalidad de una aplicación de Rails es en realidad un superconjunto de lo que proporcionan los motores.

Si deseas usar un inicializador - código que debería ejecutarse antes de que se cargue el motor - el lugar para ello es la carpeta config/initializers. La funcionalidad de este directorio se explica en la sección de Inicializadores de la guía de Configuración, y funciona exactamente de la misma manera que el directorio config/initializers dentro de una aplicación. Lo mismo ocurre si deseas usar un inicializador estándar.

Para locales, simplemente coloca los archivos de locales en el directorio config/locales, tal como lo harías en una aplicación.

5 Probando un Motor

Cuando se genera un motor, se crea una aplicación dummy más pequeña dentro de él en test/dummy. Esta aplicación se usa como un punto de montaje para el motor, para hacer que probar el motor sea extremadamente simple. Puedes extender esta aplicación generando controladores, modelos o vistas desde dentro del directorio y luego usarlos para probar tu motor.

El directorio test debe tratarse como un entorno de prueba típico de Rails, permitiendo pruebas unitarias, funcionales y de integración.

5.1 Pruebas Funcionales

Un asunto que vale la pena considerar al escribir pruebas funcionales es que las pruebas se ejecutarán en una aplicación - la aplicación test/dummy - en lugar de tu motor. Esto se debe a la configuración del entorno de prueba; un motor necesita una aplicación como anfitrión para probar su funcionalidad principal, especialmente los controladores. Esto significa que si fueras a hacer un GET típico a un controlador en una prueba funcional de un controlador como esta:

module Blorgh
  class FooControllerTest < ActionDispatch::IntegrationTest
    include Engine.routes.url_helpers

    def test_index
      get foos_url
      # ...
    end
  end
end

Puede que no funcione correctamente. Esto se debe a que la aplicación no sabe cómo enrutar estas solicitudes al motor a menos que le digas explícitamente cómo. Para hacer esto, debes establecer la variable de instancia @routes en el conjunto de rutas del motor en tu código de configuración:

module Blorgh
  class FooControllerTest < ActionDispatch::IntegrationTest
    include Engine.routes.url_helpers

    setup do
      @routes = Engine.routes
    end

    def test_index
      get foos_url
      # ...
    end
  end
end

Esto le dice a la aplicación que aún deseas realizar una solicitud GET a la acción index de este controlador, pero deseas usar la ruta del motor para llegar allí, en lugar de la de la aplicación.

Esto también asegura que los métodos helpers de URL del motor funcionen como se espera en tus pruebas.

6 Mejorando la Funcionalidad del Motor

Esta sección explica cómo agregar y/o sobrescribir la funcionalidad MVC del motor en la aplicación principal de Rails.

6.1 Sobrescribiendo Modelos y Controladores

Los modelos y controladores del motor pueden ser reabiertos por la aplicación principal para extenderlos o decorarlos.

Las sobrescrituras pueden organizarse en un directorio dedicado app/overrides, ignorado por el autoloader, y precargado en un callback to_prepare:

# config/application.rb
module MyApp
  class Application < Rails::Application
    # ...

    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
  end
end

6.1.1 Reabriendo Clases Existentes Usando class_eval

Por ejemplo, para sobrescribir el modelo del motor

# Blorgh/app/models/blorgh/article.rb
module Blorgh
  class Article < ApplicationRecord
    # ...
  end
end

simplemente crea un archivo que reabra esa clase:

# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
  # ...
end

Es muy importante que la sobrescritura reabra la clase o el módulo. Usar las palabras clave class o module las definiría si no estuvieran ya en memoria, lo cual sería incorrecto porque la definición vive en el motor. Usar class_eval como se muestra arriba asegura que estás reabriendo.

6.1.2 Reabriendo Clases Existentes Usando ActiveSupport::Concern

Usar Class#class_eval es genial para ajustes simples, pero para modificaciones de clase más complejas, podrías querer considerar usar ActiveSupport::Concern. ActiveSupport::Concern gestiona el orden de carga de módulos y clases dependientes interrelacionados en tiempo de ejecución, permitiéndote modularizar significativamente tu código.

Agregando Article#time_since_created y Sobrescribiendo Article#summary:

# MyApp/app/models/blorgh/article.rb

class Blorgh::Article < ApplicationRecord
  include Blorgh::Concerns::Models::Article

  def time_since_created
    Time.current - created_at
  end

  def summary
    "#{title} - #{truncate(text)}"
  end
end
# Blorgh/app/models/blorgh/article.rb
module Blorgh
  class Article < ApplicationRecord
    include Blorgh::Concerns::Models::Article
  end
end
# Blorgh/lib/concerns/models/article.rb

module Blorgh::Concerns::Models::Article
  extend ActiveSupport::Concern

  # `included do` hace que el bloque se evalúe en el contexto
  # en el que se incluye el módulo (es decir, Blorgh::Article),
  # en lugar de en el módulo mismo.
  included do
    attr_accessor :author_name
    belongs_to :author, class_name: "User"

    before_validation :set_author

    private
      def set_author
        self.author = User.find_or_create_by(name: author_name)
      end
  end

  def summary
    "#{title}"
  end

  module ClassMethods
    def some_class_method
      'some class method string'
    end
  end
end

6.2 Carga Automática y Motores

Por favor, consulta la guía Carga Automática y Recarga de Constantes para obtener más información sobre la carga automática y los motores.

6.3 Sobrescribiendo Vistas

Cuando Rails busca una vista para renderizar, primero buscará en el directorio app/views de la aplicación. Si no puede encontrar la vista allí, verificará en los directorios app/views de todos los motores que tengan este directorio.

Cuando se le pide a la aplicación que renderice la vista para la acción index de Blorgh::ArticlesController, primero buscará en la ruta app/views/blorgh/articles/index.html.erb dentro de la aplicación. Si no puede encontrarla, buscará dentro del motor.

Puedes sobrescribir esta vista en la aplicación simplemente creando un nuevo archivo en app/views/blorgh/articles/index.html.erb. Luego puedes cambiar completamente lo que esta vista normalmente mostraría.

Prueba esto ahora creando un nuevo archivo en app/views/blorgh/articles/index.html.erb y pon este contenido en él:

<h1>Articles</h1>
<%= link_to "New Article", new_article_path %>
<% @articles.each do |article| %>
  <h2><%= article.title %></h2>
  <small>By <%= article.author %></small>
  <%= simple_format(article.text) %>
  <hr>
<% end %>

6.4 Rutas

Las rutas dentro de un motor están aisladas de la aplicación por defecto. Esto se hace mediante la llamada isolate_namespace dentro de la clase Engine. Esto esencialmente significa que la aplicación y sus motores pueden tener rutas con nombres idénticos y no entrarán en conflicto.

Las rutas dentro de un motor se dibujan en la clase Engine dentro de config/routes.rb, así:

Blorgh::Engine.routes.draw do
  resources :articles
end

Al tener rutas aisladas como esta, si deseas enlazar a un área de un motor desde dentro de una aplicación, necesitarás usar el método proxy de enrutamiento del motor. Las llamadas a métodos de enrutamiento normales como articles_path pueden terminar yendo a ubicaciones no deseadas si tanto la aplicación como el motor tienen tal helper definido.

Por ejemplo, el siguiente ejemplo iría a articles_path de la aplicación si esa plantilla se renderiza desde la aplicación, o a articles_path del motor si se renderiza desde el motor:

<%= link_to "Blog articles", articles_path %>

Para hacer que esta ruta use siempre el método helper de enrutamiento articles_path del motor, debemos llamar al método en el método proxy de enrutamiento que comparte el mismo nombre que el motor.

<%= link_to "Blog articles", blorgh.articles_path %>

Si deseas referenciar la aplicación dentro del motor de manera similar, usa el helper main_app:

<%= link_to "Home", main_app.root_path %>

Si usaras esto dentro de un motor, siempre iría a la raíz de la aplicación. Si dejaras fuera el método "proxy de enrutamiento" main_app, podría potencialmente ir a la raíz del motor o de la aplicación, dependiendo de dónde se llame.

Si una plantilla renderizada desde dentro de un motor intenta usar uno de los métodos helpers de enrutamiento de la aplicación, puede resultar en una llamada a un método indefinido. Si encuentras tal problema, asegúrate de que no estás intentando llamar a los métodos de enrutamiento de la aplicación sin el prefijo main_app desde dentro del motor.

6.5 Activos

Los activos dentro de un motor funcionan de manera idéntica a una aplicación completa. Debido a que la clase del motor hereda de Rails::Engine, la aplicación sabrá buscar activos en los directorios app/assets y lib/assets del motor.

Como todos los demás componentes de un motor, los activos deben estar en un espacio de nombres. Esto significa que si tienes un activo llamado style.css, debería colocarse en app/assets/stylesheets/[engine name]/style.css, en lugar de app/assets/stylesheets/style.css. Si este activo no está en un espacio de nombres, existe la posibilidad de que la aplicación anfitriona pueda tener un activo llamado de manera idéntica, en cuyo caso el activo de la aplicación tendría prioridad y el del motor sería ignorado.

Imagina que tenías un activo ubicado en app/assets/stylesheets/blorgh/style.css. Para incluir este activo dentro de una aplicación, solo usa stylesheet_link_tag y referencia el activo como si estuviera dentro del motor:

<%= stylesheet_link_tag "blorgh/style.css" %>

También puedes especificar estos activos como dependencias de otros activos usando declaraciones de requerimiento de Pipeline de Activos en archivos procesados:

/*
 *= require blorgh/style
 */

Recuerda que para usar lenguajes como Sass o CoffeeScript, deberías agregar la biblioteca relevante al .gemspec de tu motor.

6.6 Activos Separados y Precompilación

Hay algunas situaciones en las que los activos de tu motor no son necesarios para la aplicación anfitriona. Por ejemplo, supongamos que has creado una funcionalidad de administración que solo existe para tu motor. En este caso, la aplicación anfitriona no necesita requerir admin.css o admin.js. Solo el diseño de administración del gem necesita estos activos. No tiene sentido que la aplicación anfitriona incluya "blorgh/admin.css" en sus hojas de estilo. En esta situación, deberías definir explícitamente estos activos para la precompilación. Esto le dice a Sprockets que agregue tus activos del motor cuando se active bin/rails assets:precompile.

Puedes definir activos para la precompilación en engine.rb:

initializer "blorgh.assets.precompile" do |app|
  app.config.assets.precompile += %w( admin.js admin.css )
end

Para obtener más información, lee la guía del Pipeline de Activos.

6.7 Otras Dependencias de Gemas

Las dependencias de gemas dentro de un motor deben especificarse dentro del archivo .gemspec en la raíz del motor. La razón es que el motor puede instalarse como un gem. Si las dependencias se especificaran dentro del Gemfile, no serían reconocidas por una instalación de gem tradicional y, por lo tanto, no se instalarían, causando que el motor no funcione correctamente.

Para especificar una dependencia que debería instalarse con el motor durante una instalación de gem tradicional, especifícala dentro del bloque Gem::Specification dentro del archivo .gemspec en el motor:

s.add_dependency "moo"

Para especificar una dependencia que solo debería instalarse como una dependencia de desarrollo de la aplicación, especifícala así:

s.add_development_dependency "moo"

Ambos tipos de dependencias se instalarán cuando se ejecute bundle install dentro de la aplicación. Las dependencias de desarrollo para el gem solo se usarán cuando se ejecuten el desarrollo y las pruebas para el motor.

Ten en cuenta que si deseas requerir inmediatamente dependencias cuando se requiera el motor, debes requerirlas antes de la inicialización del motor. Por ejemplo:

require "other_engine/engine"
require "yet_another_engine/engine"

module MyEngine
  class Engine < ::Rails::Engine
  end
end

Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.