diff --git a/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php b/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php
new file mode 100644
index 0000000000000..62eba5987c35d
--- /dev/null
+++ b/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php
@@ -0,0 +1,27 @@
+categoryRepository = $categoryRepository;
$this->productRepository = $productRepository;
+ $this->productResource = $productResource ?? ObjectManager::getInstance()->get(Product::class);
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function save(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $productLink)
{
@@ -60,7 +77,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $pro
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function delete(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $productLink)
{
@@ -68,7 +85,7 @@ public function delete(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $p
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function deleteByIds($categoryId, $sku)
{
@@ -101,4 +118,44 @@ public function deleteByIds($categoryId, $sku)
}
return true;
}
+
+ /**
+ * @inheritdoc
+ */
+ public function deleteBySkus(int $categoryId, array $productSkuList): bool
+ {
+ $category = $this->categoryRepository->get($categoryId);
+ $products = $this->productResource->getProductsIdsBySkus($productSkuList);
+
+ if (!$products) {
+ throw new InputException(__("The category doesn't contain the specified products."));
+ }
+
+ $productPositions = $category->getProductsPosition();
+
+ foreach ($products as $productId) {
+ if (isset($productPositions[$productId])) {
+ unset($productPositions[$productId]);
+ }
+ }
+
+ $category->setPostedProducts($productPositions);
+
+ try {
+ $category->save();
+ } catch (\Exception $e) {
+ throw new CouldNotSaveException(
+ __(
+ 'Could not save products "%products" to category %category',
+ [
+ "products" => implode(',', $productSkuList),
+ "category" => $category->getId()
+ ]
+ ),
+ $e
+ );
+ }
+
+ return true;
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkRepositoryTest.php
index b42262f1f0384..909b952078b58 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkRepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkRepositoryTest.php
@@ -6,40 +6,74 @@
namespace Magento\Catalog\Test\Unit\Model;
+use Magento\Catalog\Api\CategoryRepositoryInterface;
+use Magento\Catalog\Api\Data\CategoryProductLinkInterface;
+use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Catalog\Model\Category;
+use Magento\Catalog\Model\CategoryLinkRepository;
+use Magento\Catalog\Model\Product as ProductModel;
+use Magento\Catalog\Model\ResourceModel\Product;
+use Magento\Framework\Exception\CouldNotSaveException;
+use Magento\Framework\Exception\InputException;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+
+/**
+ * Test for \Magento\Catalog\Model\CategoryLinkRepository
+ * @SuppressWarnings(PHPMD.TooManyFields)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class CategoryLinkRepositoryTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \Magento\Catalog\Model\CategoryLinkRepository
+ * @var CategoryLinkRepository
*/
- protected $model;
+ private $model;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var CategoryRepositoryInterface|MockObject
*/
- protected $categoryRepositoryMock;
+ private $categoryRepositoryMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ProductRepositoryInterface|MockObject
*/
- protected $productRepositoryMock;
+ private $productRepositoryMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var CategoryProductLinkInterface|MockObject
*/
- protected $productLinkMock;
+ private $productLinkMock;
+ /**
+ * @var Product|MockObject
+ */
+ private $productResourceMock;
+
+ /**
+ * Initialize required data
+ */
protected function setUp()
{
- $this->categoryRepositoryMock = $this->createMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class);
- $this->productRepositoryMock = $this->createMock(\Magento\Catalog\Api\ProductRepositoryInterface::class);
- $this->productLinkMock = $this->createMock(\Magento\Catalog\Api\Data\CategoryProductLinkInterface::class);
- $this->model = new \Magento\Catalog\Model\CategoryLinkRepository(
+ $this->productResourceMock = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getProductsIdsBySkus'])
+ ->getMock();
+ $this->categoryRepositoryMock = $this->createMock(CategoryRepositoryInterface::class);
+ $this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class);
+ $this->productLinkMock = $this->createMock(CategoryProductLinkInterface::class);
+ $this->model = new CategoryLinkRepository(
$this->categoryRepositoryMock,
- $this->productRepositoryMock
+ $this->productRepositoryMock,
+ $this->productResourceMock
);
}
- public function testSave()
+ /**
+ * Assign a product to the category
+ *
+ * @return void
+ */
+ public function testSave(): void
{
$categoryId = 42;
$productId = 55;
@@ -47,10 +81,10 @@ public function testSave()
$sku = 'testSku';
$productPositions = [$productId => $productPosition];
$categoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\Category::class,
+ Category::class,
['getPostedProducts', 'getProductsPosition', 'setPostedProducts', 'save']
);
- $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productMock = $this->createMock(ProductModel::class);
$this->productLinkMock->expects($this->once())->method('getCategoryId')->willReturn($categoryId);
$this->productLinkMock->expects($this->once())->method('getSku')->willReturn($sku);
$this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
@@ -61,14 +95,16 @@ public function testSave()
$this->productLinkMock->expects($this->once())->method('getPosition')->willReturn($productPosition);
$categoryMock->expects($this->once())->method('setPostedProducts')->with($productPositions);
$categoryMock->expects($this->once())->method('save');
+
$this->assertTrue($this->model->save($this->productLinkMock));
}
/**
- * @expectedException \Magento\Framework\Exception\CouldNotSaveException
- * @expectedExceptionMessage Could not save product "55" with position 1 to category 42
+ * Assign a product to the category with `CouldNotSaveException`
+ *
+ * @return void
*/
- public function testSaveWithCouldNotSaveException()
+ public function testSaveWithCouldNotSaveException(): void
{
$categoryId = 42;
$productId = 55;
@@ -76,10 +112,10 @@ public function testSaveWithCouldNotSaveException()
$sku = 'testSku';
$productPositions = [$productId => $productPosition];
$categoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\Category::class,
+ Category::class,
['getProductsPosition', 'setPostedProducts', 'save', 'getId']
);
- $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productMock = $this->createMock(ProductModel::class);
$this->productLinkMock->expects($this->once())->method('getCategoryId')->willReturn($categoryId);
$this->productLinkMock->expects($this->once())->method('getSku')->willReturn($sku);
$this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
@@ -91,20 +127,28 @@ public function testSaveWithCouldNotSaveException()
$categoryMock->expects($this->once())->method('setPostedProducts')->with($productPositions);
$categoryMock->expects($this->once())->method('getId')->willReturn($categoryId);
$categoryMock->expects($this->once())->method('save')->willThrowException(new \Exception());
+
+ $this->expectExceptionMessage('Could not save product "55" with position 1 to category 42');
+ $this->expectException(CouldNotSaveException::class);
$this->model->save($this->productLinkMock);
}
- public function testDeleteByIds()
+ /**
+ * Remove the product assignment from the category
+ *
+ * @return void
+ */
+ public function testDeleteByIds(): void
{
- $categoryId = "42";
- $productSku = "testSku";
+ $categoryId = 42;
+ $productSku = 'testSku';
$productId = 55;
$productPositions = [55 => 1];
$categoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\Category::class,
+ Category::class,
['getProductsPosition', 'setPostedProducts', 'save', 'getId']
);
- $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productMock = $this->createMock(ProductModel::class);
$this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
->willReturn($categoryMock);
$this->productRepositoryMock->expects($this->once())->method('get')->with($productSku)
@@ -113,24 +157,26 @@ public function testDeleteByIds()
$productMock->expects($this->once())->method('getId')->willReturn($productId);
$categoryMock->expects($this->once())->method('setPostedProducts')->with([]);
$categoryMock->expects($this->once())->method('save');
+
$this->assertTrue($this->model->deleteByIds($categoryId, $productSku));
}
/**
- * @expectedException \Magento\Framework\Exception\CouldNotSaveException
- * @expectedExceptionMessage Could not save product "55" with position 1 to category 42
+ * Delete the product assignment from the category with `CouldNotSaveException`
+ *
+ * @return void
*/
- public function testDeleteByIdsWithCouldNotSaveException()
+ public function testDeleteByIdsWithCouldNotSaveException(): void
{
- $categoryId = "42";
- $productSku = "testSku";
+ $categoryId = 42;
+ $productSku = 'testSku';
$productId = 55;
$productPositions = [55 => 1];
$categoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\Category::class,
+ Category::class,
['getProductsPosition', 'setPostedProducts', 'save', 'getId']
);
- $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productMock = $this->createMock(ProductModel::class);
$this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
->willReturn($categoryMock);
$this->productRepositoryMock->expects($this->once())->method('get')->with($productSku)
@@ -140,50 +186,61 @@ public function testDeleteByIdsWithCouldNotSaveException()
$categoryMock->expects($this->once())->method('setPostedProducts')->with([]);
$categoryMock->expects($this->once())->method('getId')->willReturn($categoryId);
$categoryMock->expects($this->once())->method('save')->willThrowException(new \Exception());
+
+ $this->expectExceptionMessage('Could not save product "55" with position 1 to category 42');
+ $this->expectException(CouldNotSaveException::class);
$this->model->deleteByIds($categoryId, $productSku);
}
/**
- * @expectedException \Magento\Framework\Exception\InputException
- * @expectedExceptionMessage The category doesn't contain the specified product.
+ * Delete the product assignment from the category with `InputException`
+ *
+ * @return void
*/
- public function testDeleteWithInputException()
+ public function testDeleteWithInputException(): void
{
- $categoryId = "42";
- $productSku = "testSku";
+ $categoryId = 42;
+ $productSku = 'testSku';
$productId = 60;
$productPositions = [55 => 1];
$this->productLinkMock->expects($this->once())->method('getCategoryId')->willReturn($categoryId);
$this->productLinkMock->expects($this->once())->method('getSku')->willReturn($productSku);
$categoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\Category::class,
+ Category::class,
['getProductsPosition', 'setPostedProducts', 'save', 'getId']
);
- $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productMock = $this->createMock(ProductModel::class);
$this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
->willReturn($categoryMock);
$this->productRepositoryMock->expects($this->once())->method('get')->with($productSku)
->willReturn($productMock);
$categoryMock->expects($this->once())->method('getProductsPosition')->willReturn($productPositions);
$productMock->expects($this->once())->method('getId')->willReturn($productId);
-
$categoryMock->expects($this->never())->method('save');
+
+ $this->expectExceptionMessage('The category doesn\'t contain the specified product.');
+ $this->expectException(InputException::class);
$this->assertTrue($this->model->delete($this->productLinkMock));
}
- public function testDelete()
+ /**
+ * Delete the product assignment from the category
+ *
+ * @return void
+ */
+ public function testDelete(): void
{
- $categoryId = "42";
- $productSku = "testSku";
+ $categoryId = 42;
+ $productSku = 'testSku';
$productId = 55;
$productPositions = [55 => 1];
$this->productLinkMock->expects($this->once())->method('getCategoryId')->willReturn($categoryId);
$this->productLinkMock->expects($this->once())->method('getSku')->willReturn($productSku);
$categoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\Category::class,
+ Category::class,
['getProductsPosition', 'setPostedProducts', 'save', 'getId']
);
- $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productMock = $this->createMock(ProductModel::class);
$this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
->willReturn($categoryMock);
$this->productRepositoryMock->expects($this->once())->method('get')->with($productSku)
@@ -192,6 +249,82 @@ public function testDelete()
$productMock->expects($this->once())->method('getId')->willReturn($productId);
$categoryMock->expects($this->once())->method('setPostedProducts')->with([]);
$categoryMock->expects($this->once())->method('save');
+
$this->assertTrue($this->model->delete($this->productLinkMock));
}
+
+ /**
+ * Delete by products skus
+ *
+ * @return void
+ */
+ public function testDeleteBySkus(): void
+ {
+ $categoryId = 42;
+ $productSkus = ['testSku', 'testSku1', 'testSku2', 'testSku3'];
+ $productPositions = [55 => 1, 56 => 2, 57 => 3, 58 => 4];
+ $categoryMock = $this->createPartialMock(
+ Category::class,
+ ['getProductsPosition', 'setPostedProducts', 'save', 'getId']
+ );
+ $this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
+ ->willReturn($categoryMock);
+ $this->productResourceMock->expects($this->once())->method('getProductsIdsBySkus')
+ ->willReturn(['testSku' => 55, 'testSku1' => 56, 'testSku2' => 57, 'testSku3' => 58]);
+ $categoryMock->expects($this->once())->method('getProductsPosition')->willReturn($productPositions);
+ $categoryMock->expects($this->once())->method('setPostedProducts')->with([]);
+ $categoryMock->expects($this->once())->method('save');
+
+ $this->assertTrue($this->model->deleteBySkus($categoryId, $productSkus));
+ }
+
+ /**
+ * Delete by products skus with `InputException`
+ *
+ * @return void
+ */
+ public function testDeleteBySkusWithInputException(): void
+ {
+ $categoryId = 42;
+ $productSku = 'testSku';
+ $categoryMock = $this->createPartialMock(
+ Category::class,
+ ['getProductsPosition', 'setPostedProducts', 'save', 'getId']
+ );
+ $this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
+ ->willReturn($categoryMock);
+
+ $this->expectExceptionMessage('The category doesn\'t contain the specified products.');
+ $this->expectException(InputException::class);
+ $this->model->deleteBySkus($categoryId, [$productSku]);
+ }
+
+ /**
+ * Delete by products skus with `CouldNotSaveException`
+ *
+ * @return void
+ */
+ public function testDeleteSkusIdsWithCouldNotSaveException(): void
+ {
+ $categoryId = 42;
+ $productSku = 'testSku';
+ $productId = 55;
+ $productPositions = [55 => 1];
+ $categoryMock = $this->createPartialMock(
+ Category::class,
+ ['getProductsPosition', 'setPostedProducts', 'save', 'getId']
+ );
+ $this->categoryRepositoryMock->expects($this->once())->method('get')->with($categoryId)
+ ->willReturn($categoryMock);
+ $this->productResourceMock->expects($this->once())->method('getProductsIdsBySkus')
+ ->willReturn(['testSku' => $productId]);
+ $categoryMock->expects($this->once())->method('getProductsPosition')->willReturn($productPositions);
+ $categoryMock->expects($this->once())->method('setPostedProducts')->with([]);
+ $categoryMock->expects($this->once())->method('getId')->willReturn($categoryId);
+ $categoryMock->expects($this->once())->method('save')->willThrowException(new \Exception());
+
+ $this->expectExceptionMessage('Could not save products "testSku" to category 42');
+ $this->expectException(CouldNotSaveException::class);
+ $this->assertTrue($this->model->deleteBySkus($categoryId, [$productSku]));
+ }
}
diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml
index eda6dbd2d9d6f..223d690d28327 100644
--- a/app/code/Magento/Catalog/etc/di.xml
+++ b/app/code/Magento/Catalog/etc/di.xml
@@ -74,6 +74,7 @@
+