diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..0baf809
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+*.min.js
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..174dd28
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,57 @@
+{
+ "env": {
+ "amd": true,
+ "browser": true,
+ "jasmine": true
+ },
+ "rules": {
+ "consistent-return": 2,
+ "eqeqeq": [2, "smart"],
+ "guard-for-in": 2,
+ "lines-around-comment": [
+ 2,
+ {
+ "beforeBlockComment": true
+ }
+ ],
+ "max-len": [2, 120, 4],
+ "max-nested-callbacks": [2, 3],
+ "no-alert": 2,
+ "no-array-constructor": 2,
+ "no-caller": 2,
+ "no-catch-shadow": 2,
+ "no-else-return": 2,
+ "no-eval": 2,
+ "no-extend-native": 2,
+ "no-extra-bind": 2,
+ "no-floating-decimal": 2,
+ "no-implied-eval": 2,
+ "no-lone-blocks": 2,
+ "no-lonely-if": 2,
+ "no-loop-func": 2,
+ "no-multi-str": 2,
+ "no-new-object": 2,
+ "no-proto": 2,
+ "no-return-assign": 2,
+ "no-self-compare": 2,
+ "no-shadow": 2,
+ "no-undef-init": 2,
+ "no-unused-vars": [
+ 2,
+ {
+ "args": "after-used",
+ "vars": "all",
+ "varsIgnorePattern": "^config$"
+ }
+ ],
+ "no-with": 2,
+ "operator-assignment": [2, "always"],
+ "radix": 2,
+ "semi": [2, "always"],
+ "semi-spacing": 2,
+ "space-before-blocks": "error",
+ "space-before-function-paren":"error",
+ "func-style": [2, "expression"],
+ "eol-last":"error"
+ }
+}
\ No newline at end of file
diff --git a/Iazel/RegenProductUrl/Console/Command/RegenerateCategoryPathCommand.php b/Iazel/RegenProductUrl/Console/Command/RegenerateCategoryPathCommand.php
deleted file mode 100644
index c4c5d4d..0000000
--- a/Iazel/RegenProductUrl/Console/Command/RegenerateCategoryPathCommand.php
+++ /dev/null
@@ -1,145 +0,0 @@
-state = $state;
- $this->categoryUrlPathGenerator = $categoryUrlPathGenerator;
- $this->urlPersist = $urlPersist;
- parent::__construct();
- $this->categoryCollectionFactory = $categoryCollectionFactory;
- $this->eventManager = $eventManager;
- $this->categoryResource = $categoryResource;
- $this->emulation = $emulation;
- }
-
- protected function configure()
- {
- $this->setName('regenerate:category:path')
- ->setDescription('Regenerate path for given categories')
- ->addArgument(
- 'cids',
- InputArgument::IS_ARRAY,
- 'Categories to regenerate'
- )
- ->addOption(
- 'store', 's',
- InputOption::VALUE_REQUIRED,
- 'Use the specific Store View',
- Store::DEFAULT_STORE_ID
- )
- ;
- return parent::configure();
- }
-
- public function execute(InputInterface $inp, OutputInterface $out)
- {
- try{
- $this->state->getAreaCode();
- }catch ( \Magento\Framework\Exception\LocalizedException $e){
- $this->state->setAreaCode('adminhtml');
- }
-
- $store_id = $inp->getOption('store');
-
- $categories = $this->categoryCollectionFactory->create()
- ->setStore($store_id)
- ->addAttributeToSelect(['name', 'url_path', 'url_key']);
-
- $cids = $inp->getArgument('cids');
- if( !empty($cids) ) {
- $categories->addAttributeToFilter('entity_id', ['in' => $cids]);
- }
-
- $regenerated = 0;
- foreach($categories as $category)
- {
- $out->writeln('Regenerating urls for ' . $category->getName() . ' (' . $category->getId() . ')');
-
- $category->setOrigData('url_key', mt_rand(1,1000)); // set url_key in orig data to random value to force regeneration of path
- $category->setOrigData('url_path', mt_rand(1,1000)); // set url_path in orig data to random value to force regeneration of path for children
-
- // Make use of Magento's event for this
- $this->emulation->startEnvironmentEmulation($store_id, Area::AREA_FRONTEND, true);
- $this->eventManager->dispatch('regenerate_category_url_path', ['category' => $category]);
- $this->emulation->stopEnvironmentEmulation();
-
- $regenerated++;
- }
-
- $out->writeln('Done regenerating. Regenerated url paths for ' . $regenerated . ' categories');
- }
-}
diff --git a/Iazel/RegenProductUrl/Console/Command/RegenerateCategoryUrlCommand.php b/Iazel/RegenProductUrl/Console/Command/RegenerateCategoryUrlCommand.php
deleted file mode 100644
index 72dd260..0000000
--- a/Iazel/RegenProductUrl/Console/Command/RegenerateCategoryUrlCommand.php
+++ /dev/null
@@ -1,160 +0,0 @@
-state = $state;
- $this->collection = $collection;
- $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
- $this->urlPersist = $urlPersist;
- $this->categoryCollectionFactory = $categoryCollectionFactory;
-
- parent::__construct();
- $this->emulation = $emulation;
- }
-
- protected function configure()
- {
- $this->setName('regenerate:category:url')
- ->setDescription('Regenerate url for given categories')
- ->addArgument(
- 'cids',
- InputArgument::IS_ARRAY,
- 'Categories to regenerate'
- )
- ->addOption(
- 'store', 's',
- InputOption::VALUE_REQUIRED,
- 'Use the specific Store View',
- Store::DEFAULT_STORE_ID
- )
- ;
- return parent::configure();
- }
-
- public function execute(InputInterface $inp, OutputInterface $out)
- {
- try{
- $this->state->getAreaCode();
- }catch ( \Magento\Framework\Exception\LocalizedException $e){
- $this->state->setAreaCode('adminhtml');
- }
-
- $store_id = $inp->getOption('store');
- $this->emulation->startEnvironmentEmulation($store_id, Area::AREA_FRONTEND, true);
-
- $categories = $this->categoryCollectionFactory->create()
- ->setStore($store_id)
- ->addAttributeToSelect(['name', 'url_path', 'url_key']);
-
- $cids = $inp->getArgument('cids');
- if( !empty($cids) ) {
- $categories->addAttributeToFilter('entity_id', ['in' => $cids]);
- }
-
- $regenerated = 0;
- foreach($categories as $category)
- {
- $out->writeln('Regenerating urls for ' . $category->getName() . ' (' . $category->getId() . ')');
-
- $this->urlPersist->deleteByData([
- UrlRewrite::ENTITY_ID => $category->getId(),
- UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE,
- UrlRewrite::REDIRECT_TYPE => 0,
- UrlRewrite::STORE_ID => $store_id
- ]);
-
- $newUrls = $this->categoryUrlRewriteGenerator->generate($category);
- try {
- $newUrls = $this->filterEmptyRequestPaths($newUrls);
- $this->urlPersist->replace($newUrls);
- $regenerated += count($newUrls);
- }
- catch(\Exception $e) {
- $out->writeln(sprintf('Duplicated url for store ID %d, category %d (%s) - %s Generated URLs:' . PHP_EOL . '%s' . PHP_EOL, $store_id, $category->getId(), $category->getName(), $e->getMessage(), implode(PHP_EOL, array_keys($newUrls))));
- }
- }
- $this->emulation->stopEnvironmentEmulation();
- $out->writeln('Done regenerating. Regenerated ' . $regenerated . ' urls');
- }
-
- /**
- * Remove entries with request_path='' to prevent error 404 for "http://site.com/" address.
- *
- * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] $newUrls
- * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[]
- */
- private function filterEmptyRequestPaths($newUrls)
- {
- $result = [];
- foreach ($newUrls as $key => $url) {
- $requestPath = $url->getRequestPath();
- if (!empty($requestPath)) {
- $result[$key] = $url;
- }
- }
- return $result;
- }
-}
diff --git a/Iazel/RegenProductUrl/Console/Command/RegenerateCmsPageUrlCommand.php b/Iazel/RegenProductUrl/Console/Command/RegenerateCmsPageUrlCommand.php
deleted file mode 100644
index 67806e0..0000000
--- a/Iazel/RegenProductUrl/Console/Command/RegenerateCmsPageUrlCommand.php
+++ /dev/null
@@ -1,147 +0,0 @@
-state = $state;
- $this->emulation = $emulation;
- $this->pageCollectionFactory = $pageCollectionFactory;
- $this->urlPersist = $urlPersist;
- $this->cmsPageUrlRewriteGenerator = $cmsPageUrlRewriteGenerator;
- }
-
- protected function configure()
- {
- $this->setName('regenerate:cms-page:url')
- ->setDescription('Regenerate url for cms pages.')
- ->addArgument(
- 'pids',
- InputArgument::IS_ARRAY,
- 'CMS Pages to regenerate'
- )
- ->addOption(
- 'store',
- 's',
- InputOption::VALUE_OPTIONAL,
- 'Regenerate for one specific store view',
- Store::DEFAULT_STORE_ID
- )->addOption(
- 'all-stores',
- 'a',
- InputOption::VALUE_OPTIONAL,
- 'Regenerate for all stores.',
- false
- );
- }
-
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- *
- * @return int|void|null
- * @throws \Magento\Framework\Exception\LocalizedException
- */
- protected function execute(InputInterface $input, OutputInterface $output)
- {
- $output->writeln('Start regenerating urls for CMS pages.');
- try {
- $this->state->getAreaCode();
- } catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->state->setAreaCode(Area::AREA_ADMINHTML);
- }
-
- $storeId = $input->getOption('store');
- $this->emulation->startEnvironmentEmulation($storeId, Area::AREA_FRONTEND, true);
-
- $pages = $this->pageCollectionFactory->create();
-
- if (!$input->getOption('all-stores') !== false) {
- $pages->addStoreFilter($storeId);
- }
-
- if (count($input->getArgument('pids')) > 0) {
- $pageIds = $input->getArgument('pids');
- $pages->addFieldToFilter('page_id', ['in' => $pageIds]);
- }
-
- $regenerated = 0;
- /** @var Page $page */
- foreach ($pages as $page) {
- $newUrls = $this->cmsPageUrlRewriteGenerator->generate($page);
- try {
- $this->urlPersist->replace($newUrls);
- $regenerated += count($newUrls);
- } catch (UrlAlreadyExistsException $e) {
- $output->writeln(sprintf('Url for page %s (%d) already exists.' . PHP_EOL . '%s',
- $page->getTitle(), $page->getId(), $e->getMessage()));
- } catch (\Exception $e) {
- $output->writeln(sprintf('Couldn\'t replace url for %s (%d)' . PHP_EOL . '%s'));
- }
- }
-
- $this->emulation->stopEnvironmentEmulation();
- $output->writeln(sprintf('Finished regenerating. Regenerated %d urls.', $regenerated));
- return Cli::RETURN_SUCCESS;
- }
-}
\ No newline at end of file
diff --git a/Iazel/RegenProductUrl/Console/Command/RegenerateProductUrlCommand.php b/Iazel/RegenProductUrl/Console/Command/RegenerateProductUrlCommand.php
deleted file mode 100644
index a340c3a..0000000
--- a/Iazel/RegenProductUrl/Console/Command/RegenerateProductUrlCommand.php
+++ /dev/null
@@ -1,160 +0,0 @@
-state = $state;
- $this->collection = $collection;
- $this->productUrlRewriteGenerator = $productUrlRewriteGenerator;
- $this->urlPersist = $urlPersist;
- parent::__construct();
- $this->storeManager = $storeManager;
- }
-
- protected function configure()
- {
- $this->setName('regenerate:product:url')
- ->setDescription('Regenerate url for given products')
- ->addOption(
- 'store', 's',
- InputOption::VALUE_REQUIRED,
- 'Regenerate for one specific store view',
- Store::DEFAULT_STORE_ID
- )
- ->addArgument(
- 'pids',
- InputArgument::IS_ARRAY,
- 'Product IDs to regenerate'
- );
- }
-
- public function execute(InputInterface $input, OutputInterface $output)
- {
- try {
- $this->state->getAreaCode();
- } catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->state->setAreaCode('adminhtml');
- }
-
- $storeId = $input->getOption('store');
- $stores = $this->storeManager->getStores(false);
-
- if (!is_numeric($storeId)) {
- $storeId = $this->getStoreIdByCode($storeId, $stores);
- }
-
- if (!is_numeric($storeId)) {
- throw new \Exception('Store could not be found. Please enter a store ID or a store code.');
- } else {
- $this->storeManager->getStore($storeId);
- }
-
- foreach ($stores as $store) {
- // If store has been given through option, skip other stores
- if ($storeId != Store::DEFAULT_STORE_ID AND $store->getId() != $storeId) {
- continue;
- }
-
- $this->collection
- ->addStoreFilter($store->getId())
- ->setStoreId($store->getId())
- ->addFieldToFilter('visibility', ['gt' => Visibility::VISIBILITY_NOT_VISIBLE]);
-
- $pids = $input->getArgument('pids');
- if (!empty($pids)) {
- $this->collection->addIdFilter($pids);
- }
-
- $this->collection->addAttributeToSelect(['url_path', 'url_key']);
- $list = $this->collection->load();
- $regenerated = 0;
-
- /** @var \Magento\Catalog\Model\Product $product */
- foreach ($list as $product) {
- echo 'Regenerating urls for ' . $product->getSku() . ' (' . $product->getId() . ') in store ' . $store->getName() . PHP_EOL;
- $product->setStoreId($store->getId());
-
- $this->urlPersist->deleteByData([
- UrlRewrite::ENTITY_ID => $product->getId(),
- UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE,
- UrlRewrite::REDIRECT_TYPE => 0,
- UrlRewrite::STORE_ID => $store->getId()
- ]);
-
- $newUrls = $this->productUrlRewriteGenerator->generate($product);
- try {
- $this->urlPersist->replace($newUrls);
- $regenerated += count($newUrls);
- } catch (\Exception $e) {
- $output->writeln(sprintf('Duplicated url for store ID %d, product %d (%s) - %s Generated URLs:' . PHP_EOL . '%s' . PHP_EOL, $store->getId(), $product->getId(), $product->getSku(), $e->getMessage(), implode(PHP_EOL, array_keys($newUrls))));
- }
- }
- $output->writeln('Done regenerating. Regenerated ' . $regenerated . ' urls for store ' . $store->getName());
- }
- }
-
- private function getStoreIdByCode($store_id, $stores)
- {
- foreach ($stores as $store) {
- if ($store->getCode() == $store_id) {
- return $store->getId();
- }
- }
-
- return false;
- }
-}
diff --git a/Iazel/RegenProductUrl/Model/CategoryUrlPathGenerator.php b/Iazel/RegenProductUrl/Model/CategoryUrlPathGenerator.php
deleted file mode 100644
index 93a0899..0000000
--- a/Iazel/RegenProductUrl/Model/CategoryUrlPathGenerator.php
+++ /dev/null
@@ -1,21 +0,0 @@
-isObjectNew() || $category->getLevel() >= self::MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING;
- }
-}
\ No newline at end of file
diff --git a/Iazel/RegenProductUrl/etc/di.xml b/Iazel/RegenProductUrl/etc/di.xml
deleted file mode 100644
index 81c45e9..0000000
--- a/Iazel/RegenProductUrl/etc/di.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
- - Iazel\RegenProductUrl\Console\Command\RegenerateProductUrlCommand
- - Iazel\RegenProductUrl\Console\Command\RegenerateCategoryUrlCommand
- - Iazel\RegenProductUrl\Console\Command\RegenerateCategoryPathCommand
- - Iazel\RegenProductUrl\Console\Command\RegenerateCmsPageUrlCommand
-
-
-
-
-
diff --git a/Iazel/RegenProductUrl/etc/events.xml b/Iazel/RegenProductUrl/etc/events.xml
deleted file mode 100644
index 284933e..0000000
--- a/Iazel/RegenProductUrl/etc/events.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
diff --git a/Iazel/RegenProductUrl/etc/module.xml b/Iazel/RegenProductUrl/etc/module.xml
deleted file mode 100644
index 7c691f1..0000000
--- a/Iazel/RegenProductUrl/etc/module.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/Iazel/RegenProductUrl/registration.php b/Iazel/RegenProductUrl/registration.php
deleted file mode 100644
index d11a223..0000000
--- a/Iazel/RegenProductUrl/registration.php
+++ /dev/null
@@ -1,6 +0,0 @@
-=100.1",
- "magento/module-catalog-url-rewrite": ">=100.1",
- "magento/magento-composer-installer": "*"
- },
- "type": "magento2-module",
- "license": [
- "OSL-3.0",
- "AFL-3.0"
- ],
- "autoload": {
- "files": [
- "Iazel/RegenProductUrl/registration.php"
+ "name": "elgentos/regenerate-catalog-urls",
+ "description": "Regenerate Catalog URL Rewrites (products, categories, cms pages)",
+ "type": "magento2-module",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
],
- "psr-4": {
- "Iazel\\RegenProductUrl\\": "Iazel/RegenProductUrl"
+ "require": {
+ "php": "~7.4|~8.0|~8.1|~8.2|~8.3",
+ "magento/framework": "^102.0|^103.0",
+ "magento/module-catalog-url-rewrite": "^100.3|^100.4",
+ "magento/module-url-rewrite": "^101.1|^102.0"
+ },
+ "minimum-stability": "stable",
+ "archive": {
+ "exclude": [
+ "/.gitignore",
+ "/grumphp.yml",
+ "/pdepend.xml",
+ "/phpstan.neon",
+ "/phpunit.xml",
+ "/phpcs.xml",
+ "/phpmd.xml",
+ "/package.json",
+ "/.eslintrc.json",
+ "/.eslintignore",
+ "/tests"
+ ]
+ },
+ "replace": {
+ "iazel/module-regen-product-url": "*"
+ },
+ "suggest": {
+ "baldwin/magento2-module-url-data-integrity-checker": "Magento 2 module which can find potential url related problems in your catalog data",
+ "fisheye/module-url-rewrite-optimiser": "A Magento module that stops URL rewrites with category paths being generated for products when 'Use Categories Path for Product URLs' setting is disabled in config."
+ },
+ "require-dev": {
+ "mediact/testing-suite": "^2.9",
+ "mediact/coding-standard-magento2": "@stable"
+ },
+ "repositories": {
+ "magento": {
+ "type": "composer",
+ "url": "https://repo.magento.com/"
+ }
+ },
+ "archive": {
+ "exclude": [
+ "/.gitignore",
+ "/grumphp.yml",
+ "/pdepend.xml",
+ "/phpstan.neon",
+ "/phpunit.xml",
+ "/phpcs.xml",
+ "/phpmd.xml",
+ "/package.json",
+ "/.eslintrc.json",
+ "/.eslintignore",
+ "/tests"
+ ]
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "autoload": {
+ "files": [
+ "src/registration.php"
+ ],
+ "psr-4": {
+ "Elgentos\\RegenerateCatalogUrls\\": "src/"
+ }
}
- },
- "replace": {
- "iazel/module-regen-product-url": "*"
- }
}
diff --git a/grumphp.yml b/grumphp.yml
new file mode 100644
index 0000000..3779b23
--- /dev/null
+++ b/grumphp.yml
@@ -0,0 +1,2 @@
+imports:
+ - resource: 'vendor/mediact/testing-suite/config/default/grumphp.yml'
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b76468f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "eslint": "^4.19.1"
+ }
+}
\ No newline at end of file
diff --git a/pdepend.xml b/pdepend.xml
new file mode 100644
index 0000000..98099fd
--- /dev/null
+++ b/pdepend.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ memory
+
+
+
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..12a9a3c
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,10 @@
+
+
+ PHPCS
+
+
+
diff --git a/phpmd.xml b/phpmd.xml
new file mode 100644
index 0000000..4852635
--- /dev/null
+++ b/phpmd.xml
@@ -0,0 +1,10 @@
+
+
+ PHPMD
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..5d19f6e
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,9 @@
+parameters:
+ level: 6
+ paths:
+ - src
+ - tests
+ ignoreErrors:
+ - '#(class|type) Magento\\TestFramework#i'
+ - '#(class|type) Magento\\\S*Factory#i'
+ - '#(method) Magento\\Framework\\Api\\ExtensionAttributesInterface#i'
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..8d7cdbb
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,15 @@
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
diff --git a/src/Console/Command/AbstractRegenerateCommand.php b/src/Console/Command/AbstractRegenerateCommand.php
new file mode 100644
index 0000000..8375d8d
--- /dev/null
+++ b/src/Console/Command/AbstractRegenerateCommand.php
@@ -0,0 +1,153 @@
+storeManager = $storeManager;
+ $this->state = $state;
+ $this->regenerateProductUrl = $regenerateProductUrl;
+ $this->questionHelper = $questionHelper;
+ parent::__construct();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function configure()
+ {
+ $this->addOption(
+ 'store',
+ 's',
+ InputOption::VALUE_OPTIONAL,
+ 'Regenerate for a specific store view',
+ false
+ );
+ }
+
+ /**
+ * Get chosen stores
+ *
+ * @return array
+ * @throws LocalizedException
+ */
+ protected function getChosenStores(): array
+ {
+ $storeInput = $this->input->getOption('store');
+
+ if ($this->storeManager->isSingleStoreMode()) {
+ return [1];
+ }
+
+ $storeId = false;
+ if (is_numeric($storeInput)) {
+ $storeId = (int)$storeInput;
+ } elseif ($storeInput === 'all') {
+ $storeId = $storeInput;
+ } elseif (is_string($storeInput)) {
+ $storeId = $this->getStoreIdByCode($storeInput);
+ } elseif (false === $storeInput) {
+ $choices = array_merge(['all'], array_map(fn ($store) => $store->getCode(), $this->getAllStores()));
+ $question = new ChoiceQuestion(__('Pick a store')->getText(), $choices, 'all');
+ $storeCode = $this->questionHelper->ask($this->input, $this->output, $question);
+ $storeId = ($storeCode === 'all' ? 'all' : $this->getStoreIdByCode($storeCode));
+ }
+
+ if ($storeId === 'all') {
+ $stores = array_map(fn ($store) => $store->getId(), $this->getAllStores());
+ } else {
+ $stores = [$storeId];
+ }
+
+ return $stores;
+ }
+
+ /**
+ * Get all stores
+ *
+ * @param bool|null $withDefault
+ * @return array
+ */
+ protected function getAllStores(?bool $withDefault = false): array
+ {
+ return $this->storeManager->getStores($withDefault);
+ }
+
+ /**
+ * Get Store ID by code
+ *
+ * @param string $storeCode
+ * @return null|int
+ * @throws LocalizedException
+ */
+ protected function getStoreIdByCode(string $storeCode): ?int
+ {
+ foreach ($this->getAllStores() as $store) {
+ if ($store->getCode() === $storeCode) {
+ return (int)$store->getId();
+ }
+ }
+
+ throw new LocalizedException(__(
+ 'The store that was requested (%1) wasn\'t found. Verify the store and try again.',
+ $storeCode
+ ));
+ }
+}
diff --git a/src/Console/Command/RegenerateCategoryPathCommand.php b/src/Console/Command/RegenerateCategoryPathCommand.php
new file mode 100644
index 0000000..8497379
--- /dev/null
+++ b/src/Console/Command/RegenerateCategoryPathCommand.php
@@ -0,0 +1,147 @@
+categoryCollectionFactory = $categoryCollectionFactory;
+ $this->eventManager = $eventManager;
+ $this->emulation = $emulation;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function configure(): void
+ {
+ $this->setName('regenerate:category:path')
+ ->setDescription('Regenerate path for given categories')
+ ->addArgument(
+ 'cids',
+ InputArgument::IS_ARRAY,
+ 'Categories to regenerate'
+ )->addOption(
+ 'root',
+ 'r',
+ InputOption::VALUE_OPTIONAL,
+ 'Regenerate for root category and its children only',
+ false
+ );
+
+ parent::configure();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ try {
+ $this->state->getAreaCode();
+ } catch (LocalizedException $e) {
+ $this->state->setAreaCode('adminhtml');
+ }
+
+ $stores = $this->getChosenStores();
+
+ foreach ($stores as $storeId) {
+ $categories = $this->categoryCollectionFactory->create()
+ ->setStore($storeId)
+ ->addAttributeToSelect(['name', 'url_path', 'url_key', 'path'])
+ ->addAttributeToFilter('level', ['gt' => 1]);
+
+ $fromRootOnly = (int)$input->getOption('root') ?? 0;
+ $categoryIds = $input->getArgument('cids');
+ if ($fromRootOnly) {
+ //path LIKE '1/rootcategory/%' OR path = '1/rootcategory'
+ $categories->addAttributeToFilter('path', [
+ 'like' => '1/' . $fromRootOnly . '/%',
+ '=' => '1/' . $fromRootOnly
+ ]);
+ } elseif (!empty($categoryIds)) {
+ $categories->addAttributeToFilter('entity_id', ['in' => $categoryIds]);
+ }
+
+ $counter = 0;
+
+ foreach ($categories as $category) {
+ $output->writeln(
+ sprintf('Regenerating paths for %s (%s)', $category->getName(), $category->getId())
+ );
+
+ // set url_key in orig data to random value to force regeneration of path
+ $category->setOrigData('url_key', random_int(1, 1000));
+
+ // set url_path in orig data to random value to force regeneration of path for children
+ $category->setOrigData('url_path', random_int(1, 1000));
+
+ // Make use of Magento's event for this
+ $this->emulation->startEnvironmentEmulation($storeId, Area::AREA_FRONTEND, true);
+ $this->eventManager->dispatch('regenerate_category_url_path', ['category' => $category]);
+ $this->emulation->stopEnvironmentEmulation();
+
+ $counter++;
+ }
+
+ $output->writeln(
+ sprintf('Done regenerating. Regenerated url paths for %d categories', $counter)
+ );
+ }
+
+ return Cli::RETURN_SUCCESS;
+ }
+}
diff --git a/src/Console/Command/RegenerateCategoryUrlCommand.php b/src/Console/Command/RegenerateCategoryUrlCommand.php
new file mode 100644
index 0000000..8272af8
--- /dev/null
+++ b/src/Console/Command/RegenerateCategoryUrlCommand.php
@@ -0,0 +1,239 @@
+categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
+ $this->urlPersist = $urlPersist;
+ $this->categoryCollectionFactory = $categoryCollectionFactory;
+ $this->emulation = $emulation;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function configure(): void
+ {
+ $this->setName('regenerate:category:url')
+ ->setDescription('Regenerate url for given categories')
+ ->addArgument(
+ 'cids',
+ InputArgument::IS_ARRAY,
+ 'Categories to regenerate'
+ )->addOption(
+ 'root',
+ 'r',
+ InputOption::VALUE_OPTIONAL,
+ 'Regenerate for root category and its children only',
+ false
+ );
+
+ parent::configure();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ try {
+ $this->state->getAreaCode();
+ } catch (LocalizedException $e) {
+ $this->state->setAreaCode('adminhtml');
+ }
+
+ $counter = 0;
+
+ $stores = $this->getChosenStores();
+
+ $rootIdOption = (int)$input->getOption('root') ?: false;
+
+ foreach ($stores as $storeId) {
+ $currentRootId = $this->storeManager->getGroup(
+ $this->storeManager->getStore($storeId)->getStoreGroupId()
+ )->getRootCategoryId();
+ if ($rootIdOption !== false) {
+ $fromRootId = $rootIdOption;
+ if ($rootIdOption !== $currentRootId) {
+ $output->writeln(
+ sprintf(
+ 'Skipping store %s because its root category id %s, differs from the passed root category %s', //phpcs:ignore Generic.Files.LineLength.TooLong
+ $storeId,
+ $currentRootId,
+ $fromRootId
+ )
+ );
+ continue;
+ }
+ } else {
+ $fromRootId = $currentRootId;
+ }
+
+ $output->writeln(
+ sprintf('Processing store %s...', $storeId)
+ );
+
+ $rootCategory = $this->categoryCollectionFactory->create()->addAttributeToFilter(
+ 'entity_id',
+ $fromRootId
+ )->addAttributeToSelect("name")->getFirstItem();
+ if (!$rootCategory->getId()) {
+ throw new Exception(sprintf("Root category with ID %d, was not found.", $fromRootId));
+ }
+ $this->emulation->startEnvironmentEmulation($storeId, Area::AREA_FRONTEND, true);
+
+ $categories = $this->categoryCollectionFactory->create()
+ ->setStore($storeId)
+ ->addAttributeToSelect(['name', 'url_path', 'url_key', 'path'])
+ ->addAttributeToFilter('level', ['gt' => 1]);
+
+ $categoryIds = $input->getArgument('cids');
+ if ($fromRootId) {
+ //path LIKE '1/rootcategory/%' OR path = '1/rootcategory'
+ $categories->addAttributeToFilter('path', [
+ 'like' => '1/' . $fromRootId . '/%',
+ '=' => '1/' . $fromRootId
+ ]);
+ }
+ if (!empty($categoryIds)) {
+ $categories->addAttributeToFilter('entity_id', ['in' => $categoryIds]);
+ }
+
+ foreach ($categories as $category) {
+ $output->writeln(
+ sprintf(
+ 'Regenerating urls for %s (%s)',
+ implode('/', [
+ $rootCategory->getName(),
+ ...array_map(fn ($category) => $category->getName(), $category->getParentCategories())
+ ]),
+ $category->getId()
+ )
+ );
+
+ $this->urlPersist->deleteByData(
+ [
+ UrlRewrite::ENTITY_ID => $category->getId(),
+ UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE,
+ UrlRewrite::REDIRECT_TYPE => 0,
+ UrlRewrite::STORE_ID => $storeId
+ ]
+ );
+
+ $newUrls = $this->categoryUrlRewriteGenerator->generate($category);
+
+ try {
+ $newUrls = $this->filterEmptyRequestPaths($newUrls);
+ $this->urlPersist->replace($newUrls);
+ $counter += count($newUrls);
+ } catch (Exception $e) {
+ $output->writeln(
+ sprintf(
+ 'Duplicated url for store ID %d, category %d (%s) - %s Generated URLs:' .
+ PHP_EOL . '%s' . PHP_EOL,
+ $storeId,
+ $category->getId(),
+ $category->getName(),
+ $e->getMessage(),
+ implode(PHP_EOL, array_keys($newUrls))
+ )
+ );
+ }
+ }
+
+ $this->emulation->stopEnvironmentEmulation();
+ }
+ $output->writeln(
+ sprintf('Done regenerating. Regenerated %d urls', $counter)
+ );
+
+ return Cli::RETURN_SUCCESS;
+ }
+
+ /**
+ * Remove entries with request_path='' to prevent error 404 for "http://site.com/" address.
+ *
+ * @param UrlRewrite[] $newUrls
+ *
+ * @return UrlRewrite[]
+ */
+ private function filterEmptyRequestPaths(array $newUrls): array
+ {
+ $result = [];
+
+ foreach ($newUrls as $key => $url) {
+ $requestPath = $url->getRequestPath();
+
+ if (!empty($requestPath)) {
+ $result[$key] = $url;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Console/Command/RegenerateCmsPageUrlCommand.php b/src/Console/Command/RegenerateCmsPageUrlCommand.php
new file mode 100644
index 0000000..b37b947
--- /dev/null
+++ b/src/Console/Command/RegenerateCmsPageUrlCommand.php
@@ -0,0 +1,159 @@
+pageCollectionFactory = $pageCollectionFactory;
+ $this->urlPersist = $urlPersist;
+ $this->cmsPageUrlRewriteGenerator = $cmsPageUrlRewriteGenerator;
+ $this->emulation = $emulation;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function configure()
+ {
+ $this->setName('regenerate:cms-page:url')
+ ->setDescription('Regenerate url for cms pages.')
+ ->addArgument(
+ 'pids',
+ InputArgument::IS_ARRAY,
+ 'CMS Pages to regenerate'
+ );
+
+ parent::configure();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ $output->writeln('Start regenerating urls for CMS pages.');
+
+ try {
+ $this->state->getAreaCode();
+ } catch (LocalizedException $e) {
+ $this->state->setAreaCode(Area::AREA_ADMINHTML);
+ }
+
+ $counter = 0;
+
+ $stores = $this->getChosenStores();
+
+ foreach ($stores as $storeId) {
+ $this->emulation->startEnvironmentEmulation($storeId, Area::AREA_FRONTEND, true);
+
+ $pages = $this->pageCollectionFactory->create();
+
+ $pages->addStoreFilter($storeId);
+
+ if (count($input->getArgument('pids')) > 0) {
+ $pageIds = $input->getArgument('pids');
+ } else {
+ $pageIds = $pages->getAllIds();
+ }
+ $pageIds = array_unique($pageIds);
+ $pages->addFieldToFilter('page_id', ['in' => $pageIds]);
+
+ /** @var Page $page */
+ foreach ($pages as $page) {
+ $newUrls = $this->cmsPageUrlRewriteGenerator->generate($page);
+
+ try {
+ $this->urlPersist->replace($newUrls);
+ $counter += count($newUrls);
+ } catch (UrlAlreadyExistsException $e) {
+ $output->writeln(
+ sprintf(
+ 'Url for page %s (%d) already exists.' . PHP_EOL . '%s',
+ $page->getTitle(),
+ $page->getId(),
+ $e->getMessage()
+ )
+ );
+ } catch (Exception $e) {
+ $output->writeln(
+ 'Couldn\'t replace url for %s (%d)' . PHP_EOL . '%s'
+ );
+ }
+ }
+
+ $this->emulation->stopEnvironmentEmulation();
+ $output->writeln(
+ sprintf(
+ 'Finished regenerating. Regenerated %d urls.',
+ $counter
+ )
+ );
+ }
+
+ return Cli::RETURN_SUCCESS;
+ }
+}
diff --git a/src/Console/Command/RegenerateProductUrlCommand.php b/src/Console/Command/RegenerateProductUrlCommand.php
new file mode 100644
index 0000000..755b26e
--- /dev/null
+++ b/src/Console/Command/RegenerateProductUrlCommand.php
@@ -0,0 +1,55 @@
+setName('regenerate:product:url')
+ ->setDescription('Regenerate url for given products')
+ ->addArgument(
+ 'pids',
+ InputArgument::IS_ARRAY,
+ 'Product IDs to regenerate'
+ );
+
+ parent::configure();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ try {
+ $this->state->getAreaCode();
+ } catch (LocalizedException $e) {
+ $this->state->setAreaCode('adminhtml');
+ }
+
+ $this->regenerateProductUrl->setOutput($output);
+
+ $stores = $this->getChosenStores();
+
+ foreach ($stores as $storeId) {
+ $this->regenerateProductUrl->execute($input->getArgument('pids'), (int)$storeId, $output->isVerbose());
+ }
+
+ return Cli::RETURN_SUCCESS;
+ }
+}
diff --git a/src/Controller/Adminhtml/Action/Product.php b/src/Controller/Adminhtml/Action/Product.php
new file mode 100644
index 0000000..a0a4d7b
--- /dev/null
+++ b/src/Controller/Adminhtml/Action/Product.php
@@ -0,0 +1,105 @@
+collectionFactory = $collectionFactory;
+ $this->filter = $filter;
+ $this->regenerateProductUrl = $regenerateProductUrl;
+
+ parent::__construct($context);
+ }
+
+ /**
+ * Execute action based on request and return result
+ *
+ * Note: Request will be added as operation argument in future
+ *
+ * @return Redirect
+ * @throws LocalizedException
+ */
+ public function execute(): Redirect
+ {
+ $productIds = $this->getSelectedProductIds();
+ $storeId = (int)$this->getRequest()->getParam('store', 0);
+ $filters = $this->getRequest()->getParam('filters', []);
+
+ if (isset($filters['store_id'])) {
+ $storeId = (int)$filters['store_id'];
+ }
+
+ try {
+ $this->regenerateProductUrl->execute($productIds, $storeId);
+ $this->messageManager->addSuccessMessage(
+ __(
+ 'Successfully regenerated %1 urls for store id %2.',
+ $this->regenerateProductUrl->getRegeneratedCount(),
+ $storeId
+ )
+ );
+ } catch (Exception $e) {
+ $this->messageManager->addExceptionMessage(
+ $e,
+ __('Something went wrong while regenerating the product(s) url.')
+ );
+ }
+
+ $resultRedirect = $this->resultRedirectFactory->create();
+
+ return $resultRedirect->setPath('catalog/product/index');
+ }
+
+ /**
+ * Get selected Product IDs
+ *
+ * @return array
+ * @throws LocalizedException
+ */
+ private function getSelectedProductIds(): array
+ {
+ return $this->filter->getCollection(
+ $this->collectionFactory->create()
+ )->getAllIds();
+ }
+}
diff --git a/src/Model/CategoryUrlPathGenerator.php b/src/Model/CategoryUrlPathGenerator.php
new file mode 100644
index 0000000..e9659ff
--- /dev/null
+++ b/src/Model/CategoryUrlPathGenerator.php
@@ -0,0 +1,28 @@
+isObjectNew() ||
+ $category->getLevel() >= self::MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING;
+ }
+}
diff --git a/src/Service/RegenerateProductUrl.php b/src/Service/RegenerateProductUrl.php
new file mode 100644
index 0000000..c7f2d17
--- /dev/null
+++ b/src/Service/RegenerateProductUrl.php
@@ -0,0 +1,271 @@
+collectionFactory = $collectionFactory;
+ $this->urlRewriteGenerator = $urlRewriteGenerator;
+ $this->urlPersist = $urlPersist;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Process
+ *
+ * @param int[]|null $productIds
+ * @param int|null $storeId
+ * @param bool $verbose
+ *
+ * @return void
+ * @throws NoSuchEntityException
+ */
+ public function execute(?array $productIds = null, ?int $storeId = null, bool $verbose = false): void
+ {
+ $this->isVerboseMode = $verbose;
+ $this->regeneratedCount = 0;
+
+ $stores = null !== $storeId
+ ? [$this->storeManager->getStore($storeId)]
+ : $this->storeManager->getStores();
+
+ foreach ($stores as $store) {
+ $regeneratedForStore = 0;
+
+ $this->log(sprintf('Start regenerating for store %s (%d)', $store->getName(), $store->getId()));
+
+ $collection = $this->collectionFactory->create();
+ $collection
+ ->setStoreId($store->getId())
+ ->addStoreFilter($store->getId())
+ ->addAttributeToSelect('name')
+ ->addFieldToFilter('status', ['eq' => Status::STATUS_ENABLED])
+ ->addFieldToFilter('visibility', ['gt' => Visibility::VISIBILITY_NOT_VISIBLE]);
+
+ if ($productIds == null || (is_array($productIds) && empty($productIds))) {
+ $productIds = $collection->getAllIds();
+ }
+ $collection->addIdFilter($productIds);
+
+ $collection->addAttributeToSelect(['url_path', 'url_key']);
+
+ $deleteProducts = [];
+
+ /** @var Product $product */
+ foreach ($collection as $product) {
+ $deleteProducts[] = $product->getId();
+ if (count($deleteProducts) >= self::BATCH_SIZE) {
+ $this->deleteUrls($deleteProducts, $store);
+ $deleteProducts = [];
+ }
+ }
+
+ if (count($deleteProducts)) {
+ $this->deleteUrls($deleteProducts, $store, true);
+ $deleteProducts = [];
+ }
+
+ $newUrls = [];
+ /** @var Product $product */
+ foreach ($collection as $product) {
+ if ($this->isVerboseMode) {
+ $this->log(
+ sprintf(
+ 'Regenerating urls for %s (%s) in store (%s)',
+ $product->getSku(),
+ $product->getId(),
+ $store->getName()
+ )
+ );
+ }
+
+ $product->setStoreId($store->getId());
+
+ //phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge
+ $newUrls = array_merge($newUrls, $this->urlRewriteGenerator->generate($product));
+ if (count($newUrls) >= self::BATCH_SIZE) {
+ $regeneratedForStore += $this->replaceUrls($newUrls);
+ }
+ }
+
+ if (count($newUrls)) {
+ $regeneratedForStore += $this->replaceUrls($newUrls, true);
+ }
+
+ $this->log(
+ sprintf(
+ 'Done regenerating. Regenerated %d urls for store %s (%d)',
+ $regeneratedForStore,
+ $store->getName(),
+ $store->getId()
+ )
+ );
+ $this->regeneratedCount += $regeneratedForStore;
+ }
+ }
+
+ /**
+ * Generate output
+ *
+ * @param OutputInterface $output
+ *
+ * @return void
+ */
+ public function setOutput(OutputInterface $output): void
+ {
+ $this->output = $output;
+ }
+
+ /**
+ * Generate count
+ *
+ * @return int
+ */
+ public function getRegeneratedCount(): int
+ {
+ return $this->regeneratedCount;
+ }
+
+ /**
+ * Log output
+ *
+ * @param string $message
+ *
+ * @return void
+ */
+ private function log(string $message): void
+ {
+ if ($this->output !== null) {
+ $this->output->writeln($message);
+ }
+ }
+
+ /**
+ * Replace urls
+ *
+ * @param array $urls
+ * @param bool $last
+ *
+ * @return int
+ */
+ private function replaceUrls(array &$urls, bool $last = false): int
+ {
+ $this->log(sprintf('replaceUrls%s batch: %d', $last ? ' last' : '', count($urls)));
+
+ if ($this->isVerboseMode) {
+ foreach ($urls as $url) {
+ try {
+ $this->urlPersist->replace([$url]);
+ } catch (Exception $e) {
+ $this->log(
+ sprintf(
+ $e->getMessage() . ' Entity id: %d Request path: %s',
+ $url->getEntityId(),
+ $url->getRequestPath()
+ )
+ );
+ }
+ }
+ } else {
+ try {
+ $this->urlPersist->replace($urls);
+ } catch (Exception $e) {//phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch
+ }
+ }
+
+ $count = count($urls);
+ $urls = [];
+
+ return $count;
+ }
+
+ /**
+ * Remove old product urls
+ *
+ * @param array $productIds
+ * @param StoreInterface $store
+ * @param bool $last
+ *
+ * @return void
+ */
+ private function deleteUrls(array $productIds, StoreInterface $store, bool $last = false): void
+ {
+ $this->log(sprintf('deleteUrls%s batch: %d', $last ? ' last' : '', count($productIds)));
+ $this->urlPersist->deleteByData([
+ UrlRewrite::ENTITY_ID => $productIds,
+ UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE,
+ UrlRewrite::REDIRECT_TYPE => 0,
+ UrlRewrite::STORE_ID => $store->getId()
+ ]);
+ }
+}
diff --git a/src/etc/adminhtml/routes.xml b/src/etc/adminhtml/routes.xml
new file mode 100644
index 0000000..dbd13a9
--- /dev/null
+++ b/src/etc/adminhtml/routes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/src/etc/di.xml b/src/etc/di.xml
new file mode 100644
index 0000000..cfe69e6
--- /dev/null
+++ b/src/etc/di.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ - Elgentos\RegenerateCatalogUrls\Console\Command\RegenerateProductUrlCommand
+ - Elgentos\RegenerateCatalogUrls\Console\Command\RegenerateCategoryUrlCommand
+ - Elgentos\RegenerateCatalogUrls\Console\Command\RegenerateCategoryPathCommand
+ - Elgentos\RegenerateCatalogUrls\Console\Command\RegenerateCmsPageUrlCommand
+
+
+
+
+
+
+ Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator\Proxy
+
+
+
+
+ Magento\Store\Model\App\Emulation\Proxy
+
+
+
+
+ Magento\Store\Model\App\Emulation\Proxy
+
+
+
+
+ Magento\Store\Model\App\Emulation\Proxy
+
+
+
+
+
diff --git a/src/etc/events.xml b/src/etc/events.xml
new file mode 100644
index 0000000..d185c11
--- /dev/null
+++ b/src/etc/events.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/src/etc/module.xml b/src/etc/module.xml
new file mode 100644
index 0000000..0aef6c5
--- /dev/null
+++ b/src/etc/module.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/i18n/en_US.csv b/src/i18n/en_US.csv
new file mode 100644
index 0000000..554295d
--- /dev/null
+++ b/src/i18n/en_US.csv
@@ -0,0 +1,3 @@
+"Successfully regenerated %1 urls for store id %2.","Successfully regenerated %1 urls for store id %2."
+"Something went wrong while regenerating the product(s) url.","Something went wrong while regenerating the product(s) url."
+"Regenerate Url","Regenerate Url"
diff --git a/src/registration.php b/src/registration.php
new file mode 100644
index 0000000..8448d79
--- /dev/null
+++ b/src/registration.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ regenerate_url
+
+
+
+
+
+
+