ChatGPT解决这个技术问题 Extra ChatGPT

Ruby on Rails: Where to define global constants?

I'm just getting started with my first Ruby on Rails webapp. I've got a bunch of different models, views, controllers, and so on.

I'm wanting to find a good place to stick definitions of truly global constants, that apply across my whole app. In particular, they apply both in the logic of my models, and in the decisions taken in my views. I cannot find any DRY place to put these definitions where they're available both to all my models and also in all my views.

To take a specific example, I want a constant COLOURS = ['white', 'blue', 'black', 'red', 'green']. This is used all over the place, in both models and views. Where can I define it in just one place so that it's accessible?

What I've tried:

Constant class variables in the model.rb file that they're most associated with, such as @@COLOURS = [...]. But I couldn't find a sane way to define it so that I can write in my views Card.COLOURS rather than something kludgy like Card.first.COLOURS.

A method on the model, something like def colours ['white',...] end - same problem.

A method in application_helper.rb - this is what I'm doing so far, but the helpers are only accessible in views, not in models

I think I might have tried something in application.rb or environment.rb, but those don't really seem right (and they don't seem to work either)

Is there just no way to define anything to be accessible both from models and from views? I mean, I know models and views should be separate, but surely in some domains there'll be times they need to refer to the same domain-specific knowledge?

I appreciate that this is REALLY late, but for other readers I wonder why you didn't just define them in your model and use your controllers to pass them to your views. In this way, you'd have a cleaner separation of concerns - rather than creating dependencies between controller/view AND model/view.
@TomTom: Pass these constants into each view and helper that needs them? In other words, make the controller aware of which views need which constants? That sounds like more of a violation of MVC.

H
Holger Just

If your model is really "responsible" for the constants you should stick them there. You can create class methods to access them without creating a new object instance:

class Card < ActiveRecord::Base
  def self.colours
    ['white', 'blue']
  end
end

# accessible like this
Card.colours

Alternatively, you can create class variables and an accessor. This is however discouraged as class variables might act kind of surprising with inheritance and in multi-thread environments.

class Card < ActiveRecord::Base
  @@colours = ['white', 'blue'].freeze
  cattr_reader :colours
end

# accessible the same as above
Card.colours

The two options above allow you to change the returned array on each invocation of the accessor method if required. If you have true a truly unchangeable constant, you can also define it on the model class:

class Card < ActiveRecord::Base
  COLOURS = ['white', 'blue'].freeze
end

# accessible as
Card::COLOURS

You could also create global constants which are accessible from everywhere in an initializer like in the following example. This is probably the best place, if your colours are really global and used in more than one model context.

# put this into config/initializers/my_constants.rb
COLOURS = ['white', 'blue'].freeze

# accessible as a top-level constant this time
COLOURS

Note: when we define constants above, often we want to freeze the array. That prevents other code from later (inadvertently) modifying the array by e.g. adding a new element. Once an object is frozen, it can't be changed anymore.


Thank you very much. Looks like I was missing the Ruby class-fu to define the class methods. But I actually like the initializer option in this case, because the colours are used in multiple models and views. Many thanks!
If going the config/initializers/my_constants.rb route, remember to restart the server: touch tmp/restart.txt
The def self.colours example is not ideal. Each time you call def self.colours, a new instance of the array will be returned. #freeze will not help in this case. Best practice is to declare it as a Ruby constant, in which case you will always get back the same object.
@Zabba If the allocation of a single array makes a noticable difference for your app, you probably shouldn't be using Ruby in the first place... That said, using a method and returning a completely new array each time can have a couple of advantages: (1) it's the closest thing you can get towards immutable objects on your class boundary in Ruby and (2) you keep a uniform interface on your class with the possibility to adapt the return value later based on inherent state (e.g. by reading the colours from the DB) without changing the interface.
@Holger Just, at least one of your aims can still be achieved using a constant: class Card; COLOURS = ['white', 'blue'].freeze; def self.colours; COLOURS; end; end That said, allocating of an array in any language can be potentially problematic; for one, it is using memory for no (good) reason. If loading from a DB, and want to cache the value, one can also use a class instance variable, which can be lazy loaded by using the def self.colours method. Agreed about the immutability aspect though.
Z
Zabba

Some options:

Using a constant:

class Card
  COLOURS = ['white', 'blue', 'black', 'red', 'green', 'yellow'].freeze
end

Lazy loaded using class instance variable:

class Card
  def self.colours
    @colours ||= ['white', 'blue', 'black', 'red', 'green', 'yellow'].freeze
  end
end

If it is a truly global constant (avoid global constants of this nature, though), you could also consider putting a top-level constant in config/initializers/my_constants.rb for example.


Heh. Fair comment - syntax error when typing-from-memory my example :) Thanks for the tip!
Then extend the module in the class so it'll be available with Card.COLOURS.
When using extend its not working for me. When using include I can access like: Card::COLOURS
You should definitely NOT place this under /models. It's a lot better if you create an initializer.
@linkyndy I'd say it's ok to put it under /models, but only if its inside a module, e.g. module Constants; COLOURS = ...; end in a file called models/constants.rb.
H
Halil Özgür

As of Rails 4.2, you can use the config.x property:

# config/application.rb (or config/custom.rb if you prefer)
config.x.colours.options = %w[white blue black red green]
config.x.colours.default = 'white'

Which will be available as:

Rails.configuration.x.colours.options
# => ["white", "blue", "black", "red", "green"]
Rails.configuration.x.colours.default
# => "white"

