Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions core/components/minishop3/src/Utils/EventGate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace MiniShop3\Utils;

use MODX\Revolution\modX;

/**
* Canonical helpers for MODX event returnedValues and cancellation signals.
*
* Contract (see modx-pro/MiniShop3#219):
* - Plugins may mutate by-ref event properties in $scriptProperties (legacy MS2 path).
* - Plugins may set $modx->event->returnedValues as an associative array; callers merge
* shallow keys into their working params via mergeReturnedValues().
* - For named payload channels (ImportCSV: params, data, tvData, …) use applyReturnedArray():
* list arrays replace the channel; associative arrays patch via array_replace().
* - Cancellation: plugin returns false or the string "cancel" in invokeEvent response.
* - success in buildInvokeResult() follows Utils::invokeEvent: empty aggregated message.
*/
final class EventGate
{
public static function clearReturnedValues(modX $modx): void
{
if (isset($modx->event->returnedValues)) {
$modx->event->returnedValues = null;
}
}

/**
* @return array<string, mixed>
*/
public static function getReturnedValues(modX $modx): array
{
$returnedValues = $modx->event->returnedValues ?? null;

return is_array($returnedValues) ? $returnedValues : [];
}

/**
* Shallow merge of returnedValues into caller params (Utils::invokeEvent path).
*
* @param array<string, mixed> $params
* @param array<string, mixed> $returnedValues
*
* @return array<string, mixed>
*/
public static function mergeReturnedValues(array $params, array $returnedValues): array
{
if ($returnedValues === []) {
return $params;
}

return array_merge($params, $returnedValues);
}

/**
* Apply a named returnedValues channel to a working array.
*
* @param array<string|int, mixed> $current
* @param array<string, mixed> $returnedValues
*
* @return array<string|int, mixed>
*/
public static function applyReturnedArray(array $current, array $returnedValues, string $key): array
{
$channel = $returnedValues[$key] ?? null;
if (!is_array($channel)) {
return $current;
}

return array_is_list($channel)
? $channel
: array_replace($current, $channel);
}

/**
* @param mixed $eventResult modX::invokeEvent() return value
*/
public static function isCancelled(mixed $eventResult): bool
{
if (!is_array($eventResult)) {
return false;
}

foreach ($eventResult as $result) {
if ($result === false || $result === 'cancel') {
return true;
}
}

return false;
}

/**
* @param mixed $response modX::invokeEvent() return value
*/
public static function normalizeMessage(mixed $response, string $glue = '<br/>'): string
{
if (!is_array($response)) {
return trim((string)$response);
}

if (count($response) > 1) {
$response = array_filter($response, static fn($value) => !empty($value));
}

return implode($glue, $response);
}

/**
* Standard invokeEvent result for Order/Cart and other Utils callers.
*
* @param array<string, mixed> $params
* @param array<string, mixed> $returnedValues
*
* @return array{success: bool, message: string, data: array<string, mixed>, values: array<string, mixed>}
*/
public static function buildInvokeResult(array $params, array $returnedValues, string $message): array
{
return [
'success' => empty($message),
'message' => $message,
'data' => self::mergeReturnedValues($params, $returnedValues),
'values' => $returnedValues,
];
}
}
62 changes: 11 additions & 51 deletions core/components/minishop3/src/Utils/ImportCSV.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,14 @@ public function process(array $params): array
// for plugins that mutate $scriptProperties['params'] directly.
// 2) $modx->event->returnedValues['params'] — explicit channel from
// #219/#245 for plugins that prefer the returned-values contract.
$this->clearEventReturnedValues();
EventGate::clearReturnedValues($this->modx);
$eventResult = $this->modx->invokeEvent('msOnBeforeImport', [
'file' => $this->params['file'],
'params' => &$this->params,
]);
$this->params = $this->applyReturnedArray($this->params, $this->getEventReturnedValues(), 'params');
if ($this->isEventCancelled($eventResult)) {
$returnedValues = EventGate::getReturnedValues($this->modx);
$this->params = EventGate::applyReturnedArray($this->params, $returnedValues, 'params');
if (EventGate::isCancelled($eventResult)) {
$error = $this->modx->lexicon('ms3_utilities_import_cancelled');
return $this->ms3->utils->error($error);
}
Expand Down Expand Up @@ -368,7 +369,7 @@ private function processRow(array $csv): bool
// 2) $modx->event->returnedValues['data'|'tvData'|'optionData'|'gallery']
// — explicit channel from #219/#245 for plugins that prefer the
// returned-values contract.
$this->clearEventReturnedValues();
EventGate::clearReturnedValues($this->modx);
$eventResult = $this->modx->invokeEvent('msOnImportRow', [
'row' => $this->rows,
'csv' => $csv,
Expand All @@ -377,12 +378,12 @@ private function processRow(array $csv): bool
'optionData' => &$optionData,
'gallery' => &$gallery,
]);
$returnedValues = $this->getEventReturnedValues();
$data = $this->applyReturnedArray($data, $returnedValues, 'data');
$tvData = $this->applyReturnedArray($tvData, $returnedValues, 'tvData');
$optionData = $this->applyReturnedArray($optionData, $returnedValues, 'optionData');
$gallery = $this->applyReturnedArray($gallery, $returnedValues, 'gallery');
if ($this->isEventCancelled($eventResult)) {
$returnedValues = EventGate::getReturnedValues($this->modx);
$data = EventGate::applyReturnedArray($data, $returnedValues, 'data');
$tvData = EventGate::applyReturnedArray($tvData, $returnedValues, 'tvData');
$optionData = EventGate::applyReturnedArray($optionData, $returnedValues, 'optionData');
$gallery = EventGate::applyReturnedArray($gallery, $returnedValues, 'gallery');
if (EventGate::isCancelled($eventResult)) {
$this->skipped++;
return true;
}
Expand Down Expand Up @@ -694,47 +695,6 @@ private function processGallery(array $resource, array $gallery): void
}
}

