Skip to content

Commit 1a09ca6

Browse files
committed
Allow payload data modification.
1 parent ab9a6a3 commit 1a09ca6

File tree

4 files changed

+279
-7
lines changed

4 files changed

+279
-7
lines changed

src/Controller/Admin/QueuedJobsController.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,27 @@ public function edit(?int $id = null) {
229229
* @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
230230
*/
231231
public function data(?int $id = null) {
232-
return $this->edit($id);
232+
$this->QueuedJobs->addBehavior('Queue.Jsonable', ['input' => 'json', 'fields' => ['data'], 'map' => ['data_string']]);
233+
234+
$queuedJob = $this->QueuedJobs->get($id);
235+
if ($queuedJob->completed) {
236+
$this->Flash->error(__d('queue', 'The queued job is already completed.'));
237+
238+
return $this->redirect(['action' => 'view', $id]);
239+
}
240+
241+
if ($this->request->is(['patch', 'post', 'put'])) {
242+
$queuedJob = $this->QueuedJobs->patchEntity($queuedJob, $this->request->getData());
243+
if ($this->QueuedJobs->save($queuedJob)) {
244+
$this->Flash->success(__d('queue', 'The queued job has been saved.'));
245+
246+
return $this->redirect(['action' => 'view', $id]);
247+
}
248+
249+
$this->Flash->error(__d('queue', 'The queued job could not be saved. Please try again.'));
250+
}
251+
252+
$this->set(compact('queuedJob'));
233253
}
234254

235255
/**
@@ -298,10 +318,10 @@ public function test() {
298318
$allTasks = $taskFinder->all();
299319
$tasks = [];
300320
foreach ($allTasks as $task => $className) {
301-
if (substr($task, 0, 6) !== 'Queue.') {
321+
if (!str_starts_with($task, 'Queue.')) {
302322
continue;
303323
}
304-
if (substr($task, -7) !== 'Example') {
324+
if (!str_ends_with($task, 'Example')) {
305325
continue;
306326
}
307327

@@ -349,7 +369,7 @@ public function migrate() {
349369

350370
$tasks = [];
351371
foreach ($allTasks as $task => $className) {
352-
if (strpos($task, 'Queue.') !== 0) {
372+
if (!str_starts_with($task, 'Queue.')) {
353373
continue;
354374
}
355375

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
3+
namespace Queue\Model\Behavior;
4+
5+
use ArrayObject;
6+
use Cake\Collection\CollectionInterface;
7+
use Cake\Database\TypeFactory;
8+
use Cake\Datasource\EntityInterface;
9+
use Cake\Event\EventInterface;
10+
use Cake\ORM\Behavior;
11+
use Cake\ORM\Entity;
12+
use Cake\ORM\Query\SelectQuery;
13+
use InvalidArgumentException;
14+
use RuntimeException;
15+
use Shim\Database\Type\ArrayType;
16+
17+
/**
18+
* A behavior that will json_encode (and json_decode) fields if they contain an array or specific pattern.
19+
*
20+
* @author Mark Scherer
21+
* @license MIT
22+
*/
23+
class JsonableBehavior extends Behavior {
24+
25+
/**
26+
* @var array<string, mixed>
27+
*/
28+
protected array $_defaultConfig = [
29+
'fields' => [], // Fields to convert
30+
'input' => 'array', // json, array, param, list (param/list only works with specific fields)
31+
'output' => 'array', // json, array, param, list (param/list only works with specific fields)
32+
'separator' => '|', // only for param or list
33+
'keyValueSeparator' => ':', // only for param
34+
'leftBound' => '{', // only for list
35+
'rightBound' => '}', // only for list
36+
'map' => [], // map on a different DB field
37+
'encodeParams' => [ // params for json_encode
38+
'options' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
39+
'depth' => 512,
40+
],
41+
'decodeParams' => [ // params for json_decode
42+
'assoc' => true, // useful when working with multidimensional arrays
43+
'depth' => 512,
44+
'options' => JSON_THROW_ON_ERROR,
45+
],
46+
];
47+
48+
/**
49+
* @param array $config
50+
*
51+
* @throws \RuntimeException
52+
*
53+
* @return void
54+
*/
55+
public function initialize(array $config): void {
56+
if (empty($this->_config['fields'])) {
57+
throw new RuntimeException('Fields are required');
58+
}
59+
if (!is_array($this->_config['fields'])) {
60+
$this->_config['fields'] = (array)$this->_config['fields'];
61+
}
62+
if (!is_array($this->_config['map'])) {
63+
$this->_config['map'] = (array)$this->_config['map'];
64+
}
65+
if (!empty($this->_config['map']) && count($this->_config['fields']) !== count($this->_config['map'])) {
66+
throw new RuntimeException('Fields and Map need to be of the same length if map is specified.');
67+
}
68+
foreach ($this->_config['fields'] as $field) {
69+
$this->_table->getSchema()->setColumnType($field, 'array');
70+
}
71+
if ($this->_config['encodeParams']['options'] === null) {
72+
$options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_ERROR_INF_OR_NAN | JSON_PARTIAL_OUTPUT_ON_ERROR;
73+
$this->_config['encodeParams']['options'] = $options;
74+
}
75+
76+
TypeFactory::map('array', ArrayType::class);
77+
}
78+
79+
/**
80+
* Decode the fields on after find
81+
*
82+
* @param \Cake\Event\EventInterface $event
83+
* @param \Cake\ORM\Query\SelectQuery $query
84+
* @param \ArrayObject $options
85+
* @param bool $primary
86+
*
87+
* @return void
88+
*/
89+
public function beforeFind(EventInterface $event, SelectQuery $query, ArrayObject $options, bool $primary) {
90+
$query->formatResults(function (CollectionInterface $results) {
91+
return $results->map(function ($row) {
92+
if (!$row instanceof Entity) {
93+
return $row;
94+
}
95+
96+
$this->decodeItems($row);
97+
98+
return $row;
99+
});
100+
});
101+
}
102+
103+
/**
104+
* Decodes the fields of an array/entity (if the value itself was encoded)
105+
*
106+
* @param \Cake\Datasource\EntityInterface $entity
107+
*
108+
* @return void
109+
*/
110+
public function decodeItems(EntityInterface $entity) {
111+
$fields = $this->_getMappedFields();
112+
113+
foreach ($fields as $map => $field) {
114+
$val = $entity->get($field);
115+
if (is_string($val)) {
116+
$val = $this->_fromJson($val);
117+
}
118+
$entity->set($map, $this->_decode($val));
119+
}
120+
}
121+
122+
/**
123+
* Saves all fields that do not belong to the current Model into 'with' helper model.
124+
*
125+
* @param \Cake\Event\EventInterface $event
126+
* @param \Cake\Datasource\EntityInterface $entity
127+
* @param \ArrayObject $options
128+
*
129+
* @return void
130+
*/
131+
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) {
132+
$fields = $this->_getMappedFields();
133+
134+
foreach ($fields as $map => $field) {
135+
if ($entity->get($map) === null) {
136+
continue;
137+
}
138+
$val = $entity->get($map);
139+
$entity->set($field, $this->_encode($val));
140+
}
141+
}
142+
143+
/**
144+
* @return array
145+
*/
146+
protected function _getMappedFields() {
147+
$usedFields = $this->_config['fields'];
148+
$mappedFields = $this->_config['map'];
149+
if (!$mappedFields) {
150+
$mappedFields = $usedFields;
151+
}
152+
153+
$fields = [];
154+
155+
foreach ($mappedFields as $index => $map) {
156+
if (!$map || $map == $usedFields[$index]) {
157+
$fields[$usedFields[$index]] = $usedFields[$index];
158+
159+
continue;
160+
}
161+
$fields[$map] = $usedFields[$index];
162+
}
163+
164+
return $fields;
165+
}
166+
167+
/**
168+
* @param array|string $val
169+
*
170+
* @return string|null
171+
*/
172+
public function _encode($val) {
173+
if (!empty($this->_config['fields'])) {
174+
if ($this->_config['input'] === 'json') {
175+
if (!is_string($val)) {
176+
throw new InvalidArgumentException('Only accepts JSON string for input type `json`');
177+
}
178+
$val = $this->_fromJson($val);
179+
}
180+
}
181+
if (!is_array($val)) {
182+
return null;
183+
}
184+
185+
$result = json_encode($val, $this->_config['encodeParams']['options'], $this->_config['encodeParams']['depth']);
186+
if ($result === false) {
187+
return null;
188+
}
189+
190+
return $result;
191+
}
192+
193+
/**
194+
* Fields are absolutely necessary to function properly!
195+
*
196+
* @param array|null $val
197+
*
198+
* @return string|null
199+
*/
200+
public function _decode($val) {
201+
if (!is_array($val)) {
202+
return null;
203+
}
204+
205+
$flags = $this->_config['encodeParams']['options'] | JSON_PRETTY_PRINT;
206+
$decoded = json_encode($val, $flags, $this->_config['decodeParams']['depth']);
207+
if ($decoded === false) {
208+
return null;
209+
}
210+
211+
return $decoded;
212+
}
213+
214+
/**
215+
* @param string $val
216+
*
217+
* @return array
218+
*/
219+
protected function _fromJson(string $val): array {
220+
$json = json_decode($val, true, JSON_THROW_ON_ERROR);
221+
222+
return $json;
223+
}
224+
225+
}

