Skip to content

Commit

Permalink
Provide serialized way for easier 4.x upgrading. (#397)
Browse files Browse the repository at this point in the history
* Provide serialized way.

* Provide serialized way.

* Add test case.

* Cleanup.
  • Loading branch information
dereuromark authored Feb 7, 2024
1 parent e6a3924 commit 5a1f127
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 36 deletions.
40 changes: 17 additions & 23 deletions docs/sections/tasks/email.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,48 +57,42 @@ $data = [
];
```

You can also assemble a Mailer object manually and pass that along as settings directly:
You can also assemble a Message object manually and pass that along as serialized settings array directly:
```php
$data = [
'settings' => $mailerObject,
'content' => $content,
'class' => \Cake\Mailer\Message::class,
'settings' => $messageObject->__serialize(),
'serialized' => true,
];
```
Deprecated: This is not recommended as it breaks as soon as the code changes.
You can also use the convenience method `EmailTask::serialize()` here.

Or send reusable Emails via the Mailer object:
It will not yet send emails here, only assemble them.
The Email Queue task triggers the `deliver()` method.

Or even pass it as serialized string:
```php
$data = [
'settings' => $mailerObject,
'action' => 'myReusableEmail', // instead of content or headers
'vars' => [$var1, $var2, $var3]
'class' => \Cake\Mailer\Message::class,
'settings' => serialize($messageObject),
'serialized' => true,
];
```
Deprecated: This is not recommended as it breaks as soon as the code changes.

Inside a controller you can for example do this for your mailers:
```php
$mailer = $this->getMailer('User');
$mailer->viewBuilder()
->setTemplate('register');
$mailer->set...(...);

$this->loadModel('Queue.QueuedJobs')->createJob(
'Queue.Email',
['settings' => $mailer]
);
```
Do not send your emails here, only assemble them. The Email Queue task triggers the `deliver()` method.

Note: In this case the object is stored serialized in the DB.
Note: In this last case the object is stored PHP serialized in the DB.
This can break when upgrading your core and the underlying class changes.
So make sure to only upgrade your code when all jobs have been finished.


## Using custom Email class
If you are not using CakePHP core Email task:

The recommended way for Email task together with JsonSerializer is using the FQCN class string of the Message class:

```php
use App\Mailer\Message; // or your custom FQCN

$data = [
'class' => Message::class,
'settings' => $settings,
Expand Down
1 change: 0 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ parameters:
- abort
ignoreErrors:
- '#Access to an undefined property Cake\\ORM\\BehaviorRegistry::\$Search#'
- '#Parameter \#1 \$object.+ of function is\_a expects object, class-string\<Cake\\Mailer\\Message\>\|object given.#'
- '#Negated boolean expression is always false.#'
- '#Parameter \#1 \$.+ of function call_user_func_array expects .+, array.+ given.#'

Expand Down
2 changes: 1 addition & 1 deletion src/Command/JobCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function execute(Arguments $args, ConsoleIo $io) {
/** @var array<\Queue\Model\Entity\QueuedJob> $jobs */
$jobs = $this->QueuedJobs->find()
->select(['id', 'job_task', 'completed', 'attempts'])
->orderDesc('id')
->orderByDesc('id')
->limit(20)->all()->toArray();
if ($jobs) {
$io->out('Last jobs are:');
Expand Down
2 changes: 1 addition & 1 deletion src/Command/WorkerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function execute(Arguments $args, ConsoleIo $io) {

/** @var array<\Queue\Model\Entity\QueueProcess> $processes */
$processes = $this->QueueProcesses->find()
->orderDesc('modified')
->orderByDesc('modified')
->limit(10)->all()->toArray();
if ($processes) {
$io->out('Last jobs are:');
Expand Down
2 changes: 1 addition & 1 deletion src/Model/Table/QueueProcessesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public function status(): array {

$results = $this->find()
->where(['modified >' => $thresholdTime])
->orderDesc('modified')
->orderByDesc('modified')
->enableHydration(false)
->all()
->toArray();
Expand Down
2 changes: 1 addition & 1 deletion src/Model/Table/QueuedJobsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ public function getFullStats(?string $jobTask = null): array {
->select($fields)
->where($conditions)
->enableHydration(false)
->orderDesc('id')
->orderByDesc('id')
->limit(static::STATS_LIMIT)
->all()
->toArray();
Expand Down
36 changes: 33 additions & 3 deletions src/Queue/Task/EmailTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class EmailTask extends Task implements AddInterface, AddFromBackendInterface {

public Mailer $mailer;

public Message $message;

/**
* List of default variables for Email class.
*
Expand Down Expand Up @@ -92,6 +94,25 @@ public function add(?string $data): void {
$this->io->out('Alternatively, you can pass the whole Mailer in `settings` key.');
}

/**
* @param \Cake\Mailer\Message $message
*
* @return array
*/
public static function serialize(Message $message): array {
return $message->__serialize();
}

/**
* @param \Cake\Mailer\Message $object
* @param array $config
*
* @return \Cake\Mailer\Message
*/
public static function unserialize(Message $object, array $config): Message {
return $object->createFromArray($config);
}

/**
* @param array<string, mixed> $data The array passed to QueuedJobsTable::createJob()
* @param int $jobId The id of the QueuedJob entity
Expand All @@ -108,13 +129,22 @@ public function run(array $data, int $jobId): void {

/** @var class-string<\Cake\Mailer\Message>|object|null $class */
$class = $data['class'] ?? null;
if ($class && (is_a($class, Message::class) || is_subclass_of($class, Message::class))) {
/** @var \Cake\Mailer\Message|null $object */
$object = $class ? new $class() : null;
if ($class && $object && (is_subclass_of($class, Message::class) || is_a($object, Message::class))) {
$settings = $data['settings'];
$serialized = $data['serialized'] ?? false;

if ($serialized) {
$this->message = is_array($settings) ? static::unserialize($object, $settings) : unserialize($settings);
} else {
/** @var class-string<\Cake\Mailer\Message> $class */
$this->message = new $class($settings);
}

$message = new $class($settings);
try {
$transport = TransportFactory::get($data['transport'] ?? 'default');
$result = $transport->send($message);
$result = $transport->send($this->message);
} catch (Throwable $e) {
$error = $e->getMessage();
$error .= ' (line ' . $e->getLine() . ' in ' . $e->getFile() . ')' . PHP_EOL . $e->getTraceAsString();
Expand Down
2 changes: 1 addition & 1 deletion tests/TestCase/Controller/Admin/QueueControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function testAddJob() {
$this->assertResponseCode(302);

/** @var \Queue\Model\Entity\QueuedJob $job */
$job = $jobsTable->find()->orderDesc('id')->firstOrFail();
$job = $jobsTable->find()->orderByDesc('id')->firstOrFail();
$this->assertSame('Queue.Example', $job->job_task);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public function testImport() {

$queuedJobs = $this->getTableLocator()->get('Queue.QueuedJobs');
/** @var \Queue\Model\Entity\QueuedJob $queuedJob */
$queuedJob = $queuedJobs->find()->orderDesc('id')->firstOrFail();
$queuedJob = $queuedJobs->find()->orderByDesc('id')->firstOrFail();

$this->assertSame('Webhook', $queuedJob->job_task);
$this->assertSame('web-hook-102803234', $queuedJob->reference);
Expand Down
90 changes: 87 additions & 3 deletions tests/TestCase/Queue/Task/EmailTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
use Cake\Core\Configure;
use Cake\Datasource\ConnectionManager;
use Cake\Mailer\Mailer;
use Cake\Mailer\Message;
use Cake\Mailer\Transport\DebugTransport;
use Cake\Mailer\TransportFactory;
use Cake\TestSuite\TestCase;
use Queue\Console\Io;
use Queue\Queue\Task\EmailTask;
use Queue\Utility\JsonSerializer;
use Queue\Utility\Serializer;
use Shim\TestSuite\ConsoleOutput;
use Shim\TestSuite\TestTrait;
use Tools\Mailer\Message as MailerMessage;
Expand All @@ -21,7 +24,7 @@ class EmailTaskTest extends TestCase {
use TestTrait;

/**
* @var array
* @var array<string>
*/
protected array $fixtures = [
'plugin.Queue.QueuedJobs',
Expand Down Expand Up @@ -70,10 +73,91 @@ public function testAdd() {
$queuedJobsTable = $this->getTableLocator()->get('Queue.QueuedJobs');

/** @var \Queue\Model\Entity\QueuedJob $queuedJob */
$queuedJob = $queuedJobsTable->find()->orderDesc('id')->firstOrFail();
$queuedJob = $queuedJobsTable->find()->orderByDesc('id')->firstOrFail();
$this->assertSame('Queue.Email', $queuedJob->job_task);
}

/**
* @return void
*/
public function testAddMessageSerialized() {
Configure::write('Queue.serializerClass', JsonSerializer::class);

$message = new Message();
$message
->setSubject('I haz Cake')
->setEmailFormat(Message::MESSAGE_BOTH)
->setBody([
Message::MESSAGE_TEXT => 'text message',
Message::MESSAGE_HTML => '<strong>html message</strong>',
]);

$data = [
'class' => Message::class,
'settings' => $message->__serialize(),
'serialized' => true,
];

/** @var \Queue\Model\Table\QueuedJobsTable $queuedJobsTable */
$queuedJobsTable = $this->getTableLocator()->get('Queue.QueuedJobs');
$queuedJobsTable->createJob('Email', $data);

/** @var \Queue\Model\Entity\QueuedJob $queuedJob */
$queuedJob = $queuedJobsTable->find()->orderByDesc('id')->firstOrFail();

$settings = Serializer::deserialize($queuedJob->data)['settings'];
$message = (new Message())->createFromArray($settings);

$this->assertSame('I haz Cake', $message->getSubject());

$serialized = EmailTask::serialize($message);
$message = EmailTask::unserialize(new Message(), $serialized);

$this->assertSame('I haz Cake', $message->getSubject());

$this->Task->run($data, 0);

$this->assertInstanceOf(Message::class, $this->Task->message);

Configure::delete('Queue.serializerClass');
}

/**
* @return void
*/
public function testAddMessagePhpSerialized() {
$message = new Message();
$message
->setSubject('I haz Cake')
->setEmailFormat(Message::MESSAGE_BOTH)
->setBody([
Message::MESSAGE_TEXT => 'text message',
Message::MESSAGE_HTML => '<strong>html message</strong>',
]);

$data = [
'class' => Message::class,
'settings' => serialize($message),
'serialized' => true,
];

/** @var \Queue\Model\Table\QueuedJobsTable $queuedJobsTable */
$queuedJobsTable = $this->getTableLocator()->get('Queue.QueuedJobs');
$queuedJobsTable->createJob('Email', $data);

/** @var \Queue\Model\Entity\QueuedJob $queuedJob */
$queuedJob = $queuedJobsTable->find()->orderByDesc('id')->firstOrFail();

$settings = Serializer::deserialize($queuedJob->data)['settings'];
$message = unserialize($settings);

$this->assertSame('I haz Cake', $message->getSubject());

$this->Task->run($data, 0);

$this->assertInstanceOf(Message::class, $this->Task->message);
}

/**
* @return void
*/
Expand Down Expand Up @@ -145,7 +229,7 @@ public function testRunToolsEmailMessageClassString() {
$queuedJobsTable = $this->getTableLocator()->get('Queue.QueuedJobs');
$queuedJobsTable->createJob('Queue.Email', ['class' => $class, 'settings' => $settings]);

$queuedJob = $queuedJobsTable->find()->orderDesc('id')->firstOrFail();
$queuedJob = $queuedJobsTable->find()->orderByDesc('id')->firstOrFail();
$data = unserialize($queuedJob->data);
/** @var \TestApp\Mailer\TestMailer $mailer */
$class = $data['class'];
Expand Down

0 comments on commit 5a1f127

Please sign in to comment.