Skip to content

Event observers

Maho uses an observer pattern to let modules react to events dispatched throughout the system. When something significant happens — a product is saved, a customer logs in, an order is placed — the system fires a named event, and any registered observer method is called.

Dispatching events

Events are fired with Mage::dispatchEvent():

Mage::dispatchEvent('catalog_product_save_after', ['product' => $product]);

The second argument is an associative array of data made available to observers through the $observer parameter.

Registering observers

v26.5+

Since v26.5, observers are defined using PHP attributes directly on the method. Previously they were configured in XML — see Migrating from XML if you're upgrading an existing module.

Place the #[Maho\Config\Observer] attribute on a public method to register it as an event observer:

class Mage_Wishlist_Model_Observer
{
    #[Maho\Config\Observer('customer_login')]
    public function customerLogin(Maho\Event\Observer $observer): void
    {
        $customer = $observer->getEvent()->getCustomer();
        // ...
    }
}

After adding or changing any observer attribute, run:

composer dump-autoload

This compiles all attributes into vendor/composer/maho_attributes.php, which is what the runtime reads.

Parameters

Parameter Type Default Description
event string (required) Event name to observe (e.g. 'catalog_product_save_after')
area string 'global' Area scope: 'global', 'frontend', 'adminhtml', 'crontab', 'install'. Comma-separated for multiple areas (e.g. 'frontend,adminhtml')
type string 'model' 'model' (new instance per dispatch) or 'singleton' (shared instance)
id ?string auto Observer identifier. Set explicitly when other code references this observer by id
replaces ?string null The id of another observer on the same event/area to disable and replace

Area scoping

By default, observers are global — they fire regardless of the current area. Use the area parameter only when the observer must be restricted to a specific area:

// Only fires in the frontend
#[Maho\Config\Observer('controller_action_predispatch_customer_account_createpost', area: 'frontend')]
public function verify(Maho\Event\Observer $observer): void { }

// Only fires in the admin panel
#[Maho\Config\Observer('admin_user_authenticate_before', area: 'adminhtml')]
public function verifyAdmin(Maho\Event\Observer $observer): void { }

You can also target multiple areas with a comma-separated string, which registers the observer in each area without repeating the attribute:

#[Maho\Config\Observer('some_event', area: 'frontend,adminhtml')]
public function handleBothAreas(Maho\Event\Observer $observer): void { }

Multiple events on one method

The attribute is repeatable — apply it multiple times to listen to different events:

#[Maho\Config\Observer('controller_action_predispatch_checkout_onepage_savebilling', area: 'frontend')]
#[Maho\Config\Observer('controller_action_predispatch_contacts_index_post', area: 'frontend')]
#[Maho\Config\Observer('controller_action_predispatch_customer_account_createpost', area: 'frontend')]
public function verify(Maho\Event\Observer $observer): void
{
    // Runs for all three events
}

Named observers

Use the id parameter when other code needs to reference this observer:

#[Maho\Config\Observer('sales_order_invoice_pay', id: 'giftcard_create_on_payment')]
public function createGiftcardsOnInvoicePaid(Maho\Event\Observer $observer) { }

Singleton instantiation type

By default, observers use model (a fresh instance per dispatch), which is the safer choice — no risk of leaked state between event handlers. Use type: 'singleton' only when your observer needs to share state across multiple dispatches within the same request:

#[Maho\Config\Observer('catalog_product_save_after', type: 'singleton')]
public function trackChanges(Maho\Event\Observer $observer) { }

Replacing existing observers

The replaces parameter lets a new observer disable and replace an existing one. This is useful for third-party modules overriding core behavior:

#[Maho\Config\Observer('customer_login', replaces: 'original_observer_id')]
public function myCustomLogin(Maho\Event\Observer $observer): void { }

The replaces value accepts:

  • An explicit observer id (e.g. 'my_observer')
  • A class/method string (e.g. 'Mage_Catalog_Model_Observer::myMethod')
  • A class alias format (e.g. 'catalog/observer::myMethod')

How it works under the hood

  • Compiled observers use class aliases (not fully-qualified class names), so the rewrite system is fully respected. If a module rewrites a model class, attribute-based observers resolve through the rewrite chain transparently.
  • Observer ordering follows module dependency, regardless of whether the observer was defined via XML or attribute.
  • Individual observer failures are isolated and logged without breaking the rest of the dispatch.

Migrating from XML

If you're upgrading a module that used XML-based observer configuration:

Before (config.xml):

<config>
    <global>
        <events>
            <customer_login>
                <observers>
                    <wishlist>
                        <class>wishlist/observer</class>
                        <method>customerLogin</method>
                    </wishlist>
                </observers>
            </customer_login>
        </events>
    </global>
</config>

After (PHP attribute):

#[Maho\Config\Observer('customer_login')]
public function customerLogin(Maho\Event\Observer $observer): void { }

The area mapping:

XML location Attribute area value
<global><events> 'global' (default, can be omitted)
<frontend><events> area: 'frontend'
<adminhtml><events> area: 'adminhtml'
<crontab><events> area: 'crontab'

Remove the corresponding XML blocks from config.xml after migrating — both sources are read at runtime, and duplicates will cause issues.