/**
* Check if event returned cancel signal
*/
private function isEventCancelled($eventResult): bool
{
if (is_array($eventResult)) {
foreach ($eventResult as $result) {
if ($result === false || $result === 'cancel') {
return true;
}
}
}
return false;
}

private function clearEventReturnedValues(): void
{
if (isset($this->modx->event->returnedValues)) {
$this->modx->event->returnedValues = null;
}
}

private function getEventReturnedValues(): array
{
return isset($this->modx->event->returnedValues) && is_array($this->modx->event->returnedValues)
? $this->modx->event->returnedValues
: [];
}

private function applyReturnedArray(array $current, array $returnedValues, string $key): array
{
if (!isset($returnedValues[$key]) || !is_array($returnedValues[$key])) {
return $current;
}

// Lists are complete replacements; associative arrays may patch existing keys.
return array_is_list($returnedValues[$key])
? $returnedValues[$key]
: array_replace($current, $returnedValues[$key]);
}

/**
* Count rows in CSV file
*/
Expand Down
34 changes: 9 additions & 25 deletions core/components/minishop3/src/Utils/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,39 +64,23 @@ public function updateSetting($key, $value)
}

/**
* Shorthand for original modX::invokeEvent() method with some useful additions.
* Shorthand for original modX::invokeEvent() with normalized returnedValues handling.
*
* @param $eventName
* @param array $params
* @param $glue
* @param string $eventName
* @param array<string, mixed> $params
* @param string $glue
*
* @return array
* @return array{success: bool, message: string, data: array<string, mixed>, values: array<string, mixed>}
*/
public function invokeEvent($eventName, array $params = [], $glue = '<br/>')
{
if (isset($this->modx->event->returnedValues)) {
$this->modx->event->returnedValues = null;
}
EventGate::clearReturnedValues($this->modx);

$response = $this->modx->invokeEvent($eventName, $params);
if (is_array($response) && count($response) > 1) {
foreach ($response as $k => $v) {
if (empty($v)) {
unset($response[$k]);
}
}
}
$message = EventGate::normalizeMessage($response, $glue);
$returnedValues = EventGate::getReturnedValues($this->modx);

$message = is_array($response) ? implode($glue, $response) : trim((string)$response);
if (isset($this->modx->event->returnedValues) && is_array($this->modx->event->returnedValues)) {
$params = array_merge($params, $this->modx->event->returnedValues);
}

return [
'success' => empty($message),
'message' => $message,
'data' => $params,
];
return EventGate::buildInvokeResult($params, $returnedValues, $message);
}

