diff --git a/src/Component/Action/ActionItemInterface.php b/src/Component/Action/ActionItemInterface.php new file mode 100644 index 00000000..1a9b1f35 --- /dev/null +++ b/src/Component/Action/ActionItemInterface.php @@ -0,0 +1,10 @@ + null, + 'field' => null, 'format' => '%s', ]; + public function applyAsItem(ItemInterface $item): void + { + $format = $this->getOption('format'); + $field = $this->getOption('field', $this->getOption('key')); + if (null == $field) { + return; + } + + $dataValues = []; + foreach (ValueFormatter::getKeys($format) as $key) { + $dataValues[$key] = $item->getItem($key)?->getDataValue(); + } + + $item->addItem( + $field, + ValueFormatter::format($format, $dataValues) + ); + } + public function apply(array $item): array { $field = $this->getOption('field', $this->getOption('key')); @@ -28,7 +47,7 @@ public function apply(array $item): array } // don't check if array_key_exist here, concat should always work, if the field doesn't exist - // somewhat the point to form a new field from concatination + // somewhat the point to form a new field from concatenation $item[$field] = ValueFormatter::format($this->getOption('format'), $item); return $item; diff --git a/src/Component/Action/CopyAction.php b/src/Component/Action/CopyAction.php index 8d7352cf..7ac338d7 100644 --- a/src/Component/Action/CopyAction.php +++ b/src/Component/Action/CopyAction.php @@ -5,8 +5,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; use Misery\Component\Converter\Matcher; +use Misery\Model\DataStructure\ItemInterface; -class CopyAction implements ActionInterface, OptionsInterface +class CopyAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -18,6 +19,18 @@ class CopyAction implements ActionInterface, OptionsInterface 'to' => null, ]; + public function applyAsItem(ItemInterface $item): void + { + $from = $this->getOption('from'); + $to = $this->getOption('to'); + + if (null === $from || null === $to) { + return; + } + + $item->copyItem($from, $to); + } + public function apply(array $item): array { $to = $this->getOption('to'); diff --git a/src/Component/Action/DebugAction.php b/src/Component/Action/DebugAction.php index 06f7054c..f9a839a0 100644 --- a/src/Component/Action/DebugAction.php +++ b/src/Component/Action/DebugAction.php @@ -4,8 +4,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; +use Misery\Model\DataStructure\ItemInterface; -class DebugAction implements OptionsInterface, ActionInterface +class DebugAction implements OptionsInterface, ActionInterface, ActionItemInterface { use OptionsTrait; @@ -17,6 +18,11 @@ class DebugAction implements OptionsInterface, ActionInterface 'until_field' => null, ]; + public function applyAsItem(ItemInterface $item): void + { + dd($item); + } + public function apply(array $item): array { $untilField = $this->getOption('until_field'); diff --git a/src/Component/Action/ExpandAction.php b/src/Component/Action/ExpandAction.php index f5683da0..611af7c5 100644 --- a/src/Component/Action/ExpandAction.php +++ b/src/Component/Action/ExpandAction.php @@ -4,8 +4,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; +use Misery\Model\DataStructure\ItemInterface; -class ExpandAction implements OptionsInterface +class ExpandAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -17,6 +18,18 @@ class ExpandAction implements OptionsInterface 'list' => null, ]; + public function applyAsItem(ItemInterface $item): void + { + $list = $this->getOption('set', $this->getOption('list', [])); + if (empty($list)) { + return; + } + + foreach ($list as $itemCode => $itemValue) { + $item->addItem($itemCode, $itemValue); + } + } + public function apply(array $item): array { return array_replace_recursive($this->getOption('set', $this->getOption('list', [])), $item); diff --git a/src/Component/Action/ExtensionAction.php b/src/Component/Action/ExtensionAction.php index e01bf5d1..55af09e5 100644 --- a/src/Component/Action/ExtensionAction.php +++ b/src/Component/Action/ExtensionAction.php @@ -8,8 +8,9 @@ use Misery\Component\Configurator\ConfigurationTrait; use Misery\Component\Configurator\ReadOnlyConfiguration; use Misery\Component\Extension\ExtensionInterface; +use Misery\Model\DataStructure\ItemInterface; -class ExtensionAction implements OptionsInterface, ConfigurationAwareInterface +class ExtensionAction implements OptionsInterface, ConfigurationAwareInterface, ActionItemInterface { use OptionsTrait; use ConfigurationTrait; @@ -22,6 +23,24 @@ class ExtensionAction implements OptionsInterface, ConfigurationAwareInterface 'extension' => null, ]; + public function applyAsItem(ItemInterface $item): void + { + $extension = $this->getOption('extension'); + if (null === $extension) { + return; + } + + // loadExtension + if (null === $this->extension) { + $extensionFile = $this->configuration->getExtensions()[$extension.'.php'] ?? null; + $this->extension = $this->loadExtension($extensionFile, 'Extensions\\'.$extension); + } + + if (method_exists($this->extension, 'applyAsItem')) { + $this->extension->applyAsItem($item); + } + } + public function apply($item): array { $extension = $this->getOption('extension'); diff --git a/src/Component/Action/FirstValueAction.php b/src/Component/Action/FirstValueAction.php new file mode 100644 index 00000000..b9467abb --- /dev/null +++ b/src/Component/Action/FirstValueAction.php @@ -0,0 +1,83 @@ + [], + 'store_field' => null, + 'default_value' => null, + ]; + + public function applyAsItem(ItemInterface $item): void + { + $defaultValue = $this->getOption('default_value'); + $storeField = $this->getOption('store_field'); + $fields = $this->getOption('fields'); + + foreach ($fields as $field) { + if (!empty($item->getItem($field)->getDataValue())) { + $item->copyItem($field, $storeField); + return; + } + } + + $item->addItem($storeField, $defaultValue); + } + + public function apply(array $item): array + { + $defaultValue = $this->getOption('default_value'); + $storeField = $this->getOption('store_field'); + $fields = $this->getOption('fields'); + $matcher = Matcher::create('values|'.$storeField); + + foreach ($fields as $field) { + $field = $this->findMatchedValueData($item, $field) ?? $field; + + // COPY matcher if match is found + if (isset($item[$field]['matcher'])) { + $matcher = $item[$field]['matcher']->duplicateWithNewKey($storeField); + $item[$matcher->getMainKey()] = $item[$field]; + $item[$matcher->getMainKey()]['matcher'] = $matcher; + return $item; + } + } + + if (!isset($item[$matcher->getMainKey()])) { + $item[$matcher->getMainKey()] = [ + 'matcher' => $matcher, + 'data' => $defaultValue, + 'locale' => null, + 'scope' => null, + ]; + } + + return $item; + } + + private function findMatchedValueData(array $item, string $field): int|string|null + { + foreach ($item as $key => $itemValue) { + $matcher = $itemValue['matcher'] ?? null; + /** @var $matcher Matcher */ + if ($matcher && $matcher->matches($field)) { + return $key; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Component/Action/FrameAction.php b/src/Component/Action/FrameAction.php index 7ddeb379..2e665cf0 100644 --- a/src/Component/Action/FrameAction.php +++ b/src/Component/Action/FrameAction.php @@ -4,8 +4,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; +use Misery\Model\DataStructure\ItemInterface; -class FrameAction implements OptionsInterface +class FrameAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -17,6 +18,20 @@ class FrameAction implements OptionsInterface 'list' => [], ]; + public function applyAsItem(ItemInterface $item): void + { + $fields = $this->getOption('fields', $this->getOption('list')); + if (empty($fields)) { + return; + } + + // lets generate a multi-dimensional array + if (isset($fields[0])) { + $fields = array_fill_keys($fields, null); + } + $item->reFrame($fields); + } + public function apply(array $item): array { $fields = $this->getOption('fields', $this->getOption('list')); diff --git a/src/Component/Action/GroupAction.php b/src/Component/Action/GroupAction.php index 18d489f9..6cb4eeab 100644 --- a/src/Component/Action/GroupAction.php +++ b/src/Component/Action/GroupAction.php @@ -6,8 +6,9 @@ use Misery\Component\Common\Options\OptionsTrait; use Misery\Component\Configurator\ConfigurationAwareInterface; use Misery\Component\Configurator\ConfigurationTrait; +use Misery\Model\DataStructure\ItemInterface; -class GroupAction implements OptionsInterface, ConfigurationAwareInterface +class GroupAction implements OptionsInterface, ConfigurationAwareInterface, ActionInterface { use OptionsTrait; use ConfigurationTrait; @@ -20,6 +21,20 @@ class GroupAction implements OptionsInterface, ConfigurationAwareInterface 'actionProcessor' => null, ]; + public function applyAsItem(ItemInterface $item): void + { + if ($this->getOption('name') && !$this->getOption('actionProcessor')) { + $this->setOption( + 'actionProcessor', + $this->getConfiguration()->getGroupedActions($this->getOption('name')) + ); + } + + if ($this->getOption('actionProcessor')) { + $this->getOption('actionProcessor')->process($item); + } + } + public function apply(array $item): array { if ($this->getOption('name') && !$this->getOption('actionProcessor')) { diff --git a/src/Component/Action/ItemActionProcessor.php b/src/Component/Action/ItemActionProcessor.php index d67e253c..4b8dbc24 100644 --- a/src/Component/Action/ItemActionProcessor.php +++ b/src/Component/Action/ItemActionProcessor.php @@ -2,18 +2,19 @@ namespace Misery\Component\Action; +use Misery\Model\DataStructure\ItemInterface; + class ItemActionProcessor { - private $configurationRules; - - public function __construct(array $configurationRules) - { - $this->configurationRules = $configurationRules; - } + public function __construct(private readonly array $configurationRules) {} - public function process(array $item): array + public function process(ItemInterface|array $item): ItemInterface|array { foreach ($this->configurationRules as $name => $action) { + if ($item instanceof ItemInterface) { + $action->applyAsItem($item); + continue; + } $item = $action->apply($item); } diff --git a/src/Component/Action/ItemActionProcessorFactory.php b/src/Component/Action/ItemActionProcessorFactory.php index c6461087..9cdbe49b 100644 --- a/src/Component/Action/ItemActionProcessorFactory.php +++ b/src/Component/Action/ItemActionProcessorFactory.php @@ -7,18 +7,12 @@ use Misery\Component\Common\Registry\RegistryInterface; use Misery\Component\Configurator\Configuration; use Misery\Component\Configurator\ConfigurationAwareInterface; -use Misery\Component\Configurator\ConfigurationManager; use Misery\Component\Reader\ItemReaderAwareInterface; use Misery\Component\Source\SourceCollection; class ItemActionProcessorFactory implements RegisteredByNameInterface { - private $registry; - - public function __construct(RegistryInterface $registry) - { - $this->registry = $registry; - } + public function __construct(private readonly RegistryInterface $registry) {} public function createActionProcessor(SourceCollection $sources, array $configuration): ItemActionProcessor { diff --git a/src/Component/Action/KeyMapperAction.php b/src/Component/Action/KeyMapperAction.php index bb7f1775..659fee18 100644 --- a/src/Component/Action/KeyMapperAction.php +++ b/src/Component/Action/KeyMapperAction.php @@ -6,8 +6,9 @@ use Misery\Component\Common\Options\OptionsTrait; use Misery\Component\Converter\Matcher; use Misery\Component\Mapping\ColumnMapper; +use Misery\Model\DataStructure\ItemInterface; -class KeyMapperAction implements OptionsInterface +class KeyMapperAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -27,6 +28,32 @@ public function __construct() 'reverse' => false, ]; + public function applyAsItem(ItemInterface $item): void + { + $reverse = $this->getOption('reverse'); + $list = array_filter($this->getOption('list')); + + if ($reverse) { + $keys = []; + foreach ($list as $match => $replacer) { + if ($item->hasItem($replacer)) { + $item->copyItem($replacer, $match); + $keys[] = $replacer; + } + } + foreach ($keys as $keyToUnset) { + $item->removeItem($keyToUnset); + } + return; + } + + foreach ($list as $match => $replacer) { + if ($item->hasItem($match)) { + $item->moveItem($match, $replacer); + } + } + } + public function apply(array $item): array { $reverse = $this->getOption('reverse'); diff --git a/src/Component/Action/ListMapperAction.php b/src/Component/Action/ListMapperAction.php index ffe3a968..d60fd8d9 100644 --- a/src/Component/Action/ListMapperAction.php +++ b/src/Component/Action/ListMapperAction.php @@ -4,8 +4,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; +use Misery\Model\DataStructure\ItemInterface; -class ListMapperAction implements ActionInterface, OptionsInterface +class ListMapperAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -18,6 +19,30 @@ class ListMapperAction implements ActionInterface, OptionsInterface 'list' => [], ]; + public function applyAsItem(ItemInterface $item): void + { + if ([] === $this->getOption('list')) { + return; + } + + if (null === $this->getOption('field')) { + return; + } + $field = $this->getOption('field'); + $storeField = $this->getOption('store_field'); + + $itemNode = $item->getItem($field); + $dataValue = $itemNode?->getDataValue(); + if ($dataValue && array_key_exists($dataValue, $this->getOption('list'))) { + $newValue = $this->options['list'][$dataValue]; + if ($storeField) { + $item->copyItem($storeField, $newValue); + } else { + $item->editItemValue($field, $newValue); + } + } + } + public function apply(array $item): array { $value = $item[$this->options['field']] ?? null; diff --git a/src/Component/Action/RemoveAction.php b/src/Component/Action/RemoveAction.php index 2fc91c3e..07965f29 100644 --- a/src/Component/Action/RemoveAction.php +++ b/src/Component/Action/RemoveAction.php @@ -4,8 +4,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; +use Misery\Model\DataStructure\ItemInterface; -class RemoveAction implements ActionInterface, OptionsInterface +class RemoveAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -17,6 +18,14 @@ class RemoveAction implements ActionInterface, OptionsInterface 'fields' => [], ]; + public function applyAsItem(ItemInterface $item): void + { + $fields = $this->getOption('keys', $this->getOption('fields')); + foreach ($fields as $field) { + $item->removeItem($field); + } + } + public function apply(array $item): array { $fields = $this->getOption('keys', $this->getOption('fields')); diff --git a/src/Component/Action/RenameAction.php b/src/Component/Action/RenameAction.php index fd5a7cc9..0a1adbe2 100644 --- a/src/Component/Action/RenameAction.php +++ b/src/Component/Action/RenameAction.php @@ -5,8 +5,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; use Misery\Component\Mapping\ColumnMapper; +use Misery\Model\DataStructure\ItemInterface; -class RenameAction implements OptionsInterface +class RenameAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -19,16 +20,27 @@ public function __construct() $this->mapper = new ColumnMapper(); } - /** @var array */ - private $options = [ + private array $options = [ 'from' => null, 'to' => null, 'suffix' => null, 'exclude_list' => [], 'filter_list' => null, 'fields' => [], + 'strict_mode' => true, ]; + public function applyAsItem(ItemInterface $item): void + { + $from = $this->getOption('from'); + $to = $this->getOption('to'); + if (!$from || !$to) { + return; + } + + $item->moveItem($from, $to); + } + public function apply(array $item): array { $from = $this->getOption('from'); diff --git a/src/Component/Action/RetainAction.php b/src/Component/Action/RetainAction.php index 602dd327..5ca38059 100644 --- a/src/Component/Action/RetainAction.php +++ b/src/Component/Action/RetainAction.php @@ -4,6 +4,7 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; +use Misery\Model\DataStructure\ItemInterface; class RetainAction implements OptionsInterface { @@ -35,6 +36,21 @@ private function applyForSingleDimensionItem(array $item): array return $tmp; } + public function applyAsItem(ItemInterface $item): ItemInterface + { + $fields = $this->getOption('keys', $this->getOption('fields')); + if (empty($fields)) { + return $item; + } + + $itemCodesToRemove = array_diff($item->getItemCodes(), $fields); + foreach ($itemCodesToRemove as $itemCode) { + $item->removeItem($itemCode); + } + + return $item; + } + /** * The default apply will look into for multi dimensional array */ diff --git a/src/Component/Action/SetValueAction.php b/src/Component/Action/SetValueAction.php index ed9284e6..50e3d0ee 100644 --- a/src/Component/Action/SetValueAction.php +++ b/src/Component/Action/SetValueAction.php @@ -5,8 +5,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; use Misery\Component\Converter\Matcher; +use Misery\Model\DataStructure\ItemInterface; -class SetValueAction implements ActionInterface, OptionsInterface +class SetValueAction implements ActionItemInterface, OptionsInterface { use OptionsTrait; @@ -17,17 +18,41 @@ class SetValueAction implements ActionInterface, OptionsInterface 'key' => null, 'field' => null, 'value' => null, + 'allow_creation' => false, ]; + public function applyAsItem(ItemInterface $item): void + { + $allowCreation = $this->getOption('allow_creation'); + $field = $this->getOption('field', $this->getOption('key')); + $value = $this->getOption('value'); + + if ($allowCreation) { + $item->addItem($field, $value); + return; + } + + if ($item->hasItem($field)) { + $item->editItemValue($field, $value); + } + } + public function apply(array $item): array { + $allowCreation = $this->getOption('allow_creation'); $field = $this->getOption('field', $this->getOption('key')); $value = $this->getOption('value'); - $key = $this->findMatchedValueData($item, $field); - if ($key) { - $item[$key]['data'] = $value; + $field = $this->findMatchedValueData($item, $field) ?? $field; + + // matcher based data object + if (isset($item[$field]['data'])) { + $item[$field]['data'] = $value; + return $item; + } + if ($allowCreation) { + $item[$field] = $value; return $item; } diff --git a/src/Component/Action/SkipAction.php b/src/Component/Action/SkipAction.php index e3b797a1..88165156 100644 --- a/src/Component/Action/SkipAction.php +++ b/src/Component/Action/SkipAction.php @@ -5,8 +5,9 @@ use Misery\Component\Common\Options\OptionsInterface; use Misery\Component\Common\Options\OptionsTrait; use Misery\Component\Common\Pipeline\Exception\SkipPipeLineException; +use Misery\Model\DataStructure\ItemInterface; -class SkipAction implements OptionsInterface, ActionInterface +class SkipAction implements OptionsInterface, ActionItemInterface { use OptionsTrait; @@ -20,6 +21,14 @@ class SkipAction implements OptionsInterface, ActionInterface ]; private array $values = []; + public function applyAsItem(ItemInterface $item): void + { + $field = $this->getOption('field'); + $dateValue = $item->getItem($field)?->getDataValue(); + + $this->apply([$field => $dateValue]); + } + public function apply(array $item): array { $field = $this->getOption('field'); diff --git a/src/Component/Akeneo/DataStructure/AkeneoItemBuilder.php b/src/Component/Akeneo/DataStructure/AkeneoItemBuilder.php new file mode 100644 index 00000000..0439b1bc --- /dev/null +++ b/src/Component/Akeneo/DataStructure/AkeneoItemBuilder.php @@ -0,0 +1,107 @@ +isArray() + ->keyExists('values') + ; + + foreach ($productData as $property => $propertyValue) { + if ($property === 'values') { + continue; + } + if (is_array($propertyValue)) { + $propertyValue['matcher'] = Matcher::create($property); + } + $itemObj->addItem($property, $propertyValue); + } + + // convert every value into an ItemNode + foreach ($productData['values'] as $attributeCode => $productValues) { + foreach ($productValues as $productValue) { + $matcher = Matcher::create('values|'.$attributeCode, $productValue['locale'], $productValue['scope']); + // don't overwrite a type when it has been given + $productValue['type'] = $productValue['type'] ?? $attributeData[$matcher->getPrimaryKey()] ?? null; + $productValue['matcher'] = $matcher; + + $itemObj->addItem($matcher->getMainKey(), $productValue, $context); + } + } + + return $itemObj; + } + + public static function fromCatalogApiPayload(array $catalogData, array $context = []): ItemInterface + { + Assert::that($context)->keyExists('class', 'You need to supply a class'); + + $itemObj = new Item($context['class']); + + Assert::that($catalogData) + ->isArray() + ->keyExists('code') + ; + + foreach ($catalogData as $property => $propertyValue) { + if ($property === 'labels') { + foreach ($propertyValue as $locale => $labelValue) { + $value['matcher'] = $m = Matcher::create($property, $locale); + $value['data'] = $labelValue; + $itemObj->addItem($m->getMainKey(), $value); + } + continue; + } + if (is_array($propertyValue)) { + $propertyValue['matcher'] = Matcher::create($property); + } + $itemObj->addItem($property, $propertyValue); + } + + return $itemObj; + } +} diff --git a/src/Component/Akeneo/DataStructure/AkeneoScope.php b/src/Component/Akeneo/DataStructure/AkeneoScope.php new file mode 100644 index 00000000..dc6789c2 --- /dev/null +++ b/src/Component/Akeneo/DataStructure/AkeneoScope.php @@ -0,0 +1,67 @@ +channelCode = $channelCode; + $this->localeCode = $localeCode; + } + + public static function fromArray(array $data): self + { + $self = new self(); + $self->channelCode = $data['scope'] ?? $data['channel_code'] ?? $data['channel'] ?? null; + $self->localeCode = $data['locale'] ?? $data['locale_code'] ?? $data['scope_locale'] ?? null; + + return $self; + } + + public function equals(ScopeInterface $scope): bool + { + return + $this->getChannel() === $scope->getChannel() && + $this->getLocale() === $scope->getLocale() + ; + } + + public function getChannel(): ?string + { + return $this->channelCode; + } + + public function getLocale(): ?string + { + return $this->localeCode; + } + + public function isLocalizable(): bool + { + return $this->localeCode !== null; + } + + public function isScopable(): bool + { + return $this->channelCode !== null; + } + + public function toArray(): array + { + return [ + 'channel' => $this->channelCode, + 'locale' => $this->localeCode, + ]; + } + + public function __toString(): string + { + return implode('|', array_values($this->toArray())); + } +} \ No newline at end of file diff --git a/src/Component/Akeneo/DataStructure/BuildProductMatcher.php b/src/Component/Akeneo/DataStructure/BuildProductMatcher.php new file mode 100644 index 00000000..653b6bd6 --- /dev/null +++ b/src/Component/Akeneo/DataStructure/BuildProductMatcher.php @@ -0,0 +1,91 @@ +getItemNodes() as $code => $itemNode) { + $matcher = $itemNode->getMatcher(); + + if ($matcher->matches('values')) { + $fields['values'][$matcher->getPrimaryKey()][] = $itemNode->getValue(); + } else { + $fields[$code] = $itemNode->getValue(); + } + } + + return $fields; + } + + public static function revertMatcherToProduct(array $productData): array + { + $fields = []; + foreach ($productData as $field => &$fieldValue) { + $matcher = $fieldValue['matcher'] ?? null; + if ($matcher instanceof Matcher) { + unset($fieldValue['matcher']); + $fields['values'][$matcher->getPrimaryKey()][] = $fieldValue; + } else { + $fields[$field] = $fieldValue; + } + } + + return $fields; + } + + public static function revertToKeyValue(array $productData): array + { + $fields = []; + foreach ($productData as $field => $fieldValue) { + $fields[$field] = $fieldValue; + $matcher = $fieldValue['matcher'] ?? null; + if ($matcher instanceof Matcher) { + $fields[$field] = $fieldValue['data'] ?? null; + } + } + + return $fields; + } +} \ No newline at end of file diff --git a/src/Component/Common/Utils/ValueFormatter.php b/src/Component/Common/Utils/ValueFormatter.php index 73c79fcc..99c35fe2 100644 --- a/src/Component/Common/Utils/ValueFormatter.php +++ b/src/Component/Common/Utils/ValueFormatter.php @@ -25,6 +25,12 @@ public static function format(string $format, array $values): string return strtr($format, $replacements); } + public static function getKeys(string $format): array + { + preg_match_all('/%([a-zA-Z0-9_]+)%/', $format, $matches); + return $matches[1] ?? []; + } + public static function recursiveFormat(string $format, array $values): string { foreach ($values as $value) { diff --git a/src/Component/Converter/Matcher.php b/src/Component/Converter/Matcher.php index 5d638163..b4566a19 100644 --- a/src/Component/Converter/Matcher.php +++ b/src/Component/Converter/Matcher.php @@ -49,7 +49,12 @@ public function isScopable(): bool public function getPrimaryKey(): string { - return $this->matches[1]; + return $this->matches[1] ?? $this->matches[0]; + } + + public function isProperty(): bool + { + return count($this->matches) === 1; } public function getRowKey(): string @@ -64,7 +69,7 @@ public function getMainKey(): string public function matches(string $match): bool { - return in_array($match, $this->matches); + return in_array($match, $this->matches) || $match === $this->getMainKey(); } public function duplicateWithNewKey(string $newPrimaryKey): self @@ -74,6 +79,12 @@ public function duplicateWithNewKey(string $newPrimaryKey): self $matcher->locale = $this->locale; $matcher->matches = $this->matches; $matcher->matches[1] = $newPrimaryKey; + if (count(explode($this->separator, $newPrimaryKey)) > 1) { + $matcher->matches = explode($this->separator, $newPrimaryKey); + } + + // obelink-xml-v3 + //$matcher->matches = explode($this->separator, $newPrimaryKey); return $matcher; } diff --git a/src/Model/DataStructure/Item.php b/src/Model/DataStructure/Item.php new file mode 100644 index 00000000..aa6f9ec4 --- /dev/null +++ b/src/Model/DataStructure/Item.php @@ -0,0 +1,169 @@ +itemNodes as $node) { + Assert::that($node)->isInstanceOf(ItemNode::class); + } + } + + public function getClass(): string + { + return $this->class; + } + + public function addItem(string $code, mixed $itemValue, array $context = []): void + { + $this->itemNodes[$code] = ItemNode::withContext($code, $itemValue, $context); + } + + public function copyItem(string $fromCode, string $toCode): void + { + $item = $this->getItem($fromCode); + if (!$item) { + return; + } + + $itemValue = $item->getValue(); + if (null === $itemValue) { + return; + } + + // Property Values + if (!is_array($itemValue)) { + $this->addItem($toCode, $itemValue, $item->getContext()); + return; + } + + $matcher = $item->getMatcher()->duplicateWithNewKey($toCode); + + $itemValue['type'] = $item->getType(); + $itemValue['matcher'] = $matcher; + + $this->addItem($matcher->getPrimaryKey(), $itemValue, $item->getContext()); + } + + public function reFrame(array $orderedFields, bool $appendRemaining = false): void + { + // Create a new array with the specified order + $orderedNodes = []; + foreach (array_keys($orderedFields) as $key) { + $orderedNodes[$key] = $this->itemNodes[$key] ?? $orderedFields[$key]; + } + + // Append any remaining items not in the order + if ($appendRemaining) { + foreach ($this->itemNodes as $key => $node) { + if (!array_key_exists($key, $orderedNodes)) { + $orderedNodes[$key] = $node; + } + } + } + + // Replace the original array with the reordered array + $this->itemNodes = $orderedNodes; + } + + public function moveItem(string $fromCode, string $toCode): void + { + $this->copyItem($fromCode, $toCode); + $this->removeItem($fromCode); + } + + public function editItemValue(string $code, $dataValue): void + { + $item = $this->getItem($code); + $itemValue = $item->getValue(); + if (is_string($itemValue)) { + $itemValue = $dataValue; + } else { + $itemValue['data'] = $dataValue; + } + $this->addItem($item->getCode(), $itemValue, $item->getContext()); + } + + /** + * Removes a value for a given attribute code. + * + * @param string $code The attribute code to remove. + * @return void + */ + public function removeItem(string $code): void + { + if ($this->hasItem($code)) { + unset($this->itemNodes[$code]); + } + } + + public function hasItem(string $code): bool + { + return array_key_exists($code, $this->itemNodes) ?? $this->getItemByMatch($code) !== null; + } + + public function getItem(string $codeOrMatch): ?ItemNode + { + return $this->itemNodes[$codeOrMatch] ?? $this->getItemByMatch($codeOrMatch) ?? null; + } + + /** + * Gets all attributes. + * + * @return Item[] The array of all attributes. + */ + public function getItemNodes(): array + { + return $this->itemNodes; + } + + public function getItemCodes(): array + { + return array_keys($this->itemNodes); + } + + public function getItemsByScope(ScopeInterface $scope): \Generator + { + foreach ($this->itemNodes as $code => $item) { + if ($item->getScope()->equals($scope)) { + yield $code => $item; + } + } + } + + public function getItemByMatch(string $match): ?ItemNode + { + foreach ($this->itemNodes as $code => $item) { + if ($item->getMatcher()->matches($match)) { + return $item; + } + } + return null; + } + + public function getItemsByMatch(string $match): \Generator + { + foreach ($this->itemNodes as $code => $item) { + if ($item->getMatcher()->matches($match)) { + yield $code => $item; + } + } + } + + public function getItemsByType(string $type): \Generator + { + foreach ($this->itemNodes as $code => $item) { + if ($item->getType() == $type) { + yield $code => $item; + } + } + } +} diff --git a/src/Model/DataStructure/ItemInterface.php b/src/Model/DataStructure/ItemInterface.php new file mode 100644 index 00000000..fa10527c --- /dev/null +++ b/src/Model/DataStructure/ItemInterface.php @@ -0,0 +1,109 @@ + A generator yielding matching items. + */ + public function getItemsByMatch(string $match): \Generator; + + /** + * Retrieves items by their type as a generator. + * + * @param string $type The type to filter items by. + * @return \Generator A generator yielding items of the given type. + */ + public function getItemsByType(string $type): \Generator; +} diff --git a/src/Model/DataStructure/ItemNode.php b/src/Model/DataStructure/ItemNode.php new file mode 100644 index 00000000..12facb07 --- /dev/null +++ b/src/Model/DataStructure/ItemNode.php @@ -0,0 +1,85 @@ +matcher = Matcher::create($this->code, $this->scope->getLocale(), $this->scope->getChannel()); + if ($this->matcher->isProperty()) { + $this->type = 'property'; + } + + // when dealing with an array, we expect the 'data' property + if (is_array($this->value) && array_key_exists('data', $this->value)) { + + $this->type = $this->value['type'] ?? 'generic'; + + if (isset($this->value['matcher']) && $this->value['matcher'] instanceof Matcher) { + $this->matcher = $this->value['matcher']; + } + } + } + + public static function withContext(string $code, $value, array $context = []): ItemNode + { + $scope = new AkeneoScope(); + if (array_key_exists('locale', $context) && array_key_exists('scope', $context)) { + $scope = AkeneoScope::fromArray($context); + } + + return new self($code, $value, $scope); + } + + public function getContext(): array + { + return [ + 'locale' => $this->scope->getLocale(), + 'scope' => $this->scope->getChannel(), + 'original-code' => $this->getMatcher()->getPrimaryKey(), + 'code' => $this->code, + ]; + } + + public function getScope(): ScopeInterface + { + return $this->scope; + } + + public function equals(string $code): bool + { + $this->matcher->matches($code); + } + + public function getMatcher(): Matcher + { + return $this->matcher; + } + + public function getType(): string + { + return $this->type; + } + + public function getCode(): string + { + return $this->code; + } + + public function getValue() + { + return $this->value; + } + + public function getDataValue() + { + return $this->value['data'] ?? $this->value ?? null; + } +} \ No newline at end of file diff --git a/src/Model/DataStructure/ItemNodeInterface.php b/src/Model/DataStructure/ItemNodeInterface.php new file mode 100644 index 00000000..748972af --- /dev/null +++ b/src/Model/DataStructure/ItemNodeInterface.php @@ -0,0 +1,17 @@ +