Kickstarting Magento 2 Architecture Journey: Mastering Observer Architecture for Newcomers
Tags: magento2, php, beginners, tutorial
Published: 2023-06-19 21:36:09

Hello and welcome to the first post in our blog series dedicated to the architectural nuances of Magento 2. Aimed specifically at those taking their initial steps into the Magento ecosystem, this series will illuminate key architectural elements and concepts of the platform. Our inaugural topic is a vital part of Magento's Event-Driven Architecture - the Observer Pattern.

## Magento 2 Observer Architecture: An Overview
The Observer Architecture, an event-driven design pattern in Magento, paves the way for effective communication between various sections of code. It adopts the principle of 'Loose Coupling,' promoting interactions without rigid dependencies. Essentially, it lets you extend and customize Magento's functionality without modifying the core system files significantly.

Various events, such as adding a product to the cart or placing an order, initiate specific sequences of actions within Magento 2. The Dispatcher identifies these events and calls the corresponding observers - class methods assigned to carry out actions related to the event.

## Identifying Event Names

Magento 2 has predefined event names for numerous actions. To identify the correct event name, you need to understand the functionality you wish to extend or modify. Often, you can find these event names within Magento's core code files, specifically inside the dispatch function calls. A pro tip: For a more comprehensive list, refer to the [Magento 2 Event Cheat Sheet](https://github.com/alaa-almaliki/magento2-events-log) available online.

## Registering an Observer with `events.xml`

To declare an observer in Magento 2, you will utilize the events.xml file. You'll need to specify the event name you're observing and the class that will handle it. Here's a quick walkthrough on registering an observer:

Create an `events.xml` file in the etc directory of your module.

Register your observer in the `events.xml` file.

```xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="checkout_cart_product_add_after">
        <observer name="slack_message_observer" instance="Vendor\Module\Observer\SlackMessageObserver" />
    </event>
</config>
```

## Crafting an Observer: Sending a Slack Message on Cart Addition

Let's put this into practice by creating an observer that sends a Slack message whenever a product is added to the cart.

First, define your Observer class, `SlackMessageObserver.php`, within your module's Observer directory.

```php
namespace Vendor\Module\Observer;

use Magento\Framework\Event\ObserverInterface;
use Vendor\Module\Model\SlackMessageSender;

class SlackMessageObserver implements ObserverInterface
{
    private SlackMessageSender $slackMessageSender;

    public function __construct(SlackMessageSender $slackMessageSender)
    {
        $this->slackMessageSender = $slackMessageSender;
    }

public function execute(\Magento\Framework\Event\Observer $observer)
{
    $product = $observer->getDataUsingMethod('product');

    if ($product) {
        $message = 'A product has been added to the cart: ' . $product->getName();
        $this->slackMessageSender->send($message);
    }
}
}
```

In this code, we've implemented the execute method, which gets invoked when the `checkout_cart_product_add_after` event is dispatched. The execute method gathers the product data and sends a message to a Slack channel using the `SlackMessageSender` service.

## Testing Your Observer

Testing in Magento 2 typically involves two levels - Unit Testing and Integration Testing.

**Unit Testing** aims at the smallest testable parts of an application, in isolation. In our observer's case, we can create a mock of `SlackMessageSender` and confirm if the send method is called with the expected argument.

**Integration Testing**, conversely, examines the interaction between different application components. It could involve testing the entire process, from adding a product to the cart to sending the Slack message. Here's how an integration test for our observer might look:

```php
namespace Vendor\Module\Observer\Test\Integration;

use Magento\TestFramework\Helper\Bootstrap;
use Magento\Framework\Event\ManagerInterface as EventManager;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Checkout\Model\Cart;
use Vendor\Module\Observer\SlackMessageObserver;
use Vendor\Module\Model\SlackMessageSender;

class SlackMessageObserverTest extends \PHPUnit\Framework\TestCase
{
    private $objectManager;
    private $cart;
    private $slackMessageSenderMock;

    protected function setUp(): void
    {
        $this->objectManager = Bootstrap::getObjectManager();
        $this->cart = $this->objectManager->get(Cart::class);
        $this->slackMessageSenderMock = $this->createMock(SlackMessageSender::class);
        $this->objectManager->addSharedInstance($this->slackMessageSenderMock, SlackMessageSender::class);
        $this->objectManager->addSharedInstance($this->objectManager->create(SlackMessageObserver::class), SlackMessageObserver::class);
    }

    public function testObserverIsTriggeredOnAddToCart(): void
    {
        $productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
        $product = $productRepository->get('simple'); // 'simple' should be a SKU of a product in your test database

        $this->slackMessageSenderMock
            ->expects($this->once())
            ->method('send')
            ->with($this->stringContains($product->getName()));

        $this->cart->addProduct($product, 1);
        $this->cart->save();
    }

    protected function tearDown(): void
    {
        $this->objectManager->removeSharedInstance(SlackMessageSender::class);
        $this->objectManager->removeSharedInstance(SlackMessageObserver::class);
    }
}

```

