diff --git a/docs/index.rst b/docs/index.rst
index 7921a68c4..f064f1294 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -603,11 +603,11 @@ You can also add hooks directly in your factory class:
 
 Read `Initialization`_ to learn more about the ``initialize()`` method.
 
-Events
-~~~~~~
+Hooks as service / global hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-In addition to hooks, Foundry also leverages `symfony/event-dispatcher` and dispatches events that you can listen to,
-allowing to create hooks globally, as Symfony services:
+For a better control of your hooks, you can define them as services, allowing to leverage dependency injection and
+to create hooks globally:
 
 ::
 
@@ -616,26 +616,26 @@ allowing to create hooks globally, as Symfony services:
     use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
     use Zenstruck\Foundry\Persistence\Event\AfterPersist;
 
-    final class FoundryEventListener
+    final class FoundryHook
     {
-        #[AsEventListener]
+        #[AsFoundryHook(Post::class)]
         public function beforeInstantiate(BeforeInstantiate $event): void
         {
-            // do something before the object is instantiated:
+            // do something before the post is instantiated:
             // $event->parameters is what will be used to instantiate the object, manipulate as required
             // $event->objectClass is the class of the object being instantiated
             // $event->factory is the factory instance which creates the object
         }
 
-        #[AsEventListener]
+        #[AsFoundryHook(Post::class)]
         public function afterInstantiate(AfterInstantiate $event): void
         {
-            // $event->object is the instantiated object
+            // $event->object is the instantiated Post object
             // $event->parameters contains the attributes used to instantiate the object and any extras
             // $event->factory is the factory instance which creates the object
         }
 
-        #[AsEventListener]
+        #[AsFoundryHook(Post::class)]
         public function afterPersist(AfterPersist $event): void
         {
             // this event is only called if the object was persisted
@@ -643,11 +643,17 @@ allowing to create hooks globally, as Symfony services:
             // $event->parameters contains the attributes used to instantiate the object and any extras
             // $event->factory is the factory instance which creates the object
         }
+
+        #[AsFoundryHook]
+        public function afterInstantiateGlobal(AfterInstantiate $event): void
+        {
+            // Omitting class defines a "global" hook which will be called for all objects
+        }
     }
 
 .. versionadded::  2.4
 
-    Those events are triggered since Foundry 2.4.
+    The ``#[AsFoundryHook]`` attribute was added in Foundry 2.4.
 
 Initialization
 ~~~~~~~~~~~~~~
diff --git a/src/Attribute/AsFoundryHook.php b/src/Attribute/AsFoundryHook.php
new file mode 100644
index 000000000..68ff14e16
--- /dev/null
+++ b/src/Attribute/AsFoundryHook.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the zenstruck/foundry package.
+ *
+ * (c) Kevin Bond <kevinbond@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\Attribute;
+
+use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
+
+#[\Attribute(\Attribute::TARGET_METHOD)]
+final class AsFoundryHook extends AsEventListener
+{
+    public function __construct(
+        /** @var class-string */
+        public readonly ?string $objectClass = null,
+        int $priority = 0,
+    ) {
+        parent::__construct(priority: $priority);
+    }
+}
diff --git a/src/Object/Event/AfterInstantiate.php b/src/Object/Event/AfterInstantiate.php
index 7bbef74f5..356256531 100644
--- a/src/Object/Event/AfterInstantiate.php
+++ b/src/Object/Event/AfterInstantiate.php
@@ -19,16 +19,25 @@
 /**
  * @author Nicolas PHILIPPE <nikophil@gmail.com>
  *
+ * @template T of object
+ * @implements Event<T>
+ *
  * @phpstan-import-type Parameters from Factory
  */
-final class AfterInstantiate
+final class AfterInstantiate implements Event
 {
     public function __construct(
+        /** @var T */
         public readonly object $object,
         /** @phpstan-var Parameters */
         public readonly array $parameters,
-        /** @var ObjectFactory<object> */
+        /** @var ObjectFactory<T> */
         public readonly ObjectFactory $factory,
     ) {
     }
+
+    public function objectClassName(): string
+    {
+        return $this->object::class;
+    }
 }
