diff --git a/.gitignore b/.gitignore
index 22ac069..2e01698 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
+.idea
.php_cs.cache
-*~
\ No newline at end of file
+vendor
+composer.lock
+*~
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abd2328..817f3bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## 1.3.0 - 2024-02-23
+### Changed
+- Output for `eav:attributes:restore-use-default-value` only shows the table name once.
+### Added
+- Option to remove scoped attribute values for `eav:attributes:restore-use-default-value`
+### Fixed
+- Adobe Commerce B2B is now also detected as Enterprise
+
## 1.2.1 - 2021-10-28
### Added
- Add license
diff --git a/Console/Command/RestoreUseDefaultValueCommand.php b/Console/Command/RestoreUseDefaultValueCommand.php
index 5666ea5..651bffe 100755
--- a/Console/Command/RestoreUseDefaultValueCommand.php
+++ b/Console/Command/RestoreUseDefaultValueCommand.php
@@ -2,10 +2,13 @@
namespace Hackathon\EAVCleaner\Console\Command;
+use Hackathon\EAVCleaner\Filter\AttributeFilter;
+use Hackathon\EAVCleaner\Filter\StoreFilter;
use Magento\Framework\App\ProductMetadataInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Model\ResourceModel\IteratorFactory;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -26,10 +29,22 @@ class RestoreUseDefaultValueCommand extends Command
*/
private $resourceConnection;
+ /**
+ * @var string
+ */
+ private $storeFilter;
+
+ /**
+ * @var AttributeFilter
+ */
+ private $attributeFilter;
+
public function __construct(
IteratorFactory $iteratorFactory,
ProductMetaDataInterface $productMetaData,
ResourceConnection $resourceConnection,
+ StoreFilter $storeFilter,
+ AttributeFilter $attributeFilter,
string $name = null
) {
parent::__construct($name);
@@ -37,6 +52,8 @@ public function __construct(
$this->iteratorFactory = $iteratorFactory;
$this->productMetaData = $productMetaData;
$this->resourceConnection = $resourceConnection;
+ $this->storeFilter = $storeFilter;
+ $this->attributeFilter = $attributeFilter;
}
protected function configure()
@@ -53,7 +70,26 @@ protected function configure()
InputOption::VALUE_OPTIONAL,
'Set entity to cleanup (product or category)',
'product'
- );
+ )
+ ->addOption(
+ 'store_codes',
+ null,
+ InputArgument::IS_ARRAY,
+ 'Store codes from which attribute values should be removed (csv)',
+ )
+ ->addOption(
+ 'exclude_attributes',
+ null,
+ InputArgument::IS_ARRAY,
+ 'Attribute codes from which values should be preserved (csv)',
+ )
+ ->addOption(
+ 'include_attributes',
+ null,
+ InputArgument::IS_ARRAY,
+ 'Attribute codes from which values should be removed (csv)',
+ )
+ ->addOption('always_restore');
}
public function execute(InputInterface $input, OutputInterface $output): int
@@ -61,18 +97,36 @@ public function execute(InputInterface $input, OutputInterface $output): int
$isDryRun = $input->getOption('dry-run');
$isForce = $input->getOption('force');
$entity = $input->getOption('entity');
+ $storeCodes = $input->getOption('store_codes');
+ $excludeAttributes = $input->getOption('exclude_attributes');
+ $includeAttributes = $input->getOption('include_attributes');
+ $isAlwaysRestore = $input->getOption('always_restore');
+
+ try {
+ $storeIdFilter = $this->storeFilter->getStoreFilter($storeCodes);
+ } catch (Exception $e) {
+ $output->writeln($e->getMessage());
+ return Command::FAILURE;
+ }
if (!in_array($entity, ['product', 'category'])) {
$output->writeln('Please specify the entity with --entity. Possible options are product or category');
- return 1; // error.
+ return Command::FAILURE;
+ }
+
+ try {
+ $attributeFilter = $this->attributeFilter->getAttributeFilter($entity, $excludeAttributes, $includeAttributes);
+ } catch (Exception $e) {
+ $output->writeln($e->getMessage());
+ return Command::FAILURE;
}
if (!$isDryRun && !$isForce) {
if (!$input->isInteractive()) {
$output->writeln('ERROR: neither --dry-run nor --force options were supplied, and we are not running interactively.');
- return 1; // error.
+ return Command::FAILURE;
}
$output->writeln('WARNING: this is not a dry run. If you want to do a dry-run, add --dry-run.');
@@ -87,25 +141,46 @@ public function execute(InputInterface $input, OutputInterface $output): int
$dbWrite = $this->resourceConnection->getConnection('core_write');
$counts = [];
$tables = ['varchar', 'int', 'decimal', 'text', 'datetime'];
- $column = $this->productMetaData->getEdition() === 'Enterprise' ? 'row_id' : 'entity_id';
+ $column = $this->productMetaData->getEdition() === 'Community' ? 'entity_id' : 'row_id';
foreach ($tables as $table) {
// Select all non-global values
$fullTableName = $this->resourceConnection->getTableName('catalog_' . $entity . '_entity_' . $table);
+ $output->writeln(sprintf('Now processing entity `%s` in table `%s`', $entity, $fullTableName));
// NULL values are handled separately
- $query = $dbRead->query("SELECT * FROM $fullTableName WHERE store_id != 0 AND value IS NOT NULL");
+ $notNullValuesQuery=sprintf(
+ "SELECT * FROM $fullTableName WHERE store_id != 0 %s %s AND value IS NOT NULL",
+ $storeIdFilter,
+ $attributeFilter
+ );
+
+ $output->writeln(sprintf('%s', $notNullValuesQuery));
+ $query = $dbRead->query($notNullValuesQuery);
$iterator = $this->iteratorFactory->create();
- $iterator->walk($query, [function (array $result) use ($column, &$counts, $dbRead, $dbWrite, $fullTableName, $isDryRun, $output): void {
+ $iterator->walk($query, [function (array $result) use ($column, &$counts, $dbRead, $dbWrite, $fullTableName,
+ $isDryRun, $output, $isAlwaysRestore, $storeIdFilter): void {
$row = $result['row'];
- // Select the global value if it's the same as the non-global value
- $query = $dbRead->query(
- 'SELECT * FROM ' . $fullTableName
- . ' WHERE attribute_id = ? AND store_id = ? AND ' . $column . ' = ? AND BINARY value = ?',
- [$row['attribute_id'], 0, $row[$column], $row['value']]
- );
+ if (!$isAlwaysRestore) {
+ // Select the global value if it's the same as the non-global value
+ $query = $dbRead->query(
+ 'SELECT * FROM ' . $fullTableName
+ . ' WHERE attribute_id = ? AND store_id = ? AND ' . $column . ' = ? AND BINARY value = ?',
+ [$row['attribute_id'], 0, $row[$column], $row['value']]
+ );
+ } else {
+ // Select all scoped values
+ $selectScopedValuesQuery = sprintf(
+ 'SELECT * FROM %s WHERE attribute_id = ? %s AND %s = ?',
+ $fullTableName,
+ $storeIdFilter,
+ $column
+ );
+
+ $query = $dbRead->query($selectScopedValuesQuery, [$row['attribute_id'], $row[$column]]);
+ }
$iterator = $this->iteratorFactory->create();
$iterator->walk($query, [function (array $result) use (&$counts, $dbWrite, $fullTableName, $isDryRun, $output, $row): void {
@@ -120,9 +195,15 @@ public function execute(InputInterface $input, OutputInterface $output): int
}
$output->writeln(
- 'Deleting value ' . $row['value_id'] . ' "' . $row['value'] . '" in favor of '
- . $result['value_id']
- . ' for attribute ' . $row['attribute_id'] . ' in table ' . $fullTableName
+ sprintf(
+ 'Deleting value %s (%s) in favor of %s (%s) for attribute %s for store id %s',
+ $row['value_id'],
+ $row['value'],
+ $result['value_id'] ,
+ $result ['value'],
+ $row['attribute_id'],
+ $row ['store_id']
+ )
);
if (!isset($counts[$row['attribute_id']])) {
@@ -133,16 +214,17 @@ public function execute(InputInterface $input, OutputInterface $output): int
}]);
}]);
+ $nullCountWhereClause = sprintf('WHERE store_id != 0 %s %s AND value IS NULL', $storeIdFilter, $attributeFilter);
$nullCount = (int) $dbRead->fetchOne(
- 'SELECT COUNT(*) FROM ' . $fullTableName . ' WHERE store_id != 0 AND value IS NULL'
+ 'SELECT COUNT(*) FROM ' . $fullTableName . ' ' . $nullCountWhereClause
);
if (!$isDryRun && $nullCount > 0) {
$output->writeln("Deleting $nullCount NULL value(s) from $fullTableName");
// Remove all non-global null values
- $dbWrite->query(
- 'DELETE FROM ' . $fullTableName . ' WHERE store_id != 0 AND value IS NULL'
- );
+ $removeNullValuesQuery = 'DELETE FROM ' . $fullTableName . ' ' . $nullCountWhereClause;
+ $output->writeln(sprintf('%s', $removeNullValuesQuery));
+ $dbWrite->query($removeNullValuesQuery);
}
if (count($counts)) {
diff --git a/Filter/AttributeFilter.php b/Filter/AttributeFilter.php
new file mode 100644
index 0000000..2b6e299
--- /dev/null
+++ b/Filter/AttributeFilter.php
@@ -0,0 +1,73 @@
+attribute = $attribute;
+ }
+
+ /**
+ * @param string $entityType
+ * @param string|null $excludeAttributes
+ * @param string|null $includeAttributes
+ *
+ * @return array|null
+ */
+ public function getAttributeFilter(
+ string $entityType,
+ ?string $excludeAttributes,
+ ?string $includeAttributes
+ ) : string
+ {
+ $attributeFilter = '';
+
+ if ($includeAttributes !== null) {
+ $includedIds = $this->getAttributeIds($entityType, $includeAttributes);
+ if (!empty($includedIds)) {
+ $attributeFilter .= sprintf('AND attribute_id IN(%s)', implode(',', $includedIds));
+ }
+ }
+
+ if ($excludeAttributes !== null) {
+ $excludedIds = $this->getAttributeIds($entityType, $excludeAttributes);
+ if (!empty($excludedIds)) {
+ $attributeFilter .= sprintf('AND attribute_id NOT IN(%s)', implode(',', $excludedIds));
+ }
+ }
+
+ return $attributeFilter;
+ }
+
+ private function getAttributeIds(string $entityType, string $attributeCodes): ?array
+ {
+ $attributes = explode(',', $attributeCodes);
+ $attributeIds = [];
+ foreach ($attributes as $attributeCode) {
+ $attributeId = $this->attribute->getIdByCode('catalog_' . $entityType, $attributeCode);
+ if($attributeId === false) {
+ $error = sprintf('Attribute with code `%s` does not exist', $attributeCode);
+ throw new AttributeDoesNotExistException($error);
+ } else {
+ $attributeIds[] = $attributeId;
+ }
+
+ }
+ return $attributeIds;
+ }
+}
diff --git a/Filter/Exception/AdminValuesCanNotBeRemovedException.php b/Filter/Exception/AdminValuesCanNotBeRemovedException.php
new file mode 100644
index 0000000..d82ab1b
--- /dev/null
+++ b/Filter/Exception/AdminValuesCanNotBeRemovedException.php
@@ -0,0 +1,10 @@
+storeRepository = $storeRepository;
+ }
+
+ /**
+ * @param string|null $storeCodes
+ *
+ * @return string
+ */
+ public function getStoreFilter(?string $storeCodes) : string
+ {
+ if ($storeCodes !== null) {
+ $storeCodesArray = explode(',', $storeCodes);
+
+ $storeIds=[];
+ foreach ($storeCodesArray as $storeCode) {
+ if ($storeCode == 'admin') {
+ $error = 'Admin values can not be removed!';
+ throw new AdminValuesCanNotBeRemovedException($error);
+ }
+
+ try {
+ $storeId = $this->storeRepository->get($storeCode)->getId();
+ } catch (NoSuchEntityException $e) {
+ $error = sprintf('%s | Store with code `%s` does not exist.', $e->getMessage(), $storeCode);
+ throw new StoreDoesNotExistException($error);
+ }
+
+ $storeIds[] = $storeId;
+ }
+
+ return sprintf('AND store_id in(%s)', implode(',', $storeIds));
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/README.md b/README.md
index d6208c9..3ffdac2 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,20 @@ Use `--dry-run` to check result without modifying data.
## Force
Use `--force` to skip the confirmation prompt before modifying data.
+## Additional options for `eav:attributes:restore-use-default-value`
+
+### Always remove
+Use `--always_restore` to remove all values, even if the scoped value is not equal to the base value.
+
+### Store codes
+Use `--store_codes=your_store_code` to only remove values for this store.
+
+### Include attributes
+Use `--include_attributes=some_attribute,some_other_attribute` to only delete values for these attributes.
+
+### Exclude attributes
+Use `--exclude_attributes=some_attribute,some_other_attribute` to preserve values for these attributes.
+
## Installation
Installation with composer:
@@ -31,6 +45,7 @@ composer require magento-hackathon/module-eavcleaner-m2
- Nikita Zhavoronkova
- Anastasiia Sukhorukova
- Peter Jaap Blaakmeer
+- Rutger Rademaker
### Special thanks to
- Benno Lippert
diff --git a/composer.json b/composer.json
index 5993e66..422dcc1 100755
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,8 @@
"description": "Purpose of this project is to check for different flaws that can occur due to EAV and provide cleanup functions.",
"require": {
"php": "~7.3||~8.0",
- "magento/magento2-base": "~2.3"
+ "magento/magento2-base": "~2.3",
+ "magento/module-eav": "^102.1"
},
"license": "MIT",
"type": "magento2-module",
@@ -14,5 +15,17 @@
"psr-4": {
"Hackathon\\EAVCleaner\\": ""
}
+ },
+ "repositories": {
+ "magento": {
+ "type": "composer",
+ "url": "https://repo.magento.com/"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "magento/magento-composer-installer": false,
+ "magento/composer-dependency-version-audit-plugin": false
+ }
}
}