Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.idea
.php_cs.cache
*~
vendor
composer.lock
*~
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 95 additions & 17 deletions Console/Command/RestoreUseDefaultValueCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,17 +29,30 @@ 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);

$this->iteratorFactory = $iteratorFactory;
$this->productMetaData = $productMetaData;
$this->resourceConnection = $resourceConnection;
$this->storeFilter = $storeFilter;
$this->attributeFilter = $attributeFilter;
}

protected function configure()
Expand All @@ -53,26 +69,63 @@ 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
{
$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.');
Expand All @@ -87,25 +140,41 @@ 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('<info>Now processing entity `%s` in table `%s`</info>', $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('<info>%s</info>', $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): 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 global values.
$query = $dbRead->query(
'SELECT * FROM ' . $fullTableName
. ' WHERE attribute_id = ? AND store_id = ? AND ' . $column . ' = ?',
[$row['attribute_id'], 0, $row[$column]]
);
}

$iterator = $this->iteratorFactory->create();
$iterator->walk($query, [function (array $result) use (&$counts, $dbWrite, $fullTableName, $isDryRun, $output, $row): void {
Expand All @@ -120,9 +189,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']])) {
Expand All @@ -140,9 +215,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
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 = sprintf('DELETE FROM ' . $fullTableName . ' WHERE store_id != 0 %s %s AND value IS NULL',
$storeIdFilter,
$attributeFilter
);
$output->writeln(sprintf('<info>%s</info>', $removeNullValuesQuery));
$dbWrite->query($removeNullValuesQuery);
}

if (count($counts)) {
Expand Down
73 changes: 73 additions & 0 deletions Filter/AttributeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Hackathon\EAVCleaner\Filter;

use Hackathon\EAVCleaner\Filter\Exception\AttributeDoesNotExistException;
use Magento\Eav\Model\ResourceModel\Entity\Attribute;
use Magento\Eav\Setup\EavSetupFactory;

class AttributeFilter
{
/**
* @var EavSetupFactory
*/
private $attribute;

/**
* @param Attribute $attribute
*/
public function __construct(
Attribute $attribute
) {
$this->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;
}
}
10 changes: 10 additions & 0 deletions Filter/Exception/AdminValuesCanNotBeRemovedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Hackathon\EAVCleaner\Filter\Exception;

use Symfony\Component\Console\Exception\InvalidOptionException;

class AdminValuesCanNotBeRemovedException extends InvalidOptionException
{

}
10 changes: 10 additions & 0 deletions Filter/Exception/AttributeDoesNotExistException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Hackathon\EAVCleaner\Filter\Exception;

use Symfony\Component\Console\Exception\InvalidOptionException;

class AttributeDoesNotExistException extends InvalidOptionException
{

}
10 changes: 10 additions & 0 deletions Filter/Exception/StoreDoesNotExistException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Hackathon\EAVCleaner\Filter\Exception;

use Symfony\Component\Console\Exception\InvalidOptionException;

class StoreDoesNotExistException extends InvalidOptionException
{

}
54 changes: 54 additions & 0 deletions Filter/StoreFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Hackathon\EAVCleaner\Filter;

use Hackathon\EAVCleaner\Filter\Exception\AdminValuesCanNotBeRemovedException;
use Hackathon\EAVCleaner\Filter\Exception\StoreDoesNotExistException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Store\Api\StoreRepositoryInterface;

class StoreFilter
{
/**
* @var StoreRepositoryInterface
*/
private $storeRepository;

public function __construct(StoreRepositoryInterface $storeRepository)
{
$this->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 = $e->getMessage() . ' | store ID: ' . $storeCode;
throw new StoreDoesNotExistException($error);
}

$storeIds[] = $storeId;
}

return sprintf('AND store_id in(%s)', implode(',', $storeIds));
} else {
return "";
}
}
}
Loading