CakePHP 4 Events

by | Apr 21, 2025 | IT Tips | 0 comments

Found in my Drafts folder last modified 2020/08/21 at 11:20 am. Published April '25

Until yesterday I hadn't used the CakePHP Event system with the exception of putting some code in default Event callbacks such as Model.beforeSave, Model.afterSave or Controller.beforeFilter

In my application I have a screen to:

  1. Print a pallet label which sends a print job to a print class to output either a Glabels PDF or a raw text printer commands to send to CAB or Zebra Label printers
  2. which creates a new pallet record in the database,
  3. creates an associated record in a cartons table

Up until now I have primarily done the logic for the above in the Controller Action but it's busy and messy

I now have the need to take the PDF output from Glabels and attach and send an email. So this adds another step 4. to the above list. Putting this and all the above code in a controller just makes it a very long action method

So I have started using Events

An example of sending an email from a random non-cake class

i.e. if the class doesn't have $this->getEventManager() available you need to use the global event manager:

1. Register the Event with CakePHP's Global Event Manager

Here I am using config/bootstrap.php to register the Event. You can put it in the initialize or __construct methods of classes too. You just have to register the Event somewhere before it is triggered or dispatched

// config/bootstrap.php
// register the event and what callable handles it
// it just has to be before the EventManager::dispatch() call

use App\Mailer\AppMailer;

$mailer = new AppMailer();

EventManager::instance()->on($mailer);
// in src/Mailer/AppMailer add implementedEvents
// which returns the information about which event are published
// and what method run when the event is triggered (dispatched)

    public function implementedEvents(): array
    {
        return [
            // when this event is fired => run this method
            'Label.Glabel.printSuccess' => 'sendLabel'
        ];
    }

If the class has an implementedEvents method and you pass it as the first argument to EventManager::instance()->on() on() will look at the return value of implementedEvents() in AppMailer and know the name of the event and the method to call when the event is triggered

Warning: If you use the global event manager and you using one of the default events. It seems as if the default events e.g. beforeSave, afterSave events all pass through it.

Registering an Event when the target doesn't implement EventListenerInterface

If AppMailer or the class you want to call doesn't implement the EventListenerInterface meaning it doesn't have a implementedEvents method but you still wanted to call a method from it when an event is triggered then you can define the class and the method to call as follows

<?php
use App\Mailer\AppMailer;

$mailer = new AppMailer();

EventManager::instance()->on('Label.Glabel.printSuccess', [ $mailer, 'sendLabel' ]);

Registering the Event and its Callable when class implements the EventListenerInterface

2. Create an Event and Dispatch it

OK so you have said to CakePHP when a Label.Glabel.printSuccess Event happens I want you to run the 'sendLabel' method in the App\Mailer\AppMailer class.

But now you need to trigger or dispatch that event somewhere in your code. If you are using the Global EventManager you can trigger the Event from pretty much anywhere in your code after you have registered the event as above

Here is an example of triggering the Event in a class

<?php
# at top of file
namespace App\Lib\PrintLabels;

use Cake\Event\EventManager;
use Cake\Event\Event;

class Label
{

    function createGlabelPdf()
    {

        // generate Glabels PDF output here
        $results = $this->glabelsBatchPrint();

        if ($results->ok) {

            $this->setPrintContent(file_get_contents($this->getPdfOutFile()));

            $event = new Event('Label.Glabel.printSuccess', $this);
            # when the above event is dispatched (see next line of code)
            # the method specified in the implementedEvents function or 
            # whichever callable was passed using ->on() will run:
            # in this example it will be the sendLabel method in the AppMailer class
            # 'Label.Glabel.printSuccess' => 'sendLabel' 
            # it will be passed a 'subject' which in this case is the current class 
            # '$this'. The is accessible in the method 
            # sendLabel(Event $event) by calling $event->getSubject();
            EventManager::instance()->dispatch($event);

            unlink($this->getPdfOutFile());
        } else {
            // handle failure
        }
    }
}

This is my Mailer class that implements the EventListenerInterface and has the implementedEvents method

# src/Mailer/AppMailer.php
<?php
namespace App\Mailer;

