Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Do You Need That Validation? Let Me Call You Ba...

Do You Need That Validation? Let Me Call You Back About It

Rails apps start nice and cute. Fast forward a year and business logic and view logic are entangled in our validations and callbacks - getting in our way at every turn. Wasn’t this supposed to be easy?

Let’s explore different approaches to improve the situation and untangle the web.

Tobias Pfeiffer

February 24, 2019
Tweet

More Decks by Tobias Pfeiffer

Other Decks in Programming

Transcript

  1. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  2. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  3. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  4. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end Datetime fields
  5. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  6. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  7. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  8. event = Event.new( name: "Ruby On Ice", location: "Tegernsee", date:

    "24.02.2019", crew_arrives_at: "6:45", performers_arrive_at: "9:30", open_at: "9:30", starts_at: "10:00", ends_at: "16:00" ) event.valid?
  9. #<Event: name: "Ruby On Ice", location: "Tegernsee", date: Sun, 24

    Feb 2019, crew_arrives_at: Sun, 24 Feb 2019 6:45:00, performers_arrive_at: Sun, 24 Feb 2019 9:00:00, open_at: Sun, 24 Feb 2019 9:00:00, starts_at: Sun, 24 Feb 2019 9:30:00, ends_at: Sun, 24 Feb 2019 20:00:00> It works!
  10. let(:event) do build :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45)

    end it "works" do p event.ends_at # Wed, 01 Jan 2042 15:45:00 event.save! p event.ends_at # Sun, 24 Feb 2019 15:45:00 end Have fun debugging!
  11. let(:event) do create :event, ends_at: Time.zone.local(2042, 1, 1, 15, 45)

    end it "retrieves the right events" do query = FutureEvents.new expect(query.call(23.years)).to include(event) end Have fun debugging!
  12. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end
  13. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end Smell
  14. class Event < ApplicationRecord before_validation :set_datetimes_to_date def set_datetimes_to_date base_date =

    date.to_datetime DATE_TIME_FIELDS.each do |time_attribute| original = public_send(time_attribute) if original adjusted_time = base_date.change hour: original.hour, min: original.min self.public_send("#{time_attribute}=", adjusted_time) end end end end Why does the model clean up
  15. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? gitlab/user
  16. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? gitlab/user
  17. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  18. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  19. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } Ways to change?
  20. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  21. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } gitlab/user
  22. Registrations User Users Users ##. ##. ##. ##. ##. ##.

    ##. ##. Business Logic Separated Validations and Callbacks still mixed
  23. Registrations User Users Users ##. ##. ##. ##. ##. ##.

    ##. ##. Business Logic Separated Validations and Callbacks still mixed Run all the time by default
  24. Registrations User Users Users ##. ##. ##. ##. ##. ##.

    ##. ##. SignUp Show Index ##. ##. ##.
  25. class User < ApplicationRecord validates :email, presence: true, confirmation: true

    validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end A User Model
  26. class User < ApplicationRecord validates :email, presence: true, confirmation: true

    validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end Sign Up / Edit Only
  27. class User < ApplicationRecord validates :email, presence: true, confirmation: true

    validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end View Related
  28. class User < ApplicationRecord validates :email, presence: true, confirmation: true

    validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end WHWWHHYYY???
  29. Do You Need That Validation? Let Me Call You Back

    About It Tobias Pfeiffer @PragTob pragtob.info
  30. class User < ApplicationRecord validates :email, presence: true, confirmation: true

    validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end ActiveRecord Original
  31. module UserRegistration extend ActiveSupport#:Concern included do validates :email, presence: true,

    confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create end end Concerns
  32. module Copyable def copy_to(destination) Notification.suppress do # Copy logic that

    creates new # comments that we do not want # triggering notifications. end end end Suppress
  33. class Person < ApplicationRecord validates :email, uniqueness: true, on: :account_setup

    validates :age, numericality: true, on: :account_setup end Custom Contexts
  34. Form Objects Form Objects Form Objects class Registration include ActiveModel#:Model

    validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password def save if valid? user = BaseUser.new(email: email, password_digest: hash_password) user.save! send_welcome_email true else false end end end
  35. Form Objects Form Objects Plain ActiveModel class Registration include ActiveModel#:Model

    validates :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password def save if valid? user = BaseUser.new(email: email, password_digest: hash_password) user.save! send_welcome_email true else false end end end
  36. Form Objects Form Objects Validations class Registration include ActiveModel#:Model validates

    :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password ###. end
  37. Form Objects Form Objects Attributes class Registration include ActiveModel#:Model validates

    :email, presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :email, :password ###. end
  38. Form Objects Form Objects Map to ActiveRecord class Registration ###.

    def save if valid? user = BaseUser.new( email: email, password_digest: hash_password ) user.save! send_welcome_email true else false end end
  39. Form Objects Form Objects Interface class Registration ###. def save

    if valid? user = BaseUser.new( email: email, password_digest: hash_password ) user.save! send_welcome_email true else false end end
  40. Form Objects Form Objects Callbacks class Registration ###. def save

    if valid? user = BaseUser.new( email: email, password_digest: hash_password ) user.save! send_welcome_email true else false end end
  41. Inheritance! class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation:

    true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  42. Inheritance! class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation:

    true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  43. ActiveType class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence: true, confirmation:

    true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  44. class User < ApplicationRecord validates :email, presence: true, confirmation: true

    validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end Original
  45. Almost the same! class User#:AsSignUp < ActiveType#:Record[User] validates :email, presence:

    true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  46. Handle STI, Routes etc. class User#:AsSignUp < ActiveType#:Record[User] validates :email,

    presence: true, confirmation: true validates :password, confirmation: true, length: { minimum: 8 } validates :terms, acceptance: true attr_accessor :password before_save :hash_password after_commit :send_welcome_email, on: :create ###. end
  47. Changesets defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  48. Elixir defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  49. Pipe defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  50. Context defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  51. “strong parameters” defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs)

    do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  52. Validations defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  53. callback defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  54. Mixing concerns defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs)

    do user |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  55. validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email,

    if: :public_email_changed? validate :owns_commit_email, if: :commit_email_changed? before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_validation :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :set_commit_email, if: :commit_email_changed? before_save :skip_reconfirmation!, if: #>(user) { user.email_changed? #& user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: #>(user) { user.email_changed? #& !user.new_record? } Remember this?
  56. Combinable defmodule ValidationShowcase.Accounts.User do # ... def registration_changeset(user, attrs) do

    user |> base_changeset(attrs) |> cast(attrs, [:email, :password, :terms_of_service]) |> validate_required([:email, :password]) |> validate_confirmation(:email) |> validate_confirmation(:password) |> validate_length(:password, min: 8) |> validate_acceptance(:terms_of_service) |> hash_password() end end
  57. defmodule ValidationShowcase.Accounts do def create_user(attrs \\ %{}) do %User{} |>

    User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end Context
  58. defmodule ValidationShowcase.Accounts do def create_user(attrs \\ %{}) do %User{} |>

    User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end Changeset
  59. defmodule ValidationShowcase.Accounts do def create_user(attrs \\ %{}) do %User{} |>

    User.registration_changeset(attrs) |> Repo.insert() |> send_welcome_email() end end “after_commit”
  60. defmodule ValidationShowcaseWeb.UserController do def create(conn, %{"user" => user_params}) do case

    Accounts.create_user(user_params) do {:ok, user} -> conn |> put_flash(:info, "User created successfully.") |> redirect(to: Routes.user_path(conn, :show, user)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end end Controller
  61. class TbRegistrationsController < ApplicationController def create result = Registration#:Create.(params: params)

    if result.success? redirect_to "/users/", notice: 'User was created.' else @user = result["contract.default"] render "users/new" end end end
  62. class TbRegistrationsController < ApplicationController def create result = Registration#:Create.(params: params)

    if result.success? redirect_to "/users/", notice: 'User was created.' else @user = result["contract.default"] render "users/new" end end end Operation
  63. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end
  64. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Setup Model
  65. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Setup Form Object
  66. module Registration#:Contract class Create < Reform#:Form include Dry include Reform#:Form#:ActiveModel

    feature Coercion model :tb_registration property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  67. module Registration#:Contract class Create < Reform#:Form include Dry include Reform#:Form#:ActiveModel

    feature Coercion model :tb_registration property :email property :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  68. module Registration#:Contract class Create < Reform#:Form property :email property :email_confirmation,

    virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  69. Attributes module Registration#:Contract class Create < Reform#:Form property :email property

    :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size?: 8).confirmation required(:terms).value(:true?) end end end
  70. dry-validation module Registration#:Contract class Create < Reform#:Form property :email property

    :email_confirmation, virtual: true property :password, virtual: true property :password_confirmation, virtual: true property :terms, virtual: true, type: Types#:Params#:Bool validation do required(:email).filled.confirmation required(:password).value(min_size#: 8).confirmation required(:terms).value(:true?) end end end
  71. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end validate
  72. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Callback
  73. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Persist
  74. class Registration#:Create < Trailblazer#:Operation step Model(BaseUser, :new) step Contract#:Build( constant:

    Registration#:Contract#:Create ) step Contract#:Validate(key: :tb_registration) step :hash_password step Contract#:Persist() step :send_welcome_email end Callback
  75. “Models are persistence-only and solely define associations and scopes. No

    business code is to be found here. No validations, no callbacks.” trailblazer
  76. The architecture eases keeping the business logic (entities) separated from

    details such as persistence or validations. hanami/model