Introduction
Operations organize business logic in Hanami apps. They are the foundation of your app’s "service layer".
Operations are built using dry-operation.
With operations, you can model your logic as a linear flow of steps, each returning a Success or Failure. If all steps of an operation succeed, the operation completes and returns its final value as a success. If any step returns a failure, execution short circuits and returns that failure immediately.
To create an operation, run hanami generate operation:
$ bundle exec hanami generate operation books.create
This will give you the following:
# app/books/create.rb
end
end
end
end
From here, you can build your flow of steps using step. For example:
# frozen_string_literal: true
attrs = step validate(attrs)
book = step create(attrs)
step update_feeds(book)
book
end
private
# Return Success(attrs) or Failure(some_error)
end
# Return Success(book) or Failure(some_error)
end
# Return Success or Failure
end
end
end
end
Operations can work with dependencies from across your app. To include dependencies, use the Deps mixin.
For example:
# frozen_string_literal: true
include Deps["repos.book_repo"]
# ...
private
Success(book_repo.create(attrs))
end
end
end
end
To learn more about operations, see the dry-operation documentation.
Database transactions
Operations provide a #transaction block method that integrates with the databases in your app. Any step failure inside the transaction block will roll back the transaction as well as short circuiting the operation.
transaction do
attrs = step validate(attrs)
book = step create(attrs)
step update_feeds(book)
book
end
end
By default, transaction uses your "default" gateway. To use a different one, specify gateway: followed by the desired gateway name.
transaction(gateway: :other) do
# ...
end
Working with operations
Typically, operations will be called from places like actions. Such an arrangement allows you to keep your business logic well contained, and your actions focused on HTTP responsibilities only.
After calling an operation, you will receive either a Success or a Failure. You can pattern match on this result to handle each situation.
include Deps["books.create"]
case create.call(request.params[:book])
in Success(book)
response.redirect_to routes.path(:book, book.id)
in Failure[:invalid, validation]
response.render view, validation:
end
end
end
end
end
This pattern matching allows you to handle different types of failures in a clear and explicit manner.
Operations' Success and Failure results come from dry-monads. To learn more about working with results, see the dry-monads result documentation.