Another method of loading custom config:

# config/colours.yml
default: &default
  options:
    - white
    - blue
    - black
    - red
    - green
  default: white
development:
  *default
production:
  *default
# config/application.rb
config.colours = config_for(:colours)
Rails.configuration.colours
# => {"options"=>["white", "blue", "black", "red", "green"], "default"=>"white"}
Rails.configuration.colours['default']
# => "white"

In Rails 5 & 6, you can use the configuration object directly for custom configuration, in addition to config.x. However, it can only be used for non-nested configuration:

# config/application.rb
config.colours = %w[white blue black red green]

It will be available as:

Rails.configuration.colours
# => ["white", "blue", "black", "red", "green"]

I like Rails.configuration.colours best (though I wish it wasn't so long)
@TomRossi I agree, e.g. config is as good as configuration. We might hope to get a shortcut at some point :)
is this still the best way in rails 6 to define constants to be shared across multiple controllers? thanks for the answer!
@Crashalot It's still listed in the docs. "The best"? It depends. It can be in their common ancestor. Or in ApplicationController if there is nothing else in between. If the constant isn't directly related to controllers, I'd still consider a global config, etc.
@HalilÖzgür thanks for the reply. how do you define constants in a common ancestor?
j
jacklin

If a constant is needed in more than one class, I put it in config/initializers/constant.rb always in all caps (list of states below is truncated).

STATES = ['AK', 'AL', ... 'WI', 'WV', 'WY']

They are available through out the application except in model code as such:

    <%= form.label :states, %>
    <%= form.select :states, STATES, {} %>

To use the constant in a model, use attr_accessor to make the constant available.

class Customer < ActiveRecord::Base
    attr_accessor :STATES

    validates :state, inclusion: {in: STATES, message: "-- choose a State from the drop down list."}
end

nice, config/initializers/constants.rb probably would be a better choice though
i also use this, but recently came across the issue that these constants are not accessible in application.rb
my constants were working but stopped for some reason (As somehow my file moved out of initializers). After checking this answer I closely looked and moved them back and Now working. Thanks
I don't think attr_accessor is needed. Are you talking about any particular Rails version?
R
Riccardo

For application-wide settings and for global constants I recommend to use Settingslogic. This settings are stored in YML file and can be accessed from models, views and controllers. Furthermore, you can create different settings for all your environments:

  # app/config/application.yml
  defaults: &defaults
    cool:
      sweet: nested settings
    neat_setting: 24
    awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %>

    colors: "white blue black red green"

  development:
    <<: *defaults
    neat_setting: 800

  test:
    <<: *defaults

  production:
    <<: *defaults

Somewhere in the view (I prefer helper methods for such kind of stuff) or in a model you can get, for ex., array of colors Settings.colors.split(/\s/). It's very flexible. And you don't need to invent a bike.


G
Ghanshyam Anand

Try to keep all constant at one place, In my application I have created constants folder inside initializers as follows:

https://i.stack.imgur.com/UODRT.png

and I usually keep all constant in these files.

In your case you can create file under constants folder as colors_constant.rb

colors_constant.rb

https://i.stack.imgur.com/2X2Ny.png

Don't forgot to restart server


S
Steve Ross

Use a class method:

def self.colours
  ['white', 'red', 'black']
end

Then Model.colours will return that array. Alternatively, create an initializer and wrap the constants in a module to avoid namespace conflicts.


G
Greg Hurrell

Another option, if you want to define your constants in one place:

module DSL
  module Constants
    MY_CONSTANT = 1
  end
end

But still make them globally visible without having to access them in fully qualified way:

DSL::Constants::MY_CONSTANT # => 1
MY_CONSTANT # => NameError: uninitialized constant MY_CONSTANT
Object.instance_eval { include DSL::Constants }
MY_CONSTANT # => 1

D
Dennis

A common place to put application-wide global constants is inside config/application.

module MyApp
  FOO ||= ENV.fetch('FOO', nil)
  BAR ||= %w(one two three)

  class Application < Rails::Application
    config.foo_bar = :baz
  end
end

S
SriSri

I typically have a 'lookup' model/table in my rails program and use it for the constants. It is very useful if the constants are going to be different for different environments. In addition, if you have a plan to extend them, say you want to add 'yellow' on a later date, you could simply add a new row to the lookup table and be done with it.

If you give the admin permissions to modify this table, they will not come to you for maintenance. :) DRY.

Here is how my migration code looks like:

class CreateLookups < ActiveRecord::Migration
  def change
    create_table :lookups do |t|
      t.string :group_key
      t.string :lookup_key
      t.string :lookup_value
      t.timestamps
    end
  end
end

I use seeds.rb to pre-populate it.

Lookup.find_or_create_by_group_key_and_lookup_key_and_lookup_value!(group_key: 'development_COLORS', lookup_key: 'color1', lookup_value: 'red');

b
bird

The global variable should be declare in config/initializers directory

COLOURS = %w(white blue black red green)

Thanks! Others have mentioned this already. It's the last line of Holger's answer, and Zabba mentions this technique as well, though Zabba warns against it.
f
fangxing

According your condition, you can also define some environmental variables, and fetch it via ENV['some-var'] in ruby code, this solution may not fit for you, but I hope it may help others.

Example: you can create different files .development_env, .production_env, .test_env and load it according your application environments, check this gen dotenv-rails which automate this for your.


H
Hà Tiến Đạt

I think you can use gem config

https://github.com/rubyconfig/config

Easy handle and edit