/**
Expand Down
86 changes: 86 additions & 0 deletions core/components/minishop3/tests/EventGateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* Static checks for EventGate returnedValues helpers (without MODX).
*
* Run: php tests/EventGateTest.php
*/

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use MiniShop3\Utils\EventGate;

$fail = static function (string $message): never {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
};

$assertSame = static function ($expected, $actual, string $case) use ($fail): void {
if ($actual !== $expected) {
$fail($case . ': expected ' . var_export($expected, true) . ', got ' . var_export($actual, true));
}
};

$assertSame(
['a' => 1, 'b' => 2],
EventGate::mergeReturnedValues(['a' => 1], ['b' => 2]),
'mergeReturnedValues shallow merge'
);

$assertSame(
['a' => 1],
EventGate::mergeReturnedValues(['a' => 1], []),
'mergeReturnedValues empty returnedValues'
);

$assertSame(
['foo' => 'bar', 'price' => 99],
EventGate::applyReturnedArray(
['foo' => 'bar', 'price' => 10],
['data' => ['price' => 99]],
'data'
),
'applyReturnedArray associative patch'
);

$assertSame(
['one', 'two'],
EventGate::applyReturnedArray(
['old'],
['data' => ['one', 'two']],
'data'
),
'applyReturnedArray list replacement'
);

$assertSame(
['foo' => 'bar'],
EventGate::applyReturnedArray(['foo' => 'bar'], ['other' => ['x' => 1]], 'data'),
'applyReturnedArray missing channel'
);

$assertSame(true, EventGate::isCancelled([false]), 'isCancelled false value');
$assertSame(true, EventGate::isCancelled(['cancel']), 'isCancelled cancel string');
$assertSame(false, EventGate::isCancelled(['ok', 'fine']), 'isCancelled non-cancel responses');
$assertSame(false, EventGate::isCancelled('error'), 'isCancelled non-array response');

$assertSame('', EventGate::normalizeMessage([]), 'normalizeMessage empty array');
$assertSame('a<br/>b', EventGate::normalizeMessage(['a', 'b']), 'normalizeMessage array glue');
$assertSame('single', EventGate::normalizeMessage('single'), 'normalizeMessage string');
$assertSame('a<br/>b', EventGate::normalizeMessage(['a', '', 'b']), 'normalizeMessage filters empty parts');

$result = EventGate::buildInvokeResult(['x' => 1], ['y' => 2], '');
$assertSame(true, $result['success'], 'buildInvokeResult success when message empty');
$assertSame(['x' => 1, 'y' => 2], $result['data'], 'buildInvokeResult merged data');
$assertSame(['y' => 2], $result['values'], 'buildInvokeResult values key');

$result = EventGate::buildInvokeResult(['x' => 1], [], 'blocked');
$assertSame(false, $result['success'], 'buildInvokeResult failure when message set');

$result = EventGate::buildInvokeResult(['x' => 1], [], '0');
$assertSame(true, $result['success'], 'buildInvokeResult success matches legacy empty() check');

fwrite(STDOUT, "OK EventGateTest\n");
exit(0);