diff --git a/src/Object/Event/BeforeInstantiate.php b/src/Object/Event/BeforeInstantiate.php
index b5ad5cb38..a79174056 100644
--- a/src/Object/Event/BeforeInstantiate.php
+++ b/src/Object/Event/BeforeInstantiate.php
@@ -19,17 +19,25 @@
 /**
  * @author Nicolas PHILIPPE <nikophil@gmail.com>
  *
+ * @template T of object
+ * @implements Event<T>
+ *
  * @phpstan-import-type Parameters from Factory
  */
-final class BeforeInstantiate
+final class BeforeInstantiate implements Event
 {
     public function __construct(
         /** @phpstan-var Parameters */
         public array $parameters,
-        /** @var class-string */
+        /** @var class-string<T> */
         public readonly string $objectClass,
-        /** @var ObjectFactory<object> */
+        /** @var ObjectFactory<T> */
         public readonly ObjectFactory $factory,
     ) {
     }
+
+    public function objectClassName(): string
+    {
+        return $this->objectClass;
+    }
 }
diff --git a/src/Object/Event/Event.php b/src/Object/Event/Event.php
new file mode 100644
index 000000000..95382cb25
--- /dev/null
+++ b/src/Object/Event/Event.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the zenstruck/foundry package.
+ *
+ * (c) Kevin Bond <kevinbond@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\Object\Event;
+
+/**
+ * @template T of object
+ */
+interface Event
+{
+    /**
+     * @return class-string<T>
+     */
+    public function objectClassName(): string;
+}
diff --git a/src/Object/Event/HookListenerFilter.php b/src/Object/Event/HookListenerFilter.php
new file mode 100644
index 000000000..12e28e0eb
--- /dev/null
+++ b/src/Object/Event/HookListenerFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the zenstruck/foundry package.
+ *
+ * (c) Kevin Bond <kevinbond@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\Object\Event;
+
+final class HookListenerFilter
+{
+    /** @var \Closure(Event<object>): void */
+    private \Closure $listener;
+
+    /**
+     * @param array{0: object, 1: string} $listener
+     * @param class-string|null           $objectClass
+     */
+    public function __construct(array $listener, private ?string $objectClass = null)
+    {
+        if (!\is_callable($listener)) {
+            throw new \InvalidArgumentException(\sprintf('Listener must be a callable, "%s" given.', \get_debug_type($listener)));
+        }
+
+        $this->listener = $listener(...);
+    }
+
+    /**
+     * @param Event<object> $event
+     */
+    public function __invoke(Event $event): void
+    {
+        if ($this->objectClass && $event->objectClassName() !== $this->objectClass) {
+            return;
+        }
+
+        ($this->listener)($event);
+    }
+}
diff --git a/src/Persistence/Event/AfterPersist.php b/src/Persistence/Event/AfterPersist.php
index eb89763c2..6ab3fa293 100644
--- a/src/Persistence/Event/AfterPersist.php
+++ b/src/Persistence/Event/AfterPersist.php
@@ -14,21 +14,31 @@
 namespace Zenstruck\Foundry\Persistence\Event;
 
 use Zenstruck\Foundry\Factory;
+use Zenstruck\Foundry\Object\Event\Event;
 use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
 
 /**
  * @author Nicolas PHILIPPE <nikophil@gmail.com>
  *
+ * @template T of object
+ * @implements Event<T>
+ *
  * @phpstan-import-type Parameters from Factory
  */
-final class AfterPersist
+final class AfterPersist implements Event
 {
     public function __construct(
+        /** @var T */
         public readonly object $object,
         /** @phpstan-var Parameters */
         public readonly array $parameters,
-        /** @var PersistentObjectFactory<object> */
+        /** @var PersistentObjectFactory<T> */
         public readonly PersistentObjectFactory $factory,
     ) {
     }
+
+    public function objectClassName(): string
+    {
+        return $this->object::class;
+    }
 }
diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php
index 52a55e91f..3036897c4 100644
--- a/src/ZenstruckFoundryBundle.php
+++ b/src/ZenstruckFoundryBundle.php
@@ -12,12 +12,18 @@
 namespace Zenstruck\Foundry;
 
 use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
+use Symfony\Component\DependencyInjection\ChildDefinition;
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Exception\LogicException;
 use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
+use Zenstruck\Foundry\Attribute\AsFoundryHook;
 use Zenstruck\Foundry\Mongo\MongoResetter;
+use Zenstruck\Foundry\Object\Event\Event;
+use Zenstruck\Foundry\Object\Event\HookListenerFilter;
 use Zenstruck\Foundry\Object\Instantiator;
 use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter;
 use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter;
@@ -282,6 +288,25 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
                 ->replaceArgument(0, $config['mongo']['reset']['document_managers'])
             ;
         }
+
+        $container->registerAttributeForAutoconfiguration(
+            AsFoundryHook::class,
+            // @phpstan-ignore argument.type
+            static function(ChildDefinition $definition, AsFoundryHook $attribute, \ReflectionMethod $reflector) {
+                if (1 !== \count($reflector->getParameters())
+                    || !$reflector->getParameters()[0]->getType()
+                    || !$reflector->getParameters()[0]->getType() instanceof \ReflectionNamedType
+                    || !\is_a($reflector->getParameters()[0]->getType()->getName(), Event::class, true)
+                ) {
+                    throw new LogicException(\sprintf("In order to use \"%s\" attribute, method \"{$reflector->class}::{$reflector->name}()\" must have a single parameter that is a subclass of \"%s\".", AsFoundryHook::class, Event::class));
+                }
+                $definition->addTag('foundry.hook', [
+                    'class' => $attribute->objectClass,
+                    'method' => $reflector->getName(),
+                    'event' => $reflector->getParameters()[0]->getType()->getName(),
+                ]);
+            }
+        );
     }
 
     public function build(ContainerBuilder $container): void
@@ -300,6 +325,21 @@ public function process(ContainerBuilder $container): void
                 ->addMethodCall('addProvider', [new Reference($id)])
             ;
         }
+
+        // events
+        $i = 0;
+        foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) {
+            foreach ($tags as $tag) {
+                $container
+                    ->setDefinition("foundry.hook.{$tag['event']}.{$i}", new Definition(class: HookListenerFilter::class))
+                    ->setArgument(0, [new Reference($id), $tag['method']])
+                    ->setArgument(1, $tag['class'])
+                    ->addTag('kernel.event_listener', ['event' => $tag['event']])
+                ;
+
+                ++$i;
+            }
+        }
     }
 
     /**
diff --git a/tests/Fixture/Events/FoundryEventListener.php b/tests/Fixture/Events/FoundryEventListener.php
index ddc00e4f0..c9df24006 100644
--- a/tests/Fixture/Events/FoundryEventListener.php
+++ b/tests/Fixture/Events/FoundryEventListener.php
@@ -14,13 +14,16 @@
 namespace Zenstruck\Foundry\Tests\Fixture\Events;
 
 use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
+use Zenstruck\Foundry\Attribute\AsFoundryHook;
 use Zenstruck\Foundry\Object\Event\AfterInstantiate;
 use Zenstruck\Foundry\Object\Event\BeforeInstantiate;
+use Zenstruck\Foundry\Object\Event\Event;
 use Zenstruck\Foundry\Persistence\Event\AfterPersist;
 use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners;
 
 final class FoundryEventListener
 {
+    /** @param BeforeInstantiate<object> $event */
     #[AsEventListener]
     public function beforeInstantiate(BeforeInstantiate $event): void
     {
@@ -28,9 +31,10 @@ public function beforeInstantiate(BeforeInstantiate $event): void
             return;
         }
 
-        $event->parameters['name'] = "{$event->parameters['name']}\nBeforeInstantiate";
+        $event->parameters['name'] = $this->name($event->parameters['name'], $event);
     }
 
+    /** @param AfterInstantiate<object> $event */
     #[AsEventListener]
     public function afterInstantiate(AfterInstantiate $event): void
     {
@@ -38,9 +42,10 @@ public function afterInstantiate(AfterInstantiate $event): void
             return;
         }
 
