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 + + + + + + +