V2.2: Overview


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

module Bookshelf
  module Books
    class Create < Bookshelf::Operation
      def call
      end
    end
  end
end

From here, you can build your flow of steps using step. For example:

# frozen_string_literal: true

module Bookshelf
  module Books
    class Create < Bookshelf::Operation
      def call(attrs)
        attrs = step validate(attrs)
        book = step create(attrs)
        step update_feeds(book)

        book
      end

      private

      def validate(attrs)
        # Return Success(attrs) or Failure(some_error)
      end

      def create(attrs)
        # Return Success(book) or Failure(some_error)
      end

      def update_feeds(book)
        # 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

module Bookshelf
  module Books
    class Create < Bookshelf::Operation
      include Deps["repos.book_repo"]

      # ...

      private

      def create(attrs)
        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.

def call
  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.

module Bookshelf
  module Actions
    class Create < Bookshelf::Action
      include Deps["books.create"]

      def handle(request, response)
        case create.call(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.