use SL::DB::Helper::Attr;
 use SL::DB::Helper::Metadata;
 use SL::DB::Helper::Manager;
+use SL::DB::Object::Hooks;
 
 use base qw(Rose::DB::Object);
 
   return $check ? $self->$sub(@_) : $self;
 }
 
+# These three functions cannot sit in SL::DB::Object::Hooks because
+# mixins don't deal well with super classes (SUPER is the current
+# package's super class, not $self's).
+sub load {
+  my ($self, @args) = @_;
+
+  SL::DB::Object::Hooks::run_hooks($self, 'before_load');
+  my $result = $self->SUPER::load(@args);
+  SL::DB::Object::Hooks::run_hooks($self, 'after_load', $result);
+
+  return $result;
+}
+
+sub save {
+  my ($self, @args) = @_;
+  my $worker        = sub {
+    SL::DB::Object::Hooks::run_hooks($self, 'before_save');
+    my $result = $self->SUPER::save(@args);
+    SL::DB::Object::Hooks::run_hooks($self, 'after_save', $result);
+  };
+
+  return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+}
+
+sub delete {
+  my ($self, @args) = @_;
+  my $worker        = sub {
+    SL::DB::Object::Hooks::run_hooks($self, 'before_delete');
+    my $result = $self->SUPER::delete(@args);
+    SL::DB::Object::Hooks::run_hooks($self, 'after_delete', $result);
+  };
+
+  return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+}
+
 1;
 
 __END__
 
--- /dev/null
+package SL::DB::Object::Hooks;
+
+use strict;
+
+use SL::X;
+
+use parent qw(Exporter);
+our @EXPORT = qw(before_load   after_load
+                 before_save   after_save
+                 before_delete after_delete);
+
+my %hooks;
+
+# Adding hooks
+
+sub before_save {
+  _add_hook('before_save', @_);
+}
+
+sub after_save {
+  _add_hook('after_save', @_);
+}
+
+sub before_load {
+  _add_hook('before_load', @_);
+}
+
+sub after_load {
+  _add_hook('after_load', @_);
+}
+
+sub before_delete {
+  _add_hook('before_delete', @_);
+}
+
+sub after_delete {
+  _add_hook('after_delete', @_);
+}
+
+# Running hooks
+
+sub run_hooks {
+  my ($object, $when, @args) = @_;
+
+  foreach my $sub (@{ ( $hooks{$when} || { })->{ ref($object) } || [ ] }) {
+    my $result = ref($sub) eq 'CODE' ? $sub->($object, @args) : $object->call_sub($sub, @args);
+    SL::X::DBHookError->throw(error => "${when} hook '" . (ref($sub) eq 'CODE' ? '<anonymous sub>' : $sub) . "' failed") if !$result;
+  }
+}
+
+# Internals
+
+sub _add_hook {
+  my ($when, $class, $sub_name, $code) = @_;
+  $hooks{$when}           ||= { };
+  $hooks{$when}->{$class} ||= [ ];
+  push @{ $hooks{$when}->{$class} }, $sub_name;
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Object::Hooks - Hooks that are run before/after a
+load/save/delete
+
+=head1 SYNOPSIS
+
+Hooks are functions that are called before or after an object is
+loaded, saved or deleted. The package defines the hooks, and those
+hooks themselves are run as instance methods.
+
+Hooks are run in the order they're added.
+
+Hooks must return a trueish value in order to continue processing. If
+any hook returns a falsish value then an exception (instance of
+C<SL::X::DBHookError>) is thrown. However, C<SL::DB::Object> usually
+runs the hooks from within a transaction, catches the exception and
+only returns falsish in error cases.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<before_load $sub>
+
+=item C<before_save $sub>
+
+=item C<before_delete $sub>
+
+=item C<after_load $sub>
+
+=item C<after_save $sub>
+
+=item C<after_delete $sub>
+
+Adds a new hook that is called at the appropriate time. C<$sub> can be
+either a name of an existing sub or a code reference. If it is a code
+reference then the then-current C<$self> will be passed as the first
+argument.
+
+C<before> hooks are called without arguments.
+
+C<after> hooks are called with a single argument: the result of the
+C<save> or C<delete> operation.
+
+=item C<run_hooks $object, $when, @args>
+
+Runs all hooks for the object C<$object> that are defined for
+C<$when>. C<$when> is the same as one of the C<before_xyz> or
+C<after_xyz> function names above.
+
+An exception of C<SL::X::DBHookError> is thrown if any of the hooks
+returns a falsish value.
+
+This function is supposed to be called by L</SL::DB::Object::load>,
+L</SL::DB::Object::save> or L</SL::DB::Object::delete>.
+
+=back
+
+=head1 EXPORTS
+
+This mixin exports the functions L</before_load>, L</after_load>,
+L</before_save>, L</after_save>, L</before_delete>, L</after_delete>.
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut