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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Version 23.3.0

## Features

* Add state detection to observers for "import:attributes", ensuring changes are only persisted when necessary
* Handle media directory creation on demand to prevent invalid paths during file uploads and enhance error handling consistency

# Version 23.2.0

## Features
Expand Down
2 changes: 2 additions & 0 deletions etc/configuration/operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
},
"params": {
"copy-images": false,
"override-images": true,
"media-directory" : "pub/media/attribute/swatch",
"images-file-directory" : "var/importexport/media/attribute/swatch",
"clean-up-empty-columns": []
Expand Down Expand Up @@ -316,6 +317,7 @@
},
"params": {
"copy-images": false,
"override-images": true,
"media-directory" : "pub/media/attribute/swatch",
"images-file-directory" : "var/importexport/media/attribute/swatch",
"clean-up-empty-columns": []
Expand Down
21 changes: 17 additions & 4 deletions src/Observers/AttributeLabelObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use TechDivision\Import\Attribute\Utils\ColumnKeys;
use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface;
use TechDivision\Import\Observers\StateDetectorInterface;

/**
* Observer that create's the EAV attribute label.
Expand All @@ -42,10 +43,16 @@ class AttributeLabelObserver extends AbstractAttributeImportObserver
* Initializes the observer with the passed subject instance.
*
* @param \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface $attributeBunchProcessor The attribute bunch processor instance
* @param \TechDivision\Import\Observers\StateDetectorInterface|null $stateDetector The state detector instance to use
*/
public function __construct(AttributeBunchProcessorInterface $attributeBunchProcessor)
{
public function __construct(
AttributeBunchProcessorInterface $attributeBunchProcessor,
?StateDetectorInterface $stateDetector = null
) {
$this->attributeBunchProcessor = $attributeBunchProcessor;

// pass the state detector to the parent method
parent::__construct($stateDetector);
}

/**
Expand All @@ -66,8 +73,14 @@ protected function process()

// query whether or not an value for the attribute label is available
if ($attributeLabel = $this->prepareAttributes()) {
// prepare and persist the attribue label
$this->persistAttributeLabel($this->initializeAttribute($attributeLabel));
// initialize the attribute label
$attributeLabel = $this->initializeAttribute($attributeLabel);

// query whether or not the attribute label has changed and has to be persisted
if ($this->hasChanges($attributeLabel)) {
// prepare and persist the attribue label
$this->persistAttributeLabel($attributeLabel);
}
}
}

Expand Down
21 changes: 17 additions & 4 deletions src/Observers/AttributeObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use TechDivision\Import\Attribute\Utils\ColumnKeys;
use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface;
use TechDivision\Import\Observers\StateDetectorInterface;

/**
* Observer that create's the EAV attribute itself.
Expand All @@ -42,10 +43,16 @@ class AttributeObserver extends AbstractAttributeImportObserver
* Initializes the observer with the passed subject instance.
*
* @param \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface $attributeBunchProcessor The attribute bunch processor instance
* @param \TechDivision\Import\Observers\StateDetectorInterface|null $stateDetector The state detector instance to use
*/
public function __construct(AttributeBunchProcessorInterface $attributeBunchProcessor)
{
public function __construct(
AttributeBunchProcessorInterface $attributeBunchProcessor,
?StateDetectorInterface $stateDetector = null
) {
$this->attributeBunchProcessor = $attributeBunchProcessor;

// pass the state detector to the parent method
parent::__construct($stateDetector);
}

/**
Expand All @@ -64,8 +71,14 @@ protected function process()
// prepare the attribue values
$attribute = $this->initializeAttribute($this->prepareAttributes());

// insert the entity and set the entity ID
$this->setLastAttributeId($this->persistAttribute($attribute));
// query whether or not the attribute has changed and has to be persisted
if ($this->hasChanges($attribute)) {
// insert the entity and set the entity ID
$this->setLastAttributeId($this->persistAttribute($attribute));
} else {
// make the ID available for subsequent observers, even if nothing has been persisted
$this->setLastAttributeId($attribute[MemberNames::ATTRIBUTE_ID]);
}
}

/**
Expand Down
20 changes: 13 additions & 7 deletions src/Observers/AttributeOptionObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,14 @@ protected function process()
// prepare the attribue values
$attributeOption = $this->initializeAttribute($this->prepareDynamicAttributes());

// insert the attribute option and set the option ID
$this->setLastOptionId($this->persistAttributeOption($attributeOption));
// query whether or not the attribute option has changed and has to be persisted
if ($this->hasChanges($attributeOption)) {
// insert the attribute option and set the option ID
$this->setLastOptionId($this->persistAttributeOption($attributeOption));
} else {
// make the ID available for subsequent observers, even if nothing has been persisted
$this->setLastOptionId($attributeOption[MemberNames::OPTION_ID]);
}
}

/**
Expand All @@ -125,11 +131,11 @@ protected function process()
*/
protected function mergeEntity(array $entity, array $attr, $changeSetName = null)
{
return array_merge(
$entity,
$this->entityMerger ? $this->entityMerger->merge($this, $entity, $attr) : $attr,
array(EntityStatus::MEMBER_NAME => $this->detectState($entity, $attr, $changeSetName))
);
// merge the entity with the (optionally cleaned-up) attributes first, so the
// state detector compares against the actually persisted values and NOT
// against raw/default values of columns that have not been touched by the CSV
$merged = array_merge($entity, $this->entityMerger ? $this->entityMerger->merge($this, $entity, $attr) : $attr);
return array_merge($merged, array(EntityStatus::MEMBER_NAME => $this->detectState($entity, $merged, $changeSetName)));
}

/**
Expand Down
58 changes: 37 additions & 21 deletions src/Observers/AttributeOptionSwatchFileUploadObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace TechDivision\Import\Attribute\Observers;

use TechDivision\Import\Attribute\Utils\ColumnKeys;
use TechDivision\Import\Attribute\Utils\ConfigurationKeys;
use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Utils\SwatchTypes;
Expand Down Expand Up @@ -41,30 +42,45 @@ protected function process()
return;
}

// initialize the option swatch attribute
$attributeOptionSwatch = $this->initializeAttribute(array(
MemberNames::OPTION_ID => $this->getLastOptionId()
));
// skip this step for color swatches and text swatches - read the raw CSV filename reference directly from the
// row, independent of whatever the .update observer did (or, for existing image swatches, deliberately did NOT)
// persist for this row
$type = $this->getValue(ColumnKeys::SWATCH_TYPE);

// skip this step for color swatches and text swatches
if (isset($attributeOptionSwatch[MemberNames::TYPE]) && $attributeOptionSwatch[MemberNames::TYPE] === SwatchTypes::IMAGE) {
// upload the file to the configured directory
$imagePath = $this->getSubject()->uploadFile($attributeOptionSwatch[MemberNames::VALUE]);
if ($type === null || (int) $type !== SwatchTypes::IMAGE) {
return;
}

$rawValue = $this->getValue(ColumnKeys::SWATCH_VALUE);

// load the current DB state (swatch_id + the last successfully uploaded, stable path)
$attributeOptionSwatch = $this->initializeAttribute([MemberNames::OPTION_ID => $this->getLastOptionId()]);

// inject the new image path and update the attribute option swatch
$attributeOptionSwatch['value'] = $imagePath;
$this->getAttributeBunchProcessor()->persistAttributeOptionSwatch($attributeOptionSwatch);
// upload the file (or resolve the already up-to-date, stable path for it)
$imagePath = $this->getSubject()->uploadFile($rawValue);

// add debug log entry
$this->getSubject()
->getSystemLogger()
->debug(
sprintf(
'Successfully copied image %s for swatch with id %s',
$imagePath,
$attributeOptionSwatch[MemberNames::SWATCH_ID]
)
);
// skip the persist if the row already exists and already has this exact path stored - the genuine "unchanged"
// case, only reliably detectable here because "override-images" makes uploadFile()'s result stable across
// separate runs and the .update observer no longer overwrites it with the (never-matching) raw filename
// beforehand
if (isset($attributeOptionSwatch[MemberNames::SWATCH_ID])
&& isset($attributeOptionSwatch[MemberNames::VALUE])
&& $attributeOptionSwatch[MemberNames::VALUE] === $imagePath) {
return;
}

// inject the new image path and type, then persist the attribute option swatch
$attributeOptionSwatch[MemberNames::TYPE] = SwatchTypes::IMAGE;
$attributeOptionSwatch[MemberNames::VALUE] = $imagePath;
$this->getAttributeBunchProcessor()->persistAttributeOptionSwatch($attributeOptionSwatch);

// add debug log entry
$this->getSubject()->getSystemLogger()->debug(
sprintf(
'Successfully copied image %s for swatch with id %s',
$imagePath,
$attributeOptionSwatch[MemberNames::SWATCH_ID] ?? 'n/a'
)
);
}
}
45 changes: 42 additions & 3 deletions src/Observers/AttributeOptionSwatchObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
use TechDivision\Import\Utils\StoreViewCodes;
use TechDivision\Import\Attribute\Utils\ColumnKeys;
use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Utils\SwatchTypes;
use TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface;
use TechDivision\Import\Dbal\Utils\EntityStatus;
use TechDivision\Import\Observers\StateDetectorInterface;

/**
* Observer that create's the attribute option swatchs found in the additional CSV file.
Expand All @@ -42,10 +45,16 @@ class AttributeOptionSwatchObserver extends AbstractAttributeImportObserver
* Initializes the observer with the passed subject instance.
*
* @param \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface $attributeBunchProcessor The attribute bunch processor instance
* @param \TechDivision\Import\Observers\StateDetectorInterface|null $stateDetector The state detector instance to use
*/
public function __construct(AttributeBunchProcessorInterface $attributeBunchProcessor)
{
public function __construct(
AttributeBunchProcessorInterface $attributeBunchProcessor,
?StateDetectorInterface $stateDetector = null
) {
$this->attributeBunchProcessor = $attributeBunchProcessor;

// pass the state detector to the parent method
parent::__construct($stateDetector);
}

/**
Expand All @@ -61,10 +70,40 @@ protected function process()

// prepare and insert the attribute option swatch
if ($attr = $this->prepareAttributes()) {
$this->persistAttributeOptionSwatch($this->initializeAttribute($attr));
// query whether or not the attribute option swatch has changed and has to be persisted
$initialized = $this->initializeAttribute($attr);
if ($this->shouldPersist($initialized)) {
$this->persistAttributeOptionSwatch($initialized);
}
}
}

/**
* Queries whether or not the swatch has to be persisted. For existing image swatches, the decision is deferred
* entirely to AttributeOptionSwatchFileUploadObserver, since comparing the raw CSV filename reference (which is all
* this observer has access to) against the already-uploaded DB path here would always appear "changed" and defeat
* the diff - the file upload observer compares against the actually uploaded, stable target path instead.
*
* @param array $entity The (merged) entity to query
*
* @return boolean TRUE if the entity has to be persisted here, else FALSE
*/
protected function shouldPersist(array $entity): bool
{
// nothing to do, if nothing has changed at all
if (!$this->hasChanges($entity)) {
return false;
}

// for existing (= status update) image swatches, defer to the file upload observer
if ($entity[EntityStatus::MEMBER_NAME] === EntityStatus::STATUS_UPDATE && isset($entity[MemberNames::TYPE])
&& (int)$entity[MemberNames::TYPE] === SwatchTypes::IMAGE) {
return false;
}

return true;
}

/**
* Prepare the attributes of the entity that has to be persisted.
*
Expand Down
19 changes: 16 additions & 3 deletions src/Observers/AttributeOptionValueObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use TechDivision\Import\Attribute\Utils\ColumnKeys;
use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface;
use TechDivision\Import\Observers\StateDetectorInterface;

/**
* Observer that create's the attribute option values found in the additional CSV file.
Expand All @@ -43,10 +44,16 @@ class AttributeOptionValueObserver extends AbstractAttributeImportObserver
* Initializes the observer with the passed subject instance.
*
* @param \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface $attributeBunchProcessor The attribute bunch processor instance
* @param \TechDivision\Import\Observers\StateDetectorInterface|null $stateDetector The state detector instance to use
*/
public function __construct(AttributeBunchProcessorInterface $attributeBunchProcessor)
{
public function __construct(
AttributeBunchProcessorInterface $attributeBunchProcessor,
?StateDetectorInterface $stateDetector = null
) {
$this->attributeBunchProcessor = $attributeBunchProcessor;

// pass the state detector to the parent method
parent::__construct($stateDetector);
}

/**
Expand All @@ -62,7 +69,13 @@ protected function process()

// prepare and insert the attribute option value
try {
$this->persistAttributeOptionValue($this->initializeAttribute($this->prepareAttributes()));
// initialize the attribute option value
$attributeOptionValue = $this->initializeAttribute($this->prepareAttributes());

// query whether or not the attribute option value has changed and has to be persisted
if ($this->hasChanges($attributeOptionValue)) {
$this->persistAttributeOptionValue($attributeOptionValue);
}
} catch (\Exception $e) {
// prepare a log message
$message = sprintf(
Expand Down
Loading
Loading