templates/Admin/QueuedJobs/data.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<fieldset>
1919
<legend><?= __d('queue', 'Edit Queued Job Payload') ?></legend>
2020
<?php
21-
echo $this->Form->control('data', ['rows' => 20]);
21+
echo $this->Form->control('data_string', ['rows' => 20]);
2222
?>
2323
</fieldset>
2424
<?= $this->Form->button(__d('queue', 'Submit')) ?>

tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public function testEditPost() {
8787

8888
$this->assertResponseCode(302);
8989

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

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

104104
$this->assertResponseCode(200);
105105
}
106106

107+
/**
108+
* @return void
109+
*/
110+
public function testDataPost() {
111+
$job = $this->createJob();
112+
113+
$data = [
114+
'data_string' => <<<JSON
115+
{
116+
"class": "App\\\\Command\\\\RealNotificationCommand",
117+
"args": [
118+
"--verbose",
119+
"-d"
120+
]
121+
}
122+
JSON,
123+
];
124+
$this->post(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'data', $job->id], $data);
125+
126+
$this->assertResponseCode(302);
127+
128+
/** @var \Queue\Model\Entity\QueuedJob $job */
129+
$job = $this->fetchTable('Queue.QueuedJobs')->get($job->id);
130+
$expected = '{"class":"App\\\\Command\\\\RealNotificationCommand","args":["--verbose","-d"]}';
131+
$this->assertSame($expected, $job->data);
132+
}
133+
107134
/**
108135
* Test index method
109136
*

0 commit comments

Comments
 (0)