use App\Lib\Exception\MissingConfigurationException;
use Cake\Mailer\Mailer;
use Cake\Event\Event;
use Cake\Core\Exception\Exception;
use InvalidArgumentException;
use App\Lib\Utility\SettingsTrait;
use Cake\Mailer\Exception\MissingActionException;
use BadMethodCallException;
/*
 * The Mailer class already implements EntityInterface
 * but if you class doesn't you need add this use
 * use Cake\Datasource\EntityInterface;
 */

class AppMailer extends Mailer
{
    use SettingsTrait;

    public function implementedEvents(): array
    {
        return [
            // when this event is fired => run this method
            'Label.Glabel.printSuccess' => 'sendLabel'
        ];
    }

    /**
     * 
     * @param Event $subject 
     * @return void 
     * @throws MissingConfigurationException 
     * @throws Exception 
     * @throws InvalidArgumentException 
     * @throws MissingActionException 
     * @throws BadMethodCallException 
     */
    public function sendLabel(Event $event)
    {  # this shows that you can call send and pass it another action and args
       $this->send('sendLabelPdfAttachment', [ $event->getSubject()]);
    }

    public function sendLabelPdfAttachment($label){

        $to = $this->addressParse($this->getSetting('EMAIL_PALLET_LABEL_TO'));

        if(empty($to)) {
            return; 
        }
        
        /* 
        $itemCode = $labels->getItemCode();
        $batch = $labels->getBatch();
        $reference = $labels->getReference();
        $jobId = $labels->getJobId(); 
        */

        $pdfFile = $label->getPdfOutFile();
        
        $this->setProfile('production')
            ->setTo($to)
            ->setEmailFormat('both') // html and text
            ->setAttachments([$label->getJobId() . '.pdf' => $pdfFile ])
            ->setSubject(sprintf('Label - %s', $label->getJobId(), ))
            ->viewBuilder()
            //->setVars(compact('itemCode', 'batch', 'reference', 'jobId')) 
            ->setTemplate('send_label');
    }

    /**
     * addressParse returns either an empty array or email addresses formatted
     * for the Mailer::setTo()
     * e.g. [ '[email protected]' => "James McDonald" , '[email protected]' => "Example Email" ]
     * 
     * @param array $addresses 
     * @return array 
     */
    public function addressParse(array $addresses): array
    {

        $add = [];
        foreach ($addresses as $addressLine) {
            $add = array_merge($add, mailparse_rfc822_parse_addresses($addressLine));
        }

        $formatted = [];

        foreach ($add as $a) {
            if(filter_var($a['address'], FILTER_VALIDATE_EMAIL)){
                     $formatted[$a['address']] = $a['display'];
            }
        }

        return $formatted;
    }
}

Don't De-register Your Default Events When You Implement Custom Events

If you add the implementedEvents method to a model remember to either call parent::implementedEvents() or add the default event that you want to call into the current method

<?php

class PalletsTable extends Table
{

    public function implementedEvents()
    {
        return [
            'Model.Pallets.addFile' => 'addFile'
        ];
    }

    public function afterSave(Event $event, EntityInterface $entity, $options = [])
    {
        // this will not fire
    }
}

Call parent::implementedEvents() and merge it with the custom events

<?php

class PalletsTable extends Table
{

    public function implementedEvents()
    {
        return array_merge(
            parent::implementedEvents(),
            [ 
                'Model.Pallets.addFile' => 'addFile'
            ]);
    }

    public function afterSave(Event $event, EntityInterface $entity, $options = [])
    {
        // will now fire
    }
}


Or just manually define the default events that you still want to fire

<?php

class PalletsTable extends Table
{

    public function implementedEvents()
    {
        return [
                'Model.afterSave' => 'afterSave',
                'Model.beforeSave' => 'beforeSave',
                'Model.Pallets.addFile' => 'addFile'
            ];
    }

    public function afterSave(Event $event, EntityInterface $entity, $options = [])
    {
        // will now fire
    }
}

Be careful the global event manager will execute multiple default events happily

One important thing you should consider is that there are events that will be triggered having the same name but different subjects, so checking it in the event object is usually required in any function that gets attached globally in order to prevent some bugs. Remember that with the flexibility of using the global manager, some additional complexity is incurred.

From the docs https://book.cakephp.org/4/en/core-libraries/events.html#registering-event-listeners

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.