Improvements/nested input sync methods (#119)

* New nested property input sync lifecycle hooks

---------

Co-authored-by: Willem Poortman <git@wpoortman.nl>
This commit is contained in:
Willem PoortmanandWillem Poortman authored and GitHub committed 2023-08-03 13:41:01 +02:00
1 parent 9f282a3c69
commit d7269575dd
8 files changed
+203 -49

No files matched your search

+16
View File
@@ -378,6 +378,14 @@ class Explanation extends \Magewirephp\Magewire\Component
{
public $nested = ['foo' => ['bar' => 'Hello world']];
// Since v1.10.6: Before nested array properties are updating.
public function updatingNested(array $value): array
{
// Make sure "nested.foo.bar" is always strtolower (helloworld)
$value['foo']['bar'] = strtolower($value['foo']['bar']);
return $value;
}
// Before it's getting updated.
public function updatingNestedFooBar(string $value): string
{
@@ -391,6 +399,14 @@ class Explanation extends \Magewirephp\Magewire\Component
// Returns ['foo' => ['bar' => 'hello world']]
return strtolower($value);
}
// Since v1.10.6: After nested array properties are updated.
public function updatedNested(array $value): array
{
// Evantually strtoupper "nested.foo.bar" again (HELLOWORLD).
$value['foo']['bar'] = strtoupper($value['foo']['bar']);
return $value;
}
}
```
+5 -4
View File
@@ -39,8 +39,8 @@ class Property
throw new ComponentException(__('Public property %1 does\'nt exist', [$property]));
}
$path = substr(strstr($path, '.'), 1);
$data = $this->arrayManager->set($path, $component->{$property}, $value, '.');
$path = substr(strstr($path, '.'), 1);
$data = $this->arrayManager->set($path, $component->{$property}, $value, '.');
return compact('property', 'data', 'realpath', 'path');
}
@@ -54,15 +54,16 @@ class Property
* Use a callback function to assign component property
* values except default reserved properties.
*/
public function assign(callable $callback, Component $component, array $data = null): void
public function assign(callable $callback, Component $component, array $data = null, bool $merge = true): void
{
$publicProperties = $component->getPublicProperties(true);
$data = $data === null ? $publicProperties : array_merge($publicProperties, $data);
$data = $data === null ? $publicProperties : ($merge ? array_merge($publicProperties, $data) : $data);
foreach ($data as $property => $value) {
if (in_array($property, Component::RESERVED_PROPERTIES, true)) {
continue;
}
if (array_key_exists($property, $publicProperties)) {
$callback($component, $property, $value);
}
+29
View File
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/**
* Copyright © Willem Poortman 2021-present. All rights reserved.
*
* Please read the README and LICENSE files for more
* details on copyrights and license information.
*/
namespace Magewirephp\Magewire\Model;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\View\Layout;
use Magewirephp\Magewire\Component;
use Psr\Log\LoggerInterface;
abstract class Action implements ActionInterface
{
abstract public function handle(Component $component, array $payload);
public function inspect(Component $component, array $updates): bool
{
return true;
}
public function evaluate(Component $component, array $updates)
{
return true;
}
}
+2 -2
View File
@@ -11,11 +11,11 @@ namespace Magewirephp\Magewire\Model\Action;
use Magento\Framework\Exception\LocalizedException;
use Magewirephp\Magewire\Exception\ComponentActionException;
use Magewirephp\Magewire\Component;
use Magewirephp\Magewire\Model\Action;
use Magewirephp\Magewire\Model\Action\Type\Factory as TypeFactory;
use Magewirephp\Magewire\Model\Action\Type\Magic;
use Magewirephp\Magewire\Model\ActionInterface;
class CallMethod implements ActionInterface
class CallMethod extends Action
{
public const ACTION = 'callMethod';
+2 -2
View File
@@ -11,10 +11,10 @@ namespace Magewirephp\Magewire\Model\Action;
use Magento\Framework\Exception\LocalizedException;
use Magewirephp\Magewire\Exception\ComponentActionException;
use Magewirephp\Magewire\Component;
use Magewirephp\Magewire\Model\ActionInterface;
use Magewirephp\Magewire\Model\Action;
use Magewirephp\Magewire\Model\Hydrator\Listener as ListenerHydrator;
class FireEvent implements ActionInterface
class FireEvent extends Action
{
public const ACTION = 'fireEvent';
+102 -29
View File
@@ -10,13 +10,14 @@ namespace Magewirephp\Magewire\Model\Action;
use Exception;
use Magewirephp\Magewire\Exception\ComponentActionException;
use Magewirephp\Magewire\Exception\ComponentException;
use Magewirephp\Magewire\Component;
use Magewirephp\Magewire\Exception\ValidationException;
use Magewirephp\Magewire\Helper\Property as PropertyHelper;
use Magewirephp\Magewire\Model\ActionInterface;
use Magewirephp\Magewire\Model\Action;
use Psr\Log\LoggerInterface;
class SyncInput implements ActionInterface
class SyncInput extends Action
{
public const ACTION = 'syncInput';
@@ -31,56 +32,128 @@ class SyncInput implements ActionInterface
$this->logger = $logger;
}
public function inspect(Component $component, array $updates): bool
{
foreach ($this->reduceProperty($component, $updates) as $property => $value) {
$this->assign($component, $property, $this->updating($component, $property, $value));
};
return parent::inspect($component, $updates);
}
/**
* @throws ComponentActionException
*/
public function handle(Component $component, array $payload)
{
if (!isset($payload['name'], $payload['value'])) {
if (! isset($payload['name'], $payload['value'])) {
throw new ComponentActionException(__('Invalid update payload'));
}
// Grep the required type elements from the action payload.
$property = $payload['name'];
$value = $payload['value'];
try {
$containsDots = $this->propertyHelper->containsDots($property);
$nested = $this->propertyHelper->containsDots($property);
$transform = [];
if ($containsDots) {
if ($nested) {
// Full property value including its new payload value.
$transform = $this->propertyHelper->transformDots($property, $value, $component);
// Search for the newly set property value by path.
$value = $this->propertyHelper->searchViaDots($transform['path'], $transform['data']);
}
// Prepare lifecycle methods.
$before = 'updating' . str_replace(' ', '', ucwords(str_replace(['-', '_', '.'], ' ', $property)));
$after = str_replace('updating', 'updated', $before);
// Assign 'updating' result in the middle and re-assign the final result at the end.
$methods = [$before, 'updating', 'assign', 'updated', $after, 'assign'];
// Try to run existing pre-assignment methods if they exist.
$value = $this->updating($component, $property, $value);
foreach ($methods as $method) {
if ($method === 'assign') {
if ($containsDots) {
$component->{$transform['property']} = $transform['data'];
} else {
$component->{$property} = $value;
}
} elseif (method_exists($component, $method)) {
try {
$value = $component->{$method}($value, $property);
if ($containsDots) {
// Put the new value in its original nested spot.
$transform = $this->propertyHelper->transformDots($property, $value, $component);
}
} catch (ValidationException $exception) {
$this->logger->critical('Magewire:' . $exception->getMessage());
}
}
if ($nested) {
$transform = $this->propertyHelper->transformDots($property, $value, $component);
}
// Regular value assignment.
$this->assign($component, $transform ? $transform['property'] : $property, $transform ? $transform['data'] : $value);
// Try to run post-assignment methods if they exist.
$value = $this->updated($component, $property, $value);
if ($nested) {
$transform = $this->propertyHelper->transformDots($property, $value, $component);
}
// Re-assign the final value onto the component property.
$this->assign($component, $transform ? $transform['property'] : $property, $transform ? $transform['data'] : $value);
} catch (Exception $exception) {
$this->logger->critical(
__sprintf('Magewire: Something went wrong while syncing property "%s" onto component "%s"', [$property, $component->name]),
['exception' => $exception]
);
return;
}
}
public function evaluate(Component $component, array $updates)
{
foreach ($this->reduceProperty($component, $updates) as $property => $value) {
$this->assign($component, $property, $this->updated($component, $property, $value));
};
return parent::evaluate($component, $updates);
}
private function assign(Component $component, string $property, $value): void
{
$this->propertyHelper->assign(fn ($component, $property, $value) => $component->{$property} = $value, $component, [$property => $value], false);
}
private function updating(Component $component, string $property, $value)
{
$methods = ['updating' . str_replace(' ', '', ucwords(str_replace(['-', '_', '.'], ' ', $property))), 'updating'];
foreach ($methods as $method) {
if (method_exists($component, $method)) {
$value = $component->{$method}($value, $property);
}
}
return $value;
}
private function updated(Component $component, string $property, $value)
{
$methods = ['updated', 'updated' . str_replace(' ', '', ucwords(str_replace(['-', '_', '.'], ' ', $property)))];
foreach ($methods as $method) {
if (method_exists($component, $method)) {
$value = $component->{$method}($value, $property);
}
}
return $value;
}
protected function reduceProperty(Component $component, array $updates): array
{
$nested = [];
foreach ($updates as $update) {
$property = $update['payload']['name'];
$value = $update['payload']['value'];
if ($this->propertyHelper->containsDots($property)) {
try {
$transform = $this->propertyHelper->transformDots($property, $value, $component);
$nested[$transform['property']] = $component->{$transform['property']};
/** @todo multidimensional array support still needs to be included here. */
} catch (ComponentException $exception) {
$this->logger->critical($exception->getMessage(), ['exception' => $exception]);
}
}
}
return $nested;
}
}
+16 -5
View File
@@ -16,12 +16,23 @@ use Magewirephp\Magewire\Component;
interface ActionInterface
{
/**
* Handle update action.
* Runs before (planning stage) a individual action update is beind handled. Inspection is responsible
* for allowing updates for the current type to proceed or not. In other words, when returning false,
* both the handle and evaluate methods won't be able to run.
*
* @param Component $component
* @param array $payload
*
* @return mixed
* @param array $updates // specific updates only for this action type.
*/
public function inspect(Component $component, array $updates): bool;
/**
* Handle update action (execution stage).
*/
public function handle(Component $component, array $payload);
/**
* Runs after (evaluation stage) a individual action update is beind handled.
*
* @param array $updates // specific updates only for this action type.
*/
public function evaluate(Component $component, array $updates);
}
+31 -7
View File
@@ -63,13 +63,37 @@ class ComponentManager
throw new LocalizedException(__('No request object found'));
}
foreach ($updates as $update) {
try {
$this->updateActionsPool[$update['type']]->handle($component, $update['payload']);
} catch (AcceptableException $exception) {
continue;
} catch (Exception $exception) {
throw new LocalizedException(__($exception->getMessage()));
// Temporary typed update basket.
$types = [];
// Key to handle the update request.
$handle = false;
foreach ($updates as $key => $update) {
// Process an inspection before the first update by type runs.
if ($key === 0 || (isset($updates[$key]) && $updates[$key]['type'] !== $updates[$key - 1]['type'])) {
// Filter out only those who have an simular update type.
$types = array_filter($updates, fn ($value) => $value['type'] === $update['type']);
// Lock update handling until inspect releases it.
$handle = false;
if (! empty($types)) {
$handle = $this->updateActionsPool[$update['type']]->inspect($component, $types);
}
}
if ($handle) {
try {
$this->updateActionsPool[$update['type']]->handle($component, $update['payload']);
} catch (AcceptableException $exception) {
continue;
} catch (Exception $exception) {
throw new LocalizedException(__($exception->getMessage()));
}
// Process an evaluation after the last update by type ran.
if ((! isset($updates[$key + 1]) || $updates[$key + 1]['type'] !== $update['type'])) {
$this->updateActionsPool[$update['type']]->evaluate($component, $types);
}
}
}