I've multiple issues to load / require classes under my app/services
folder in a Rails 5 project and I'm starting to give up on this issue.
First of all and to be clear, services/
are simple PORO classes I use throughout my project to abstract most of the business logic from the controllers, models, etc.
The tree looks like this
app/
services/
my_service/
base.rb
funny_name.rb
my_service.rb
models/
funny_name.rb
First, when I tried to use MyService.const_get('FunnyName')
it got FunnyName
from my models directory. It does not seem to have the same behavior when I do MyService::FunnyName
directly though, in most of my tests and changes this was working fine, it's odd.
I realised Rails config.autoload_paths
does not load things recursively ; it would makes sense that the first FunnyName
to be catch is the models/funny_name.rb
because it's definitely loaded but not the other.
That's ok, let's find a workaround. I added this to my application.rb
:
config.autoload_paths += Dir[Rails.root.join('app', 'services', '**/')]
Which will add all the subdirectories of services into config.autoload_paths
. Apparently it's not recommended to write things like that since Rails 5 ; but the idea does look right to me.
Now, when I start my application it crashes and output something like this
Unable to autoload constant Base, expected /.../backend/app/services/my_service/base.rb to define it (LoadError)
Names were changed but it's the matching path from the tree I wrote previously
The thing is, base.rb
is defined in the exact file the error leads me, which contains something like
class MyService
class Base
end
end
So I try other workaround, lots of them, nothing ever works. So I end up totally removing the autoload_paths
and add this directly in the application.rb
Dir[Rails.root.join('app', 'services', '**', '*.rb')].each { |file| require file }
Now the base.rb
is correctly loaded, the MyService.const_get('FunnyName')
will actually return the correct class and everything works, but it's a disgusting workaround. Also, it has yet not been tested in production
but it might create problems depending the environment.
Requiring the whole tree from the application.rb
sounds like a bad idea and I don't think it can be kept this way.
What's the cleanest way to add custom services/
directory in Rails ? It contains multiple subdirectories and classes with simple names which are also present in other parts of the app (models, base.rb
, etc.)
How do you avoid confusing the autoload_paths
? Is there something else I don't know which could do the trick ? Why did base.rb
even crash here ?
After deeper investigation and attempts, I realised that I had to eager_load
the services to avoid getting wrong constants when calling meta functionalities such as const_get('MyClassWithModelName')
.
But here's is the thing : the classic eager_load_paths
won't work because for some reason those classes will apparently be loaded before the entire core of Rails is initialized, and simple class names such as Base
will actually be mixed up with the core, therefore make everything crash.
Some could say "then rename Base into something else" but should I change a class name wrapped into a namespace because Rails tell me to ? I don't think so. Class names should be kept simple, and what I do inside a custom namespace is no concern of Rails.
I had to think it through and write down my own hook of Rails configuration. We load the core and all its functionalities and then service/
recursively.
On a side note, it won't add any weight to the production environment, and it's very convenient for development.
Place this in config/environment/development.rb
and all other environment you want to eager load without Rails class conflicts (such as test.rb
in my case)
# we eager load all services and subdirectories after Rails itself has been initializer
# why not use `eager_load_paths` or `autoload_paths` ? it makes conflict with the Rails core classes
# here we do eager them the same way but afterwards so it never crashes or has conflicts.
# see `initializers/after_eager_load_paths.rb` for more details
config.after_eager_load_paths = Dir[Rails.root.join('app', 'services', '**/')]
Then create a new file initializers/after_eager_load_paths.rb
containing this
# this is a customized eager load system
# after Rails has been initialized and if the `after_eager_load_paths` contains something
# we will go through the directories recursively and eager load all ruby files
# this is to avoid constant mismatch on startup with `autoload_paths` or `eager_load_paths`
# it also prevent any autoload failure dû to deep recursive folders with subclasses
# which have similar name to top level constants.
Rails.application.configure do
if config.respond_to?(:after_eager_load_paths) && config.after_eager_load_paths.instance_of?(Array)
config.after_initialize do
config.after_eager_load_paths.each do |path|
Dir["#{path}/*.rb"].each { |file| require file }
end
end
end
end
Works like a charm. You can also change require
by load
if you need it.
When I do this (which is in all of my projects), it looks something like this:
app
|- services
| |- sub_service
| | |- service_base.rb
| | |- useful_service.rb
| |- service_base.rb
I put all common method definitions in app/services/service_base.rb
:
app/services/service_base.rb
class ServiceBase
attr_accessor *%w(
args
).freeze
class < self
def call(args={})
new(args).call
end
end
def initialize(args)
@args = args
end
end
I put any methods common to the sub_services
in app/services/sub_service/service_base.rb
:
app/services/sub_service/service_base.rb
class SubService::ServiceBase < ServiceBase
def call
end
private
def a_subservice_method
end
end
And then any unique methods in useful_service
:
app/services/sub_service/useful_service.rb
class SubService::UsefulService < SubService::ServiceBase
def call
a_subservice_method
a_useful_service_method
end
private
def a_useful_service_method
end
end
Then, I can do something like:
SubService::UsefulService.call(some: :args)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With