diff --git a/developer_manual/basics/events.rst b/developer_manual/basics/events.rst index 17c8f5f26ab..f24d36b783c 100644 --- a/developer_manual/basics/events.rst +++ b/developer_manual/basics/events.rst @@ -4,166 +4,385 @@ Events ====== -Events are used to communicate between different aspects of the Nextcloud eco system. They are used in the Nextcloud server internally, for server-to-apps communication as well as inter-app communication. +.. contents:: + :local: + :depth: 2 +Introduction +------------ -Overview --------- +In Nextcloud, it is often important for distinct components -- including apps -- to communicate without tightly coupling their code together. **Events** are a standard solution to this problem. -The term "events" is a bit broad in Nextcloud and there are multiple ways of emitting them. +An **event** is a signal emitted by code when something noteworthy happens, such as: -* `OCP event dispatcher`_ -* `Hooks`_ -* `Public Emitter`_ +- a user logging in, +- a file being uploaded, or +- a group being deleted +Other parts of the system -- including third-party apps -- can "listen" for these events and react by running additional logic, for example: -OCP event dispatcher --------------------- +- sending a notification, +- updating a log, or +- blocking an operation based on business rules -This mechanism is a versatile and typed approach to events in Nextcloud's php code. It uses objects rather than just passing primitives or untyped arrays. This should help provide a better developer experience while lowering the risk of unexpected changes in the API that are hard to find after the initial implementation. +This pattern enables: -Naming scheme -````````````` +- **Loose coupling:** The code that emits the event does not need to know which listeners will respond. +- **Extensibility:** New features and apps can add behavior or change workflows by registering listeners for relevant events. +- **Maintainability:** Changing or extending how an event is handled does not require altering the core functionality that emits it. + +**Example:** +Imagine a file-sharing platform - ahem - where you want to log every time a file is deleted. Instead of modifying every place in the codebase where deletions happen, the component that handles deletion emits a "file deleted" event whenever it deletes a file. A logging module can then listen for this event and record the deletion whenever it occurs -- no matter the source. + +**In summary:** +Events allow Nextcloud (and its apps and integrations) to build flexible, maintainable, and powerful features that respond to key actions across the system. + +Overview of Events in Nextcloud +------------------------------- + +The modern mechanism for emitting and listening to events in the server and apps is Nextcloud's **OCP Event Dispatcher**. Utilizing the OCP Event Dispatcher is the recommended and standard approach; it delivers type-safety, dependency-injection-friendly listeners, and clear, future-proof event contracts. + +.. warning:: + Older approaches -- **hooks** and **public emitters** -- were used in early Nextcloud versions. They are now deprecated and available only for rare migration or compatibility scenarios. + +.. tip:: + If you’re migrating old code, see `Hooks (Deprecated)`_ and `Public Emitters (Deprecated)`_ sections -- or refer to older documentation versions -- for more historical context helpful to transitioning, including some of the other ways of registering listeners. + +OCP Event Dispatcher +-------------------- -The name should reflect the subject and the actions. Suffixing event classes with `Event` makes it easier to recognize their purpose. +This mechanism provides a robust, typed approach to events in Nextcloud's PHP code. It uses objects rather than just passing primitives or untyped arrays, improving developer experience and reducing the risk of unexpected API changes that are hard to diagnose after the initial implementation. -For example, if a user is created, a `UserCreatedEvent` will be emitted. +Naming Events +````````````` -Events are usually emitted *after* the event has happened. If it's emitted before, it should be prefixed with `Before`. +Event class names should clearly reflect the subject and the action, and should be suffixed with ``Event``, making their purpose immediately recognizable. -Thus `BeforeUserCreatedEvent` is emitted *before* the user data is written to the database. +For example, if a user is created, a ``UserCreatedEvent`` will be emitted. -.. note:: Although you may choose to name your event classes differently, sticking to the convention will allow Nextcloud developers understand each other's apps more easily. +Events are usually emitted *after* the action has occurred. If emitted before, the event should be prefixed with ``Before``, e.g., ``BeforeUserCreatedEvent`` is emitted before user data is written to the database. -.. note:: For backwards compatibility with the Symfony class `GenericEvent `_, Nextcloud also provides a ``\OCP\EventDispatcher\Event`` class. With the release of Nextcloud 22 this class has been deprecated. Named and typed event classes should be used instead. +.. note:: + Although you may choose to name your event classes differently, sticking to this naming convention helps all Nextcloud developers understand each other's apps more easily. -Writing events +Writing Events `````````````` -As a rule events are dedicated classes extending ``\OCP\EventDispatcher\Event``. +Events are dedicated classes extending ``\OCP\EventDispatcher\Event``: .. code-block:: php `_. Therefore they need a constructor that takes the arguments, private members to store them and getters to access the values in listeners. +This event simply signals that *something happened*. In many cases, you want to transport data with the event -- for example, the affected resource. In these cases, events act as `data transfer objects `_: they need a constructor for the data, private members to store it, and getters for listeners to access the values: .. code-block:: php user = $user; + public function __construct( + private IUser $user + ) { } public function getUser(): IUser { return $this->user; } - } -Writing a listener -`````````````````` +You may never need to write your own event, as many `Public Events `_ are already implemented by Nextcloud core and apps. -A listener can be a simple callback function (or anything else that is `callable `_, or a dedicated class. +.. tip:: + Don't get too hung up regarding data transportation at the moment if you're unfamiliar with the topic. We'll return to the ``UserCreatedEvent`` and DTO in the context of a fuller example later on. For now, let's return to the simpler ``AddEvent``, which merely fires ("this happened"), without transporting any data to our listener. +.. note:: + You may see older code that calls the parent constructor (i.e. ``parent::__construct();``) in its Event class. This is no longer necessary; it won't hurt anything in existing code, but is a no-op. -Listener callbacks -****************** +Writing Listeners +````````````````` -You can use simple callback to react on events. They will receive the event object as first and only parameter. You can type-hint the base `Event` class or the subclass you expect and register for. +A listener is a class that handles an event by implementing the ``OCP\EventDispatcher\IEventListener`` interface. Class names should end with ``Listener``. .. code-block:: php getContainer()->query(IEventDispatcher::class); - $dispatcher->addListener(AddEvent::class, function(AddEvent $event) { - // ... - }); + use OCA\MyApp\Events\AddEvent; + use OCP\EventDispatcher\Event; + use OCP\EventDispatcher\IEventListener; + + /** + * Listener that adds two to a counter whenever an AddEvent is fired. + */ + class AddTwoListener implements IEventListener { + + // The logic triggered in response to an event + public function handle(Event $event): void { + if (!($event instanceof AddEvent)) { + return; + } + + $event->addToCounter(2); } } -.. note:: Type-hinting the actual event class will give you better IDE and static analyzers support. It's generally safe to assume the dispatcher will not give you any other objects. +The listener is registered during app bootstrap and is instantiated by the upstream DI container when the corresponding event is fired. During the listener's existence, the handler (``handle()``) within it is called whenever an ``AddEvent`` is fired. The listener's handler implements the business logic (i.e. does the +interesting thing). -Listener classes -**************** +.. note:: + PHP parameter type hints cannot be more specific than those on the interface, so you can't type-hint ``AddEvent`` in the method signature; instead use instanceof inside the handler method. -A class that can handle an event will implement the ``\OCP\EventDispatcher\IEventListener`` interface. Class names should end with `Listener`. +.. note:: + By default there is no persistent "listener object" kept by the dispatcher. Each time the event is fired, the DI container will lazily instantiate a new instance of the listener (the class), invoke the handle() method, and then discard the instance. There are more advanced approaches to listener registration (singleton/shared), but + that is a more advanced use case -- not the default for DI-registered event listeners in Nextcloud. You may find this approach in core, but rarely in apps. + +Registering Listeners +````````````````````` + +Registering connects your listener class to the events. Modern Nextcloud apps implement the ``IBootstrap`` interface in their ``Application`` class. Event listeners should be registered in the :php:meth:`register()` method of this class by calling ``registerEventListener()``. The listener class is instantiated only when the event is fired: .. code-block:: php registerEventListener(AddEvent::class, AddTwoListener::class); + } + + public function boot(IBootContext $context): void { + } + } + +The ``EventListener`` class (``AddTwoListener``) is instantiated by the DI container, so you can add a constructor (in the listener class) with any type-hinted dependencies your event listener needs (such as services). The ``Event`` object itself will be passed to the ``handle()`` method when the event fires. Example based on the ``AddTwoListener`` event listener class we created previously: + +.. code-block:: php + + myService->logAdded($event); + + // Continue with other logic if needed $event->addToCounter(2); } } +The event (``AddEvent``, etc) will **not** be passed to the listener's constructor; it’s passed to ``handle()``. The listener is injected with ``MyService`` at instantiate time; its handler is called whenever ``AddEvent`` is fired during its lifetime. When the event listener is instantiated, the upstream container injects dependencies per the type-hints in the listener's constructor (In this case, a service called ``MyService $myservice``). The``MyService`` dependency/injected service is available for use by the handler as needed. -.. note:: Php parameter type hints are not allowed to be more specific than the type hints on the interface, thus you can't use `AddEvent` in the method signature but use an `instanceOf` instead. +.. tip:: + You may see older code that registers listeners in a slightly different way, such as by using lower level functions such as ``addServiceListener()`` and ``addListener()`` (and/or possibly registering via the constructor). These are not covered here as they are not recommended for newer implementations. If maintaining a + legacy app that does not implement ``IBootstrap``, event listeners may be registered in the ``Application`` class as outlined in previous versions of the documentation. For all new development, use ``IBootstrap`` pattern described here. -In the ``Application.php`` the event and the listener class are connected. The class is instantiated only when the actual event is fired. +Expanded Example +```````````````` + +Below is an expanded example, building on on our earlier ``UserCreatedEvent``. It demonstrates: + +- How to use the event's ``getUser()`` method to access payload data +- How to inject and use a logger service in the listener .. code-block:: php user; + } + } + +.. code-block:: php + + getUser(); + + // Log the username of the created user + $username = $user->getUID(); + $this->logger->info("A new user was created: {$username}"); + } + } + +To register the listener in your app's bootstrap class: + +.. code-block:: php + + getContainer()->get(IEventDispatcher::class); - $dispatcher->addServiceListener(AddEvent::class, AddTwoListener::class); + use OCP\AppFramework\Bootstrap\IBootstrap; + use OCP\AppFramework\Bootstrap\IRegistrationContext; + use OCA\MyApp\Events\UserCreatedEvent; + use OCA\MyApp\Events\LogCreatedUserListener; + + class Bootstrap implements IBootstrap { + + public function register(IRegistrationContext $context): void { + $context->registerEventListener(UserCreatedEvent::class, LogCreatedUserListener::class); + } + + public function boot(IBootContext $context): void { } } -.. note:: The listener is resolved via the DI container, therefore you can add a constructor and type-hint services required for processing the event. +**Explanation:** -Available Events -```````````````` +- The ``UserCreatedEvent`` transports the ``IUser`` object as its payload. +- ``LogCreatedUserListener`` is an event listener that receives an injected logger service via DI. +- Inside ``handle()``, it checks if the event is a ``UserCreatedEvent``, uses ``getUser()``, then logs the new user’s UID. +- The listener is registered using ``registerEventListener()`` within the app's bootstrap. + +Emitting Events +``````````````` + +To allow other apps or components to react to actions in your app, you can emit (dispatch) your own events at key points in your code using the ``\OCP\EventDispatcher\IEventDispatcher`` service, typically injected into your services or controllers: + +.. code-block:: php + + userFactory->create($uid); + + // ... any other logic ... + + // Emit an event so other apps can react + $event = new UserCreatedEvent($user); + $this->dispatcher->dispatch($event); + + return $user; + } + } + +Available Public Events +``````````````````````` Here you find an overview of the public events that can be consumed in apps. See their source files for more details. @@ -393,18 +612,20 @@ This event is triggered whenever the viewer is loaded and extensions should be l .. include:: _available_events_ocp.rst -Hooks ------ +Hooks (Deprecated) +------------------ .. deprecated:: 18 Use the `OCP event dispatcher`_ instead. .. sectionauthor:: Bernhard Posselt -Hooks are used to execute code before or after an event has occurred. This is for instance useful to run cleanup code after users, groups or files have been deleted. Hooks should be registered in the :doc:`Bootstrapping process <../app_development/bootstrap>`. +Hooks are a legacy event mechanism. Do **NOT** use for new app development. -Available hooks -``````````````` +Hooks should be registered in the :doc:`Bootstrapping process <../app_development/bootstrap>`. + +Using Hooks +``````````` The scope is the first parameter that is passed to the **listen** method, the second parameter is the method and the third one the callback that should be executed once the hook is being called, e.g.: @@ -429,10 +650,13 @@ Hooks can also be removed by using the **removeListener** method on the object: $userManager->removeListener(null, null, $callback); +Available hooks +``````````````` + The following hooks are available: Session -``````` +******* Injectable from the ServerContainer with the ``\OCP\IUserSession`` service. @@ -450,7 +674,7 @@ Hooks available in scope **\\OC\\User**: * **logout** () UserManager -``````````` +*********** Injectable from the ServerContainer with the ``\OCP\IUserManager`` service. @@ -464,7 +688,7 @@ Hooks available in scope **\\OC\\User**: * **postCreateUser** (\\OC\\User\\User $user, string $password) GroupManager -```````````` +************ Hooks available in scope **\\OC\\Group**: @@ -478,7 +702,7 @@ Hooks available in scope **\\OC\\Group**: * **postCreate** (\\OC\\Group\\Group $group) Filesystem root -``````````````` +*************** Injectable from the ServerContainer by calling the method **getRootFolder()**, **getUserFolder()** or **getAppFolder()**. @@ -506,7 +730,7 @@ Filesystem hooks available in scope **\\OC\\Files**: * **postRename** (\\OCP\\Files\\Node $source, \\OCP\\Files\\Node $target) Filesystem scanner -`````````````````` +****************** Filesystem scanner hooks available in scope **\\OC\\Files\\Utils\\Scanner**: @@ -516,10 +740,10 @@ Filesystem scanner hooks available in scope **\\OC\\Files\\Utils\\Scanner**: * **postScanFolder** (string $absolutePath) -Public emitter --------------- +Public emitters (Deprecated) +---------------------------- .. deprecated:: 18 Use the `OCP event dispatcher`_ instead. -tbd +Emitters are a legacy event mechanism. Do **NOT** use for new app development.