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:
- 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
- which creates a new pallet record in the database,
- 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