Skip to content

Commit 0b0443a

Browse files
committed
Merge branch 'v1-develop' into v1
2 parents a6e5876 + 5664102 commit 0b0443a

31 files changed

+983
-108
lines changed

Checker/Catalog/Category/UrlKey.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category;
6+
7+
use Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlKey\DuplicateUrlKey as DuplicateUrlKeyChecker;
8+
use Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlKey\EmptyUrlKey as EmptyUrlKeyChecker;
9+
10+
class UrlKey
11+
{
12+
const URL_KEY_ATTRIBUTE = 'url_key';
13+
const STORAGE_IDENTIFIER = 'category-url-key';
14+
15+
private $duplicateUrlKeyChecker;
16+
private $emptyUrlKeyChecker;
17+
18+
public function __construct(
19+
DuplicateUrlKeyChecker $duplicateUrlKeyChecker,
20+
EmptyUrlKeyChecker $emptyUrlKeyChecker
21+
) {
22+
$this->duplicateUrlKeyChecker = $duplicateUrlKeyChecker;
23+
$this->emptyUrlKeyChecker = $emptyUrlKeyChecker;
24+
}
25+
26+
/**
27+
* @return array<array<string, mixed>>
28+
*/
29+
public function execute(): array
30+
{
31+
$categoryData = array_merge(
32+
$this->duplicateUrlKeyChecker->execute(),
33+
$this->emptyUrlKeyChecker->execute()
34+
);
35+
36+
return $categoryData;
37+
}
38+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlKey;
6+
7+
use Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlPath as UrlPathChecker;
8+
use Baldwin\UrlDataIntegrityChecker\Util\Stores as StoresUtil;
9+
10+
class DuplicateUrlKey
11+
{
12+
const DUPLICATED_PROBLEM_DESCRIPTION =
13+
'%s categories were found which have a duplicated url_key value: "%s" within the same parent.'
14+
. ' Please fix because this will cause problems.';
15+
16+
private $storesUtil;
17+
private $urlPathChecker;
18+
private $urlPathsInfo;
19+
20+
public function __construct(
21+
StoresUtil $storesUtil,
22+
UrlPathChecker $urlPathChecker
23+
) {
24+
$this->storesUtil = $storesUtil;
25+
$this->urlPathChecker = $urlPathChecker;
26+
$this->urlPathsInfo = [];
27+
}
28+
29+
/**
30+
* @return array<array<string, mixed>>
31+
*/
32+
public function execute(): array
33+
{
34+
$categoryData = $this->checkForDuplicatedUrlKeyAttributeValues();
35+
36+
return $categoryData;
37+
}
38+
39+
/**
40+
* @return array<array<string, mixed>>
41+
*/
42+
private function checkForDuplicatedUrlKeyAttributeValues(): array
43+
{
44+
$categoriesWithProblems = [];
45+
46+
$storeIds = $this->storesUtil->getAllStoreIds();
47+
foreach ($storeIds as $storeId) {
48+
$categoryUrlPaths = $this->getCategoryUrlPathsByStoreId($storeId);
49+
$urlPathsCount = array_count_values($categoryUrlPaths);
50+
51+
foreach ($urlPathsCount as $urlPath => $count) {
52+
if ($count === 1) {
53+
continue;
54+
}
55+
56+
$categories = $this->urlPathsInfo[$urlPath];
57+
58+
foreach ($categories as $category) {
59+
$categoriesWithProblems[] = [
60+
'catId' => (int) $category->getEntityId(),
61+
'name' => $category->getName(),
62+
'storeId' => $storeId,
63+
'problem' => sprintf(
64+
self::DUPLICATED_PROBLEM_DESCRIPTION,
65+
$count,
66+
$category->getUrlKey()
67+
),
68+
];
69+
}
70+
}
71+
}
72+
73+
return $categoriesWithProblems;
74+
}
75+
76+
/**
77+
* @return array<string>
78+
*/
79+
private function getCategoryUrlPathsByStoreId(int $storeId): array
80+
{
81+
$urlPaths = [];
82+
83+
$categories = $this->urlPathChecker->getAllVisibleCategoriesWithStoreId($storeId);
84+
foreach ($categories as $category) {
85+
$urlPath = $this->urlPathChecker->getCalculatedUrlPathForCategory($category, $storeId);
86+
87+
$rootCatId = 0;
88+
$path = $category->getPath() ?: '';
89+
if (preg_match('#^(\d+)/(\d+)/.+#', $path, $matches) === 1) {
90+
$rootCatId = $matches[2];
91+
}
92+
93+
$urlPath = $rootCatId . UrlPathChecker::URL_PATH_SEPARATOR . $urlPath;
94+
95+
$urlPaths[] = $urlPath;
96+
$this->urlPathsInfo[$urlPath][] = $category;
97+
}
98+
99+
return $urlPaths;
100+
}
101+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlKey;
6+
7+
use Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlKey as UrlKeyChecker;
8+
use Baldwin\UrlDataIntegrityChecker\Util\Stores as StoresUtil;
9+
use Magento\Catalog\Api\Data\CategoryInterface;
10+
use Magento\Catalog\Model\Attribute\ScopeOverriddenValueFactory as AttributeScopeOverriddenValueFactory;
11+
use Magento\Catalog\Model\Category as CategoryModel;
12+
use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
13+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
14+
use Magento\Store\Model\Store;
15+
16+
class EmptyUrlKey
17+
{
18+
const EMPTY_PROBLEM_DESCRIPTION = 'Category has an empty url_key value. This needs to be fixed.';
19+
20+
private $storesUtil;
21+
private $categoryCollectionFactory;
22+
private $attributeScopeOverriddenValueFactory;
23+
24+
public function __construct(
25+
StoresUtil $storesUtil,
26+
CategoryCollectionFactory $categoryCollectionFactory,
27+
AttributeScopeOverriddenValueFactory $attributeScopeOverriddenValueFactory
28+
) {
29+
$this->storesUtil = $storesUtil;
30+
$this->categoryCollectionFactory = $categoryCollectionFactory;
31+
$this->attributeScopeOverriddenValueFactory = $attributeScopeOverriddenValueFactory;
32+
}
33+
34+
/**
35+
* @return array<array<string, mixed>>
36+
*/
37+
public function execute(): array
38+
{
39+
$categoryData = $this->checkForEmptyUrlKeyAttributeValues();
40+
41+
return $categoryData;
42+
}
43+
44+
/**
45+
* @return array<array<string, mixed>>
46+
*/
47+
private function checkForEmptyUrlKeyAttributeValues(): array
48+
{
49+
$categoriesWithProblems = [];
50+
51+
$storeIds = $this->storesUtil->getAllStoreIds();
52+
foreach ($storeIds as $storeId) {
53+
// we need a left join when using the non-default store view
54+
// and especially for the case where storeId 0 doesn't have a value set for this attribute
55+
$joinType = $storeId === Store::DEFAULT_STORE_ID ? 'inner' : 'left';
56+
57+
$collection = $this->categoryCollectionFactory->create();
58+
$collection
59+
->setStoreId($storeId)
60+
->addAttributeToSelect(UrlKeyChecker::URL_KEY_ATTRIBUTE)
61+
->addAttributeToSelect('name')
62+
->addAttributeToFilter('level', ['gt' => 1]) // cats with levels 0 or 1 aren't used in the frontend
63+
->addAttributeToFilter('entity_id', ['neq' => CategoryModel::TREE_ROOT_ID])
64+
->addAttributeToFilter([
65+
[
66+
'attribute' => UrlKeyChecker::URL_KEY_ATTRIBUTE,
67+
'null' => true,
68+
],
69+
[
70+
'attribute' => UrlKeyChecker::URL_KEY_ATTRIBUTE,
71+
'eq' => '',
72+
],
73+
], null, $joinType)
74+
;
75+
76+
$categoriesWithProblems[] = $this->getCategoriesWithProblems($storeId, $collection);
77+
}
78+
79+
if (!empty($categoriesWithProblems)) {
80+
$categoriesWithProblems = array_merge(...$categoriesWithProblems);
81+
}
82+
83+
return $categoriesWithProblems;
84+
}
85+
86+
/**
87+
* @param CategoryCollection<CategoryModel> $collection
88+
*
89+
* @return array<array<string, mixed>>
90+
*/
91+
private function getCategoriesWithProblems(int $storeId, CategoryCollection $collection): array
92+
{
93+
$problems = [];
94+
95+
foreach ($collection as $category) {
96+
$isOverridden = $this
97+
->attributeScopeOverriddenValueFactory
98+
->create()
99+
->containsValue(CategoryInterface::class, $category, UrlKeyChecker::URL_KEY_ATTRIBUTE, $storeId)
100+
;
101+
102+
if ($isOverridden || $storeId === Store::DEFAULT_STORE_ID) {
103+
$problems[] = [
104+
'catId' => (int) $category->getEntityId(),
105+
'name' => $category->getName(),
106+
'storeId' => $storeId,
107+
'problem' => self::EMPTY_PROBLEM_DESCRIPTION,
108+
];
109+
}
110+
}
111+
112+
return $problems;
113+
}
114+
}

Checker/Catalog/Category/UrlPath.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public function checkForIncorrectUrlPathAttributeValues(): array
9191
/**
9292
* @return CategoryCollection<Category>
9393
*/
94-
private function getAllVisibleCategoriesWithStoreId(int $storeId): CategoryCollection
94+
public function getAllVisibleCategoriesWithStoreId(int $storeId): CategoryCollection
9595
{
9696
$categories = $this->categoryCollectionFactory->create()
9797
->addAttributeToSelect('name')
@@ -111,7 +111,7 @@ private function doesCategoryUrlPathMatchCalculatedUrlPath(Category $category, i
111111
return $calculatedUrlPath === $currentUrlPath;
112112
}
113113

114-
private function getCalculatedUrlPathForCategory(Category $category, int $storeId): string
114+
public function getCalculatedUrlPathForCategory(Category $category, int $storeId): string
115115
{
116116
if ($this->calculatedUrlPathPerCategoryAndStoreId === null) {
117117
$this->fetchAllCategoriesWithUrlPathCalculatedByUrlKey();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Baldwin\UrlDataIntegrityChecker\Console\Command;
6+
7+
use Baldwin\UrlDataIntegrityChecker\Checker\Catalog\Category\UrlKey as UrlKeyChecker;
8+
use Baldwin\UrlDataIntegrityChecker\Console\CategoryResultOutput;
9+
use Baldwin\UrlDataIntegrityChecker\Storage\Meta as MetaStorage;
10+
use Baldwin\UrlDataIntegrityChecker\Updater\Catalog\Category\UrlKey as UrlKeyUpdater;
11+
use Magento\Framework\App\Area as AppArea;
12+
use Magento\Framework\App\State as AppState;
13+
use Magento\Framework\Console\Cli;
14+
use Symfony\Component\Console\Command\Command as ConsoleCommand;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
19+
class CheckCategoryUrlKeys extends ConsoleCommand
20+
{
21+
private $appState;
22+
private $resultOutput;
23+
private $urlKeyUpdater;
24+
private $metaStorage;
25+
26+
public function __construct(
27+
AppState $appState,
28+
CategoryResultOutput $resultOutput,
29+
UrlKeyUpdater $urlKeyUpdater,
30+
MetaStorage $metaStorage
31+
) {
32+
$this->appState = $appState;
33+
$this->resultOutput = $resultOutput;
34+
$this->urlKeyUpdater = $urlKeyUpdater;
35+
$this->metaStorage = $metaStorage;
36+
37+
parent::__construct();
38+
}
39+
40+
protected function configure()
41+
{
42+
$this->setName('catalog:category:integrity:urlkey');
43+
$this->setDescription('Checks data integrity of the values of the url_key category attribute.');
44+
$this->addOption(
45+
'force',
46+
'f',
47+
InputOption::VALUE_NONE,
48+
'Force the command to run, even if it is already marked as already running'
49+
);
50+
51+
parent::configure();
52+
}
53+
54+
protected function execute(InputInterface $input, OutputInterface $output)
55+
{
56+
try {
57+
$this->appState->setAreaCode(AppArea::AREA_CRONTAB);
58+
59+
$force = $input->getOption('force');
60+
if ($force === true) {
61+
$this->metaStorage->clearStatus(UrlKeyChecker::STORAGE_IDENTIFIER);
62+
}
63+
64+
$categoryData = $this->urlKeyUpdater->refresh(MetaStorage::INITIATOR_CLI);
65+
$cliResult = $this->resultOutput->outputResult($categoryData, $output);
66+
67+
$output->writeln(
68+
"\n<info>Data was stored and you can now also review it in the admin of Magento</info>"
69+
);
70+
71+
return $cliResult;
72+
} catch (\Throwable $ex) {
73+
$output->writeln(
74+
"<error>An unexpected exception occured: '{$ex->getMessage()}'</error>\n{$ex->getTraceAsString()}"
75+
);
76+
}
77+
78+
return Cli::RETURN_FAILURE;
79+
}
80+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Baldwin\UrlDataIntegrityChecker\Controller\Adminhtml\Catalog\Category\UrlKey;
6+
7+
use Magento\Backend\App\Action as BackendAction;
8+
use Magento\Backend\App\Action\Context as BackendContext;
9+
use Magento\Backend\Model\View\Result\Page as BackendResultPage;
10+
use Magento\Framework\View\Result\PageFactory as ResultPageFactory;
11+
12+
class Index extends BackendAction
13+
{
14+
const ADMIN_RESOURCE = 'Baldwin_UrlDataIntegrityChecker::catalog_data_integrity';
15+
16+
private $resultPageFactory;
17+
18+
public function __construct(
19+
BackendContext $context,
20+
ResultPageFactory $resultPageFactory
21+
) {
22+
parent::__construct($context);
23+
24+
$this->resultPageFactory = $resultPageFactory;
25+
}
26+
27+
public function execute()
28+
{
29+
/** @var BackendResultPage */
30+
$resultPage = $this->resultPageFactory->create();
31+
$resultPage->setActiveMenu('Baldwin_UrlDataIntegrityChecker::catalog_category_urlkey');
32+
$resultPage->getConfig()->getTitle()->prepend('Data Integrity - Category Url Key');
33+
34+
return $resultPage;
35+
}
36+
}

0 commit comments

Comments
 (0)