-        $event->object->name = "{$event->object->name}\nAfterInstantiate";
+        $event->object->name = $this->name($event->object->name, $event);
     }
 
+    /** @param AfterPersist<object> $event */
     #[AsEventListener]
     public function afterPersist(AfterPersist $event): void
     {
@@ -48,6 +53,67 @@ public function afterPersist(AfterPersist $event): void
             return;
         }
 
-        $event->object->name = "{$event->object->name}\nAfterPersist";
+        $event->object->name = $this->name($event->object->name, $event);
+    }
+
+    /** @param BeforeInstantiate<EntityForEventListeners> $event */
+    #[AsFoundryHook(EntityForEventListeners::class)]
+    public function beforeInstantiateWithFoundryAttribute(BeforeInstantiate $event): void
+    {
+        $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} with Foundry attribute";
+    }
+
+    /** @param AfterInstantiate<EntityForEventListeners> $event */
+    #[AsFoundryHook(EntityForEventListeners::class)]
+    public function afterInstantiateWithFoundryAttribute(AfterInstantiate $event): void
+    {
+        $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute";
+    }
+
+    /** @param AfterPersist<EntityForEventListeners> $event */
+    #[AsFoundryHook(EntityForEventListeners::class)]
+    public function afterPersistWithFoundryAttribute(AfterPersist $event): void
+    {
+        $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute";
+    }
+
+    /** @param BeforeInstantiate<object> $event */
+    #[AsFoundryHook()]
+    public function globalBeforeInstantiate(BeforeInstantiate $event): void
+    {
+        if (EntityForEventListeners::class !== $event->objectClass) {
+            return;
+        }
+
+        $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} global";
+    }
+
+    /** @param AfterInstantiate<object> $event */
+    #[AsFoundryHook()]
+    public function globalAfterInstantiate(AfterInstantiate $event): void
+    {
+        if (!$event->object instanceof EntityForEventListeners) {
+            return;
+        }
+
+        $event->object->name = "{$this->name($event->object->name, $event)} global";
+    }
+
+    /** @param AfterPersist<object> $event */
+    #[AsFoundryHook()]
+    public function globalAfterPersist(AfterPersist $event): void
+    {
+        if (!$event->object instanceof EntityForEventListeners) {
+            return;
+        }
+
+        $event->object->name = "{$this->name($event->object->name, $event)} global";
+    }
+
+    private function name(string $name, Event $event): string // @phpstan-ignore missingType.generics
+    {
+        $eventName = (new \ReflectionClass($event))->getShortName();
+
+        return "{$name}\n{$eventName}";
     }
 }
diff --git a/tests/Integration/Persistence/EventsTest.php b/tests/Integration/EventsTest.php
similarity index 74%
rename from tests/Integration/Persistence/EventsTest.php
rename to tests/Integration/EventsTest.php
index 358d0f3bc..25398d187 100644
--- a/tests/Integration/Persistence/EventsTest.php
+++ b/tests/Integration/EventsTest.php
@@ -11,13 +11,12 @@
  * file that was distributed with this source code.
  */
 
-namespace Zenstruck\Foundry\Tests\Integration\Persistence;
+namespace Zenstruck\Foundry\Tests\Integration;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Zenstruck\Foundry\Test\Factories;
 use Zenstruck\Foundry\Test\ResetDatabase;
 use Zenstruck\Foundry\Tests\Fixture\Events\FactoryWithEventListeners;
-use Zenstruck\Foundry\Tests\Integration\RequiresORM;
 
 final class EventsTest extends KernelTestCase
 {
@@ -34,8 +33,14 @@ public function it_can_call_hooks(): void
             <<<TXT
                 events
                 BeforeInstantiate
+                BeforeInstantiate with Foundry attribute
+                BeforeInstantiate global
                 AfterInstantiate
+                AfterInstantiate with Foundry attribute
+                AfterInstantiate global
                 AfterPersist
+                AfterPersist with Foundry attribute
+                AfterPersist global
                 TXT,
             $address->name
         );