Skip to content

WIP: FIX #8897: "Use default" mechanism for Admin GUI and APIs #27035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
194 changes: 117 additions & 77 deletions app/code/Magento/Catalog/Model/CategoryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,122 +7,127 @@

namespace Magento\Catalog\Model;

use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Catalog\Model\ResourceModel\Category as CategoryResourceModel;
use Magento\Framework\Api\ExtensibleDataObjectConverter;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\StateException;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Store\Model\StoreManagerInterface;

/**
* Repository for categories.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInterface
class CategoryRepository implements CategoryRepositoryInterface
{
private const CACHE_TAG_ALL_STORES = 'all';

/**
* @var Category[]
*/
protected $instances = [];

/**
* @var \Magento\Store\Model\StoreManagerInterface
* @var StoreManagerInterface
*/
protected $storeManager;

/**
* @var \Magento\Catalog\Model\CategoryFactory
* @var CategoryFactory
*/
protected $categoryFactory;

/**
* @var \Magento\Catalog\Model\ResourceModel\Category
* @var CategoryResourceModel
*/
protected $categoryResource;
protected $categoryResourceModel;

/**
* @var \Magento\Framework\EntityManager\MetadataPool
* @var MetadataPool
*/
protected $metadataPool;

/**
* @var \Magento\Framework\Api\ExtensibleDataObjectConverter
* @var ExtensibleDataObjectConverter
*/
private $extensibleDataObjectConverter;

/**
* List of fields that can used config values in case when value does not defined directly
*
* @var array
* @var string[]
*/
protected $useConfigFields = ['available_sort_by', 'default_sort_by', 'filter_price_range'];

/**
* @param \Magento\Catalog\Model\CategoryFactory $categoryFactory
* @param \Magento\Catalog\Model\ResourceModel\Category $categoryResource
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param CategoryFactory $categoryFactory
* @param CategoryResourceModel $categoryResourceModel
* @param StoreManagerInterface $storeManager
* @param ExtensibleDataObjectConverter|null $extensibleDataObjectConverter
* @param MetadataPool $metadataPool
*/
public function __construct(
\Magento\Catalog\Model\CategoryFactory $categoryFactory,
\Magento\Catalog\Model\ResourceModel\Category $categoryResource,
\Magento\Store\Model\StoreManagerInterface $storeManager
CategoryFactory $categoryFactory,
CategoryResourceModel $categoryResourceModel,
StoreManagerInterface $storeManager,
ExtensibleDataObjectConverter $extensibleDataObjectConverter = null,
MetadataPool $metadataPool = null
) {
$this->categoryFactory = $categoryFactory;
$this->categoryResource = $categoryResource;
$this->categoryResourceModel = $categoryResourceModel;
$this->storeManager = $storeManager;
$this->extensibleDataObjectConverter = $extensibleDataObjectConverter
?? ObjectManager::getInstance()->get(ExtensibleDataObjectConverter::class);
$this->metadataPool = $metadataPool ?? ObjectManager::getInstance()->get(MetadataPool::class);
}

/**
* @inheritdoc
*/
public function save(\Magento\Catalog\Api\Data\CategoryInterface $category)
public function save(CategoryInterface $category)
{
$storeId = (int)$this->storeManager->getStore()->getId();
$existingData = $this->getExtensibleDataObjectConverter()
->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class);
$storeId = $this->getCategoryStoreId($category);

$existingData = $this->extensibleDataObjectConverter->toNestedArray($category, [], CategoryInterface::class);
$existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id']));
$existingData['store_id'] = $storeId;

$useDefaultAttributes = array_filter($category->getData(), function ($attributeValue) {
return null === $attributeValue;
});

if ($category->getId()) {
$metadata = $this->getMetadataPool()->getMetadata(
CategoryInterface::class
);
$metadata = $this->metadataPool->getMetadata(CategoryInterface::class);

$category = $this->get($category->getId(), $storeId);
$existingData[$metadata->getLinkField()] = $category->getData(
$metadata->getLinkField()
);

if (isset($existingData['image']) && is_array($existingData['image'])) {
if (!empty($existingData['image']['delete'])) {
$existingData['image'] = null;
} else {
if (isset($existingData['image'][0]['name']) && isset($existingData['image'][0]['tmp_name'])) {
$existingData['image'] = $existingData['image'][0]['name'];
} else {
unset($existingData['image']);
}
}
}
$existingData[$metadata->getLinkField()] = $category->getData($metadata->getLinkField());
$existingData = $this->handleCategoryImage($existingData);
} else {
$parentId = $category->getParentId() ?: $this->storeManager->getStore()->getRootCategoryId();
$parentCategory = $this->get($parentId, $storeId);
$existingData['path'] = $parentCategory->getPath();
$existingData['parent_id'] = $parentId;
}
$category->addData($existingData);

$this->updateUseDefaultAttributes($category, $useDefaultAttributes);

try {
$this->validateCategory($category);
$this->categoryResource->save($category);
$this->categoryResourceModel->save($category);
} catch (\Exception $e) {
throw new CouldNotSaveException(
__(
'Could not save category: %1',
$e->getMessage()
),
__('Could not save category: %1', $e->getMessage()),
$e
);
}
unset($this->instances[$category->getId()]);
$this->flushCategoryCache($category->getId());
return $this->get($category->getId(), $storeId);
}

Expand All @@ -131,40 +136,39 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category)
*/
public function get($categoryId, $storeId = null)
{
$cacheKey = $storeId ?? 'all';
$cacheKey = $storeId ?? self::CACHE_TAG_ALL_STORES;
if (!isset($this->instances[$categoryId][$cacheKey])) {
/** @var Category $category */
$category = $this->categoryFactory->create();
if (null !== $storeId) {
$category->setStoreId($storeId);
}
$category->load($categoryId);

$this->categoryResourceModel->load($category, $categoryId);

if (!$category->getId()) {
throw NoSuchEntityException::singleField('id', $categoryId);
}
$this->instances[$categoryId][$cacheKey] = $category;
}

return $this->instances[$categoryId][$cacheKey];
}

/**
* @inheritdoc
*/
public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category)
public function delete(CategoryInterface $category)
{
try {
$categoryId = $category->getId();
$this->categoryResource->delete($category);
$this->categoryResourceModel->delete($category);
} catch (\Exception $e) {
throw new StateException(
__(
'Cannot delete category with id %1',
$category->getId()
),
$e
);
throw new StateException(__('Cannot delete category with id %1', $category->getId()), $e);
}
unset($this->instances[$categoryId]);

$this->flushCategoryCache($categoryId);

return true;
}

Expand All @@ -174,15 +178,15 @@ public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category)
public function deleteByIdentifier($categoryId)
{
$category = $this->get($categoryId);
return $this->delete($category);
return $this->delete($category);
}

/**
* Validate category process
*
* @param Category $category
* @param Category $category
* @return void
* @throws \Magento\Framework\Exception\LocalizedException
* @throws LocalizedException
*/
protected function validateCategory(Category $category)
{
Expand All @@ -197,45 +201,81 @@ protected function validateCategory(Category $category)
if ($validate !== true) {
foreach ($validate as $code => $error) {
if ($error === true) {
$attribute = $this->categoryResource->getAttribute($code)->getFrontend()->getLabel();
throw new \Magento\Framework\Exception\LocalizedException(
$attribute = $this->categoryResourceModel->getAttribute($code)->getFrontend()->getLabel();
throw new LocalizedException(
__('The "%1" attribute is required. Enter and try again.', $attribute)
);
} else {
throw new \Magento\Framework\Exception\LocalizedException(__($error));
throw new LocalizedException(__($error));
}
}
}
$category->unsetData('use_post_data_config');
}

/**
* Lazy loader for the converter.
* If CategoryInterface object has `store_id` set, use it during save. Otherwise use Current `store_id`.
*
* @return \Magento\Framework\Api\ExtensibleDataObjectConverter
* @param CategoryInterface $category
* @return int
*
* @deprecated 101.0.0
* @throws NoSuchEntityException
*/
private function getExtensibleDataObjectConverter()
private function getCategoryStoreId(CategoryInterface $category): int
{
if ($this->extensibleDataObjectConverter === null) {
$this->extensibleDataObjectConverter = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Framework\Api\ExtensibleDataObjectConverter::class);
// return $this->storeManager->getStore()->getId();
if (method_exists($category, 'getStoreId') && $category->getStoreId() !== null) {
$categoryStoreId = (int)$category->getStoreId();
}
return $this->extensibleDataObjectConverter;

return $categoryStoreId ?? $this->storeManager->getStore()->getId();
}

/**
* This method fetches values of Category that should be inherited from global scope
*
* @param CategoryInterface $category
* @param array $useDefaultAttributes
* @return void
*/
private function updateUseDefaultAttributes(CategoryInterface $category, array $useDefaultAttributes = []): void
{
$category->setData('use_default', array_map(function () {
return true;
}, $useDefaultAttributes));
}

/**
* Method removes object by ID from Repository internal cache.
*
* @param int $categoryId
* @see \Magento\Catalog\Model\CategoryRepository::$instances
*/
private function flushCategoryCache(int $categoryId): void
{
unset($this->instances[$categoryId]);
}

/**
* Lazy loader for the metadata pool.
* Determines whether Delete or Update action was requested. Performs necessary actions.
*
* @return \Magento\Framework\EntityManager\MetadataPool
* @param array $categoryData
* @return array
*/
private function getMetadataPool()
private function handleCategoryImage(array $categoryData): array
{
if (null === $this->metadataPool) {
$this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Framework\EntityManager\MetadataPool::class);
if (isset($categoryData['image']) && is_array($categoryData['image'])) {
if (isset($categoryData['image']['delete']) && !empty($categoryData['image']['delete'])) {
$categoryData['image'] = null;
} else {
if (isset($categoryData['image'][0]['name']) && isset($categoryData['image'][0]['tmp_name'])) {
$categoryData['image'] = $categoryData['image'][0]['name'];
} else {
unset($categoryData['image']);
}
}
}
return $this->metadataPool;

return $categoryData;
}
}
3 changes: 2 additions & 1 deletion app/code/Magento/Catalog/etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,8 @@
</type>
<type name="Magento\Catalog\Model\CategoryRepository">
<arguments>
<argument name="categoryResource" xsi:type="object">Magento\Catalog\Model\ResourceModel\Category\Proxy</argument>
<argument name="categoryResourceModel" xsi:type="object">Magento\Catalog\Model\ResourceModel\Category\Proxy</argument>
<argument name="metadataPool" xsi:type="object">Magento\Framework\EntityManager\MetadataPool\Proxy</argument>
</arguments>
</type>
<type name="Magento\Catalog\Helper\Product\ConfigurationPool">
Expand Down
Loading