In addition, a basic unit test might look like this:

```php
namespace Vendor\Module\Test\Unit;

use PHPUnit\Framework\TestCase;
use Vendor\Module\Observer\SlackMessageObserver;
use Vendor\Module\Model\SlackMessageSender;
use Magento\Catalog\Model\Product;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event;

class SlackMessageObserverTest extends TestCase
{
    public function testExecute(): void
    {
        $productName = 'Test Product';
        
        // Mock product
        $productMock = $this->createMock(Product::class);
        $productMock->method('getName')->willReturn($productName);
        
        // Mock event
        $eventMock = $this->createMock(Event::class);
        $eventMock->method('getData')->with('product')->willReturn($productMock);
        
        // Mock observer
        $observerMock = $this->createMock(Observer::class);
        $observerMock->method('getEvent')->willReturn($eventMock);
        
        // Mock SlackMessageSender
        $slackMessageSenderMock = $this->createMock(SlackMessageSender::class);
        $slackMessageSenderMock->expects($this->once())
            ->method('send')
            ->with($this->stringContains($productName));
        
        // Instantiate the SlackMessageObserver with the mocked SlackMessageSender
        $observer = new SlackMessageObserver($slackMessageSenderMock);
        $observer->execute($observerMock);
    }
}
```

Testing is as critical as writing code itself. Tests ensure your modules' stability and reliability, and mitigate future issues arising from updates or changes in other parts of the system.

## Best Practices

### Declare the Observer in the Appropriate Scope

The first step in using Magento 2 observers effectively is to ensure you declare them in the right scope: `global`, `frontend`, or `adminhtml`. The scope of your observer depends on where you need it to operate. A `global` scope means the observer operates universally, both in the admin area and the frontend. If your observer is only needed in the backend, use the `adminhtml` scope. Meanwhile, if it's strictly for the user-facing part of your site, then `frontend` is the appropriate scope.

### Make the Observer as Efficient as Possible

Efficiency is key when it comes to coding, particularly with observers in Magento 2. To increase the efficiency of your observer, streamline your code. Try to reduce unnecessary loops and calculations as these can slow down your web pages. Remember, a lean observer is a fast observer. For instance, if there's an SQL query within your observer code that's not essential or could be optimized, it might be worth revisiting.

### Avoid Cyclical Event Loops

Avoiding cyclical event loops is another crucial best practice. In a cyclical event loop, an event triggers an observer which then dispatches another event, which then triggers the initial event, and so on. This can lead to significant performance issues. To prevent this, ensure that your observers are mindful of the events they're triggering and avoid creating unnecessary dependencies.

### Do Not Rely on Invocation Order

Finally, when dealing with multiple observers in Magento 2, do not rely on their invocation order. The order in which observers are invoked in Magento 2 is not specified, so building dependencies based on order can lead to unpredictable results. Each observer should be designed to operate independently, regardless of when it's invoked in the sequence.

In conclusion, using Magento 2 observers effectively involves careful scope placement, lean and efficient coding, avoidance of cyclical event loops, and independent design to prevent reliance on invocation order. Following these best practices will help you optimize your Magento 2 observer use.

## Come to an end

And there we have it - the **first** steps into Magento 2's intriguing observer architecture. Don't worry if it seems a bit overwhelming now; with each post in this series, the fog will clear, and the road will become more navigable. Remember, all great journeys start with a single step.

As we journey together, I'd love to connect with you on other platforms as well. You can follow me on [Twitter](https://twitter.com/DavidLambauer) for regular insights, quick tips, and all things Magento. Or join me on [Github](https://github.com/DavidLambauer) to explore my projects, learn from the code and maybe even contribute to some open source initiatives!

Stay tuned for the next post in this series, and don't forget, the world of Magento 2 is expansive and full of opportunities for those willing to explore. As always, happy Magento coding!