Skip to content

Commit

Permalink
Allow payload data modification.
Browse files Browse the repository at this point in the history
  • Loading branch information
dereuromark committed Nov 18, 2024
1 parent ab9a6a3 commit 1a09ca6
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 7 deletions.
28 changes: 24 additions & 4 deletions src/Controller/Admin/QueuedJobsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,27 @@ public function edit(?int $id = null) {
* @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
*/
public function data(?int $id = null) {
return $this->edit($id);
$this->QueuedJobs->addBehavior('Queue.Jsonable', ['input' => 'json', 'fields' => ['data'], 'map' => ['data_string']]);

$queuedJob = $this->QueuedJobs->get($id);
if ($queuedJob->completed) {
$this->Flash->error(__d('queue', 'The queued job is already completed.'));

return $this->redirect(['action' => 'view', $id]);
}

if ($this->request->is(['patch', 'post', 'put'])) {
$queuedJob = $this->QueuedJobs->patchEntity($queuedJob, $this->request->getData());
if ($this->QueuedJobs->save($queuedJob)) {
$this->Flash->success(__d('queue', 'The queued job has been saved.'));

return $this->redirect(['action' => 'view', $id]);
}

$this->Flash->error(__d('queue', 'The queued job could not be saved. Please try again.'));
}

$this->set(compact('queuedJob'));
}

/**
Expand Down Expand Up @@ -298,10 +318,10 @@ public function test() {
$allTasks = $taskFinder->all();
$tasks = [];
foreach ($allTasks as $task => $className) {
if (substr($task, 0, 6) !== 'Queue.') {
if (!str_starts_with($task, 'Queue.')) {
continue;
}
if (substr($task, -7) !== 'Example') {
if (!str_ends_with($task, 'Example')) {
continue;
}

Expand Down Expand Up @@ -349,7 +369,7 @@ public function migrate() {

$tasks = [];
foreach ($allTasks as $task => $className) {
if (strpos($task, 'Queue.') !== 0) {
if (!str_starts_with($task, 'Queue.')) {
continue;
}

Expand Down
225 changes: 225 additions & 0 deletions src/Model/Behavior/JsonableBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?php

namespace Queue\Model\Behavior;

use ArrayObject;
use Cake\Collection\CollectionInterface;
use Cake\Database\TypeFactory;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query\SelectQuery;
use InvalidArgumentException;
use RuntimeException;
use Shim\Database\Type\ArrayType;

/**
* A behavior that will json_encode (and json_decode) fields if they contain an array or specific pattern.
*
* @author Mark Scherer
* @license MIT
*/
class JsonableBehavior extends Behavior {

/**
* @var array<string, mixed>
*/
protected array $_defaultConfig = [
'fields' => [], // Fields to convert
'input' => 'array', // json, array, param, list (param/list only works with specific fields)
'output' => 'array', // json, array, param, list (param/list only works with specific fields)
'separator' => '|', // only for param or list
'keyValueSeparator' => ':', // only for param
'leftBound' => '{', // only for list
'rightBound' => '}', // only for list
'map' => [], // map on a different DB field
'encodeParams' => [ // params for json_encode
'options' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
'depth' => 512,
],
'decodeParams' => [ // params for json_decode
'assoc' => true, // useful when working with multidimensional arrays
'depth' => 512,
'options' => JSON_THROW_ON_ERROR,
],
];

/**
* @param array $config
*
* @throws \RuntimeException
*
* @return void
*/
public function initialize(array $config): void {
if (empty($this->_config['fields'])) {
throw new RuntimeException('Fields are required');
}
if (!is_array($this->_config['fields'])) {
$this->_config['fields'] = (array)$this->_config['fields'];
}
if (!is_array($this->_config['map'])) {
$this->_config['map'] = (array)$this->_config['map'];
}
if (!empty($this->_config['map']) && count($this->_config['fields']) !== count($this->_config['map'])) {
throw new RuntimeException('Fields and Map need to be of the same length if map is specified.');
}
foreach ($this->_config['fields'] as $field) {
$this->_table->getSchema()->setColumnType($field, 'array');
}
if ($this->_config['encodeParams']['options'] === null) {
$options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_ERROR_INF_OR_NAN | JSON_PARTIAL_OUTPUT_ON_ERROR;
$this->_config['encodeParams']['options'] = $options;
}

TypeFactory::map('array', ArrayType::class);
}

/**
* Decode the fields on after find
*
* @param \Cake\Event\EventInterface $event
* @param \Cake\ORM\Query\SelectQuery $query
* @param \ArrayObject $options
* @param bool $primary
*
* @return void
*/
public function beforeFind(EventInterface $event, SelectQuery $query, ArrayObject $options, bool $primary) {
$query->formatResults(function (CollectionInterface $results) {
return $results->map(function ($row) {
if (!$row instanceof Entity) {
return $row;
}

$this->decodeItems($row);

return $row;
});
});
}

/**
* Decodes the fields of an array/entity (if the value itself was encoded)
*
* @param \Cake\Datasource\EntityInterface $entity
*
* @return void
*/
public function decodeItems(EntityInterface $entity) {
$fields = $this->_getMappedFields();

foreach ($fields as $map => $field) {
$val = $entity->get($field);
if (is_string($val)) {
$val = $this->_fromJson($val);
}
$entity->set($map, $this->_decode($val));
}
}

/**
* Saves all fields that do not belong to the current Model into 'with' helper model.
*
* @param \Cake\Event\EventInterface $event
* @param \Cake\Datasource\EntityInterface $entity
* @param \ArrayObject $options
*
* @return void
*/
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) {
$fields = $this->_getMappedFields();

foreach ($fields as $map => $field) {
if ($entity->get($map) === null) {
continue;
}
$val = $entity->get($map);
$entity->set($field, $this->_encode($val));
}
}

/**
* @return array
*/
protected function _getMappedFields() {
$usedFields = $this->_config['fields'];
$mappedFields = $this->_config['map'];
if (!$mappedFields) {
$mappedFields = $usedFields;
}

$fields = [];

foreach ($mappedFields as $index => $map) {
if (!$map || $map == $usedFields[$index]) {
$fields[$usedFields[$index]] = $usedFields[$index];

continue;
}
$fields[$map] = $usedFields[$index];
}

return $fields;
}

/**
* @param array|string $val
*
* @return string|null
*/
public function _encode($val) {
if (!empty($this->_config['fields'])) {
if ($this->_config['input'] === 'json') {
if (!is_string($val)) {
throw new InvalidArgumentException('Only accepts JSON string for input type `json`');
}
$val = $this->_fromJson($val);
}
}
if (!is_array($val)) {
return null;
}

$result = json_encode($val, $this->_config['encodeParams']['options'], $this->_config['encodeParams']['depth']);
if ($result === false) {
return null;
}

return $result;
}

/**
* Fields are absolutely necessary to function properly!
*
* @param array|null $val
*
* @return string|null
*/
public function _decode($val) {
if (!is_array($val)) {
return null;
}

$flags = $this->_config['encodeParams']['options'] | JSON_PRETTY_PRINT;
$decoded = json_encode($val, $flags, $this->_config['decodeParams']['depth']);
if ($decoded === false) {
return null;
}

return $decoded;
}

/**
* @param string $val
*
* @return array
*/
protected function _fromJson(string $val): array {
$json = json_decode($val, true, JSON_THROW_ON_ERROR);

return $json;
}

}
2 changes: 1 addition & 1 deletion templates/Admin/QueuedJobs/data.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<fieldset>
<legend><?= __d('queue', 'Edit Queued Job Payload') ?></legend>
<?php
echo $this->Form->control('data', ['rows' => 20]);
echo $this->Form->control('data_string', ['rows' => 20]);
?>
</fieldset>
<?= $this->Form->button(__d('queue', 'Submit')) ?>
Expand Down
31 changes: 29 additions & 2 deletions tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function testEditPost() {

$this->assertResponseCode(302);

$queuedJobs = $this->getTableLocator()->get('Queue.QueuedJobs');
$queuedJobs = $this->fetchTable('Queue.QueuedJobs');
/** @var \Queue\Model\Entity\QueuedJob $modifiedJob */
$modifiedJob = $queuedJobs->get($job->id);
$this->assertSame(8, $modifiedJob->priority);
Expand All @@ -97,13 +97,40 @@ public function testEditPost() {
* @return void
*/
public function testData() {
$job = $this->createJob();
$job = $this->createJob(['data' => '{"verbose":true,"count":22,"string":"string"}']);

$this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'data', $job->id]);

$this->assertResponseCode(200);
}

/**
* @return void
*/
public function testDataPost() {
$job = $this->createJob();

$data = [
'data_string' => <<<JSON
{
"class": "App\\\\Command\\\\RealNotificationCommand",
"args": [
"--verbose",
"-d"
]
}
JSON,
];
$this->post(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'data', $job->id], $data);

$this->assertResponseCode(302);

/** @var \Queue\Model\Entity\QueuedJob $job */
$job = $this->fetchTable('Queue.QueuedJobs')->get($job->id);
$expected = '{"class":"App\\\\Command\\\\RealNotificationCommand","args":["--verbose","-d"]}';
$this->assertSame($expected, $job->data);
}

/**
* Test index method
*
Expand Down

0 comments on commit 1a09ca6

Please sign in to comment.