├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug-report---.md │ └── feature_request.md ├── .gitignore ├── CHANGELOG.md ├── Console └── Command │ ├── RegenerateUrlRewrites.php │ └── RegenerateUrlRewritesAbstract.php ├── Helper └── Regenerate.php ├── LICENSE_AFL.txt ├── LICENSE_OSL.txt ├── Model ├── AbstractRegenerateRewrites.php ├── RegenerateCategoryRewrites.php └── RegenerateProductRewrites.php ├── README.md ├── composer.json ├── etc ├── config.xml ├── di.xml └── module.xml └── registration.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: olegkoval77 2 | patreon: olegkoval 3 | custom: ["https://www.paypal.com/donate/?hosted_button_id=995MLRKBNY9QQ"] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report ❗️ 3 | about: Create a issue report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: olegkoval 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear description of what you expected to happen. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature Request \U0001F4A1" 3 | about: Suggest a new idea for the project. 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Summary 11 | 12 | Brief explanation of the feature. 13 | 14 | ### Basic example 15 | 16 | If the proposal involves a new or changed API, include a basic code example. Omit this section if it's not applicable. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | **/.DS_Store 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # "Regenerate Url Rewrites" Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [1.7.1] - 2025-05-30 8 | ### Changed 9 | - adapted for compatibility with PHP 8.4 (deprecated implicitly nullable types) 10 | 11 | ## [1.7.0] - 2025-04-15 12 | ### Changed 13 | - adapted for compatibility with Magento 2.4.7-p4 14 | - adapted the composer.json file to be compatible with Composer 2 15 | 16 | ## [1.6.2] - 2023-10-10 17 | ### Changed 18 | - fixed Symfony Command constant issue 19 | - exclude non visible products from url regeneration 20 | 21 | ## [1.6.1] - 2023-08-24 22 | ### Changed 23 | - fixed compatibility with Symfony Console 5 and Magento 2.4.6 24 | - updated contact email to Gmail email (my own domain olegkoval.com was stolen) 25 | 26 | ## [1.6.0] - 2021-01-27 27 | ### Changed 28 | - adapted to Magento 2.3.5 29 | - fixed incorrect generation when URL suffix is slash 30 | 31 | ## [1.5.6] - 2020-04-13 32 | ### Changed 33 | - updated logic of "cleaning" of Url Rewrites and duplications check 34 | 35 | ## [1.5.5] - 2020-04-02 36 | ### Changed 37 | - updated logic of Url Rewrite regeneration via category entity 38 | - fixed compilation issue in helper 39 | 40 | ## [1.5.4] - 2020-03-21 41 | ### Changed 42 | - fixed issue of non-empty/non-false "request_path" of product entity. 43 | - modified logic of Url Rewrite db table updates 44 | 45 | ## [1.5.3] - 2020-03-20 46 | ### Changed 47 | - updated Url Rewrite preparing function 48 | - updated logic of Url Rewrite regeneration via category entity 49 | - updated save logic 50 | 51 | ## [1.5.2] - 2020-03-18 52 | ### Changed 53 | - updated logic of Url Rewrite regeneration via category entity 54 | - CLI options logic optimized (for category entity) 55 | 56 | ## [1.5.1] - 2020-03-08 57 | ### Changed 58 | - fixed issue of url_key and url rewrites regeneration based on product name value 59 | 60 | ## [1.5.0] - 2020-02-26 61 | ### Changed 62 | - revised and restructured code 63 | - modified functional logic of extension 64 | - removed option "--check-use-category-in-product-url" 65 | 66 | ## [1.4.3] - 2019-05-12 67 | ### Added 68 | - new option "no-regen-url-key" 69 | 70 | ### Changed 71 | - fixed a "typo" issue 72 | 73 | ## [1.4.2] - 2019-04-04 74 | ### Added 75 | - new option "--check-use-category-in-product-url" 76 | - info into log about conflicted URL Rewrites 77 | 78 | ### Changed 79 | - fixed logical issues in url_key regeneration 80 | - a fix for category/products rewrites for multistore 81 | - fixed issue of division by zero in progress bar 82 | - update the url_key regeneration behavior to use UrlPathGenerators 83 | - modified logic of displaying console messages (notifications, errors, exceptions...) 84 | 85 | ## [1.4.1] - 2019-02-20 86 | ### Changed 87 | - fixed the issue of removing previously added URL rewrites of product when the same URL key exists; 88 | - modified progress bar 89 | 90 | ## [1.4.0] - 2019-02-11 91 | ### Added 92 | - new option "--entity-type" 93 | - new option "--products-range" 94 | - new option "--product-id" 95 | - new option "--category-range" 96 | - new option "--category-id" 97 | 98 | ### Changed 99 | - revised and restructured code 100 | - modified logic of url rewrites regeneration 101 | - removed "--clean-url-key" 102 | 103 | ## [1.3.1] - 2018-11-14 104 | ### Changed 105 | - fixed issue of empty product URL keys 106 | - fixed double slashes issue 107 | - update category attributes via resource saveAttribute() 108 | - use proxy for CategoryUrlPathGenerator 109 | 110 | ## [1.3.0] - 2018-10-29 111 | ### Added 112 | - new option "--no-cache-clean" 113 | - new option "--no-cache-flush" 114 | - new option "--no-progress" 115 | - new option "--no-clean-url-key" 116 | 117 | ### Changed 118 | - optimized code 119 | - modified logic of url rewrites regeneration 120 | - fixed issue of store filter in a category collection 121 | 122 | ## [1.2.3] - 2018-10-03 123 | ### Added 124 | - display additional debug information for "URL key for specified store already exists" error 125 | 126 | ### Changed 127 | - modified logic of url rewrites regeneration 128 | 129 | ## [1.2.2] - 2018-10-02 130 | ### Changed 131 | - fixed setStoreId() on null error 132 | 133 | ## [1.2.1] - 2018-09-25 134 | ### Changed 135 | - fixed compilation issues 136 | 137 | ## [1.2.0] - 2018-09-25 138 | ### Changed 139 | - added proxies to CLI commands 140 | - modified logic of url rewrites regeneration 141 | - updated a composer file 142 | - fixed issue of a compatibility with new Magento Commerce versions 143 | 144 | ## [1.1.1] - 2018-09-10 145 | ### Changed 146 | - fix composer file format issue 147 | 148 | ## [1.1.0] - 2018-09-09 149 | ### Added 150 | - added feature to add a Pro features through a "Layer" class 151 | 152 | ### Changed 153 | - fix issue when optional arguments require value 154 | - updated a code structure 155 | 156 | ## [1.0.6] - 2018-07-26 157 | ### Added 158 | - new option to run URL rewrite generation without running full reindex 159 | 160 | ### Changed 161 | - update help notice to show INPUT_KEY_SAVE_REWRITES_HISTORY and INPUT_KEY_NO_REINDEX 162 | 163 | ## [1.0.5] - 2018-05-13 164 | ### Added 165 | - new option to save current URL rewrites 166 | 167 | ### Changed 168 | - improve the store ID arguments workflow 169 | 170 | ## [1.0.4] - 2017-11-13 171 | ### Added 172 | - additional checks of storeId argument 173 | 174 | ## [1.0.3] - 2017-10-25 175 | ### Added 176 | - check if area code is set 177 | 178 | ## [1.0.2] - 2017-10-20 179 | ### Fixed 180 | - fix "Area code not set" issue 181 | 182 | ## [1.0.1] - 2017-10-10 183 | ### Fixed 184 | - fix store id issue in collection filter 185 | 186 | ## [1.0.0] - 2017-09-29 187 | Release of Magento 2 "Regenerate Url Rewrites" extension 188 | -------------------------------------------------------------------------------- /Console/Command/RegenerateUrlRewrites.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | namespace OlegKoval\RegenerateUrlRewrites\Console\Command; 12 | 13 | use Magento\Framework\Exception\LocalizedException; 14 | use Symfony\Component\Console\Command\Command; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | class RegenerateUrlRewrites extends RegenerateUrlRewritesAbstract 21 | { 22 | /** 23 | * @var null|InputInterface 24 | */ 25 | protected ?InputInterface $_input = null; 26 | 27 | /** 28 | * @var null|OutputInterface 29 | */ 30 | protected ?OutputInterface $_output = null; 31 | 32 | /** 33 | * @return void 34 | */ 35 | protected function configure(): void 36 | { 37 | $this->setName('ok:urlrewrites:regenerate') 38 | ->setDescription('Regenerate Url Rewrites of products and categories') 39 | ->setDefinition([ 40 | new InputOption( 41 | self::INPUT_KEY_STORE_ID, 42 | null, 43 | InputArgument::OPTIONAL, 44 | 'Specific store id' 45 | ), 46 | new InputOption( 47 | self::INPUT_KEY_REGENERATE_ENTITY_TYPE, 48 | null, 49 | InputArgument::OPTIONAL, 50 | 'Entity type which URLs regenerate: product or category. Default is "product".' 51 | ), 52 | new InputOption( 53 | self::INPUT_KEY_SAVE_REWRITES_HISTORY, 54 | null, 55 | InputOption::VALUE_NONE, 56 | 'Save current URL Rewrites' 57 | ), 58 | new InputOption( 59 | self::INPUT_KEY_NO_REINDEX, 60 | null, 61 | InputOption::VALUE_NONE, 62 | 'Do not run reindex when URL rewrites are generated.' 63 | ), 64 | new InputOption( 65 | self::INPUT_KEY_NO_PROGRESS, 66 | null, 67 | InputOption::VALUE_NONE, 68 | 'Do not show progress indicator.' 69 | ), 70 | new InputOption( 71 | self::INPUT_KEY_NO_CACHE_FLUSH, 72 | null, 73 | InputOption::VALUE_NONE, 74 | 'Do not run cache:flush when URL rewrites are generated.' 75 | ), 76 | new InputOption( 77 | self::INPUT_KEY_NO_CACHE_CLEAN, 78 | null, 79 | InputOption::VALUE_NONE, 80 | 'Do not run cache:clean when URL rewrites are generated.' 81 | ), 82 | new InputOption( 83 | self::INPUT_KEY_CATEGORIES_RANGE, 84 | null, 85 | InputArgument::OPTIONAL, 86 | 'Categories ID range, e.g.: 15-40' 87 | ), 88 | new InputOption( 89 | self::INPUT_KEY_PRODUCTS_RANGE, 90 | null, 91 | InputArgument::OPTIONAL, 92 | 'Products ID range, e.g.: 101-152' 93 | ), 94 | new InputOption( 95 | self::INPUT_KEY_CATEGORY_ID, 96 | null, 97 | InputArgument::OPTIONAL, 98 | 'Specific category ID, e.g.: 123' 99 | ), 100 | new InputOption( 101 | self::INPUT_KEY_PRODUCT_ID, 102 | null, 103 | InputArgument::OPTIONAL, 104 | 'Specific product ID, e.g.: 107' 105 | ), 106 | new InputOption( 107 | self::INPUT_KEY_NO_REGEN_URL_KEY, 108 | null, 109 | InputOption::VALUE_NONE, 110 | 'Prevent url_key regeneration' 111 | ), 112 | ]); 113 | } 114 | 115 | /** 116 | * Regenerate Url Rewrites 117 | * @param InputInterface $input 118 | * @param OutputInterface $output 119 | * @return int 0 if everything went fine, or an exit code 120 | */ 121 | protected function execute(InputInterface $input, OutputInterface $output): int 122 | { 123 | set_time_limit(0); 124 | $this->_input = $input; 125 | $this->_output = $output; 126 | 127 | $this->_output->writeln('Regenerating of URL rewrites:'); 128 | $this->_showSupportMe(); 129 | $this->getCommandOptions(); 130 | 131 | if (count($this->_errors) > 0) { 132 | foreach ($this->_errors as $error) { 133 | $this->_addConsoleMsg($error); 134 | } 135 | return Command::FAILURE; 136 | } 137 | 138 | // set area code if needed 139 | try { 140 | $areaCode = $this->_appState->getAreaCode(); 141 | } catch (LocalizedException $e) { 142 | // if area code is not set then magento generate exception "LocalizedException" 143 | try { 144 | $this->_appState->setAreaCode('adminhtml'); 145 | } catch (LocalizedException $e) {} 146 | } 147 | 148 | foreach ($this->_commandOptions['storesList'] as $storeId => $storeCode) { 149 | $this->_output->writeln(''); 150 | $this->_output->writeln("[Type: {$this->_commandOptions['entityType']}, Store ID: {$storeId}, Store View code: {$storeCode}]:"); 151 | $this->_storeManager->setCurrentStore($storeId); 152 | 153 | if ($this->_commandOptions['entityType'] == self::INPUT_KEY_REGENERATE_ENTITY_TYPE_PRODUCT) { 154 | $this->regenerateProductRewrites->regenerateOptions = $this->_commandOptions; 155 | $this->regenerateProductRewrites->regenerate($storeId); 156 | } elseif ($this->_commandOptions['entityType'] == self::INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY) { 157 | $this->regenerateCategoryRewrites->regenerateOptions = $this->_commandOptions; 158 | $this->regenerateCategoryRewrites->regenerate($storeId); 159 | } 160 | } 161 | 162 | $this->_output->writeln(''); 163 | $this->_output->writeln(''); 164 | 165 | $this->_displayConsoleMsg(); 166 | 167 | $this->_runReindexation(); 168 | $this->_runClearCache(); 169 | 170 | $this->_showSupportMe(); 171 | $this->_output->writeln('Finished'); 172 | 173 | return Command::SUCCESS; 174 | } 175 | 176 | /** 177 | * Get command options 178 | * @return void 179 | */ 180 | public function getCommandOptions(): void 181 | { 182 | $options = $this->_input->getOptions(); 183 | $allStores = $this->_getAllStoreIds(); 184 | $distinctOptionsUsed = 0; 185 | 186 | if ( 187 | isset($options[self::INPUT_KEY_REGENERATE_ENTITY_TYPE]) 188 | && in_array( 189 | $options[self::INPUT_KEY_REGENERATE_ENTITY_TYPE], 190 | array(self::INPUT_KEY_REGENERATE_ENTITY_TYPE_PRODUCT, self::INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY) 191 | ) 192 | ) { 193 | $this->_commandOptions['entityType'] = $options[self::INPUT_KEY_REGENERATE_ENTITY_TYPE]; 194 | } 195 | 196 | if (isset($options[self::INPUT_KEY_SAVE_REWRITES_HISTORY]) && $options[self::INPUT_KEY_SAVE_REWRITES_HISTORY] === true) { 197 | $this->_commandOptions['saveOldUrls'] = true; 198 | } 199 | 200 | if (isset($options[self::INPUT_KEY_NO_REGEN_URL_KEY]) && $options[self::INPUT_KEY_NO_REGEN_URL_KEY] === true) { 201 | $this->_commandOptions['noRegenUrlKey'] = true; 202 | } 203 | 204 | if (isset($options[self::INPUT_KEY_NO_REINDEX]) && $options[self::INPUT_KEY_NO_REINDEX] === true) { 205 | $this->_commandOptions['runReindex'] = false; 206 | } 207 | 208 | if (isset($options[self::INPUT_KEY_NO_PROGRESS]) && $options[self::INPUT_KEY_NO_PROGRESS] === true) { 209 | $this->_commandOptions['showProgress'] = false; 210 | } 211 | 212 | if (isset($options[self::INPUT_KEY_NO_CACHE_CLEAN]) && $options[self::INPUT_KEY_NO_CACHE_CLEAN] === true) { 213 | $this->_commandOptions['runCacheClean'] = false; 214 | } 215 | 216 | if (isset($options[self::INPUT_KEY_NO_CACHE_FLUSH]) && $options[self::INPUT_KEY_NO_CACHE_FLUSH] === true) { 217 | $this->_commandOptions['runCacheFlush'] = false; 218 | } 219 | 220 | if (isset($options[self::INPUT_KEY_PRODUCTS_RANGE])) { 221 | if (!$this->helper->isRegisteredProVersion()) { 222 | $this->_addError($this->helper->getPurchaseProVersionMsg()); 223 | } 224 | 225 | $this->_commandOptions['productsFilter'] = $this->_generateIdsRangeArray( 226 | $options[self::INPUT_KEY_PRODUCTS_RANGE], 227 | 'product' 228 | ); 229 | $distinctOptionsUsed++; 230 | } 231 | 232 | if (isset($options[self::INPUT_KEY_PRODUCT_ID])) { 233 | if (!$this->helper->isRegisteredProVersion()) { 234 | $this->_addError($this->helper->getPurchaseProVersionMsg()); 235 | } 236 | 237 | $this->_commandOptions['productId'] = (int)$options[self::INPUT_KEY_PRODUCT_ID]; 238 | 239 | if ($this->_commandOptions['productId'] == 0) { 240 | $this->_errors[] = __('ERROR: product ID should be greater than 0.'); 241 | } else { 242 | $distinctOptionsUsed++; 243 | } 244 | } 245 | 246 | if (isset($options[self::INPUT_KEY_CATEGORIES_RANGE])) { 247 | if (!$this->helper->isRegisteredProVersion()) { 248 | $this->_addError($this->helper->getPurchaseProVersionMsg()); 249 | } 250 | 251 | $this->_commandOptions['categoriesFilter'] = $this->_generateIdsRangeArray( 252 | $options[self::INPUT_KEY_CATEGORIES_RANGE], 253 | 'category' 254 | ); 255 | $distinctOptionsUsed++; 256 | 257 | // if this option was used then for 100% user want to regenerate entity type "category" 258 | $this->_commandOptions['entityType'] = self::INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY; 259 | } 260 | 261 | if (isset($options[self::INPUT_KEY_CATEGORY_ID])) { 262 | if (!$this->helper->isRegisteredProVersion()) { 263 | $this->_addError($this->helper->getPurchaseProVersionMsg()); 264 | } 265 | 266 | $this->_commandOptions['categoryId'] = (int)$options[self::INPUT_KEY_CATEGORY_ID]; 267 | 268 | if ($this->_commandOptions['categoryId'] == 0) { 269 | $this->_errors[] = __('ERROR: category ID should be greater than 0.'); 270 | } else { 271 | $distinctOptionsUsed++; 272 | } 273 | 274 | // if this option was used then for 100% user want to regenerate entity type "category" 275 | $this->_commandOptions['entityType'] = self::INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY; 276 | } 277 | 278 | if ( 279 | $this->_commandOptions['entityType'] == self::INPUT_KEY_REGENERATE_ENTITY_TYPE_PRODUCT 280 | && ( 281 | count($this->_commandOptions['categoriesFilter']) > 0 282 | || (int) $this->_commandOptions['categoryId'] > 0 283 | ) 284 | ) { 285 | $this->_errors[] = $this->_getLogicalConflictError( 286 | self::INPUT_KEY_REGENERATE_ENTITY_TYPE_PRODUCT, 287 | self::INPUT_KEY_CATEGORIES_RANGE, 288 | self::INPUT_KEY_CATEGORY_ID 289 | ); 290 | } 291 | 292 | if ( 293 | $this->_commandOptions['entityType'] == self::INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY 294 | && ( 295 | count($this->_commandOptions['productsFilter']) > 0 296 | || (int) $this->_commandOptions['productId'] > 0 297 | ) 298 | ) { 299 | $this->_errors[] = $this->_getLogicalConflictError( 300 | self::INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY, 301 | self::INPUT_KEY_PRODUCTS_RANGE, 302 | self::INPUT_KEY_PRODUCT_ID 303 | ); 304 | } 305 | 306 | if ($distinctOptionsUsed > 1) { 307 | $this->_errors[] = __( 308 | "ERROR: you can use only one of the option (not together):\n'--%o1' or '--%o2' or '--%o3' or '--%o4'.", 309 | [ 310 | 'o1' => self::INPUT_KEY_CATEGORIES_RANGE, 311 | 'o2' => self::INPUT_KEY_PRODUCTS_RANGE, 312 | 'o3' => self::INPUT_KEY_CATEGORY_ID, 313 | 'o4' => self::INPUT_KEY_PRODUCT_ID 314 | ] 315 | ); 316 | } 317 | 318 | // get store ID (if was set) 319 | $storeId = $this->_input->getOption(self::INPUT_KEY_STORE_ID); 320 | 321 | // if store ID is not specified the re-generate for all stores 322 | if (is_null($storeId)) { 323 | $this->_commandOptions['storesList'] = $allStores; 324 | } 325 | // we will re-generate URL only in this specific store (if it exists) 326 | elseif (strlen($storeId) && ctype_digit($storeId)) { 327 | if (isset($allStores[$storeId])) { 328 | $this->_commandOptions['storesList'] = array( 329 | (int)$storeId => $allStores[$storeId] 330 | ); 331 | } else { 332 | $this->_errors[] = __('ERROR: store with this ID not exists.')->render(); 333 | } 334 | } 335 | // display error if user set some incorrect value 336 | else { 337 | $this->_errors[] = __('ERROR: store ID should have a integer value.')->render(); 338 | } 339 | } 340 | 341 | /** 342 | * Generate logical conflict error 343 | * 344 | * @param string $option1 345 | * @param string $option2 346 | * @param string $option3 347 | * @return string 348 | */ 349 | private function _getLogicalConflictError(string $option1, string $option2, string $option3): string 350 | { 351 | return __( 352 | "ERROR: you can not use this options together (logical conflict):\n'--%o1' with '--%o2'/'--%o3'", 353 | [ 354 | 'o1' => $option1, 355 | 'o2' => $option2, 356 | 'o3' => $option3 357 | ] 358 | )->render(); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /Console/Command/RegenerateUrlRewritesAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | namespace OlegKoval\RegenerateUrlRewrites\Console\Command; 12 | 13 | use Magento\Framework\Phrase; 14 | use Symfony\Component\Console\Command\Command; 15 | use Magento\Framework\App\ResourceConnection; 16 | use Magento\Framework\App\State as AppState; 17 | use Magento\Store\Model\StoreManagerInterface; 18 | use OlegKoval\RegenerateUrlRewrites\Helper\Regenerate as RegenerateHelper; 19 | use OlegKoval\RegenerateUrlRewrites\Model\RegenerateProductRewrites; 20 | use OlegKoval\RegenerateUrlRewrites\Model\RegenerateCategoryRewrites; 21 | 22 | abstract class RegenerateUrlRewritesAbstract extends Command 23 | { 24 | const INPUT_KEY_STORE_ID = 'store-id'; 25 | const INPUT_KEY_REGENERATE_ENTITY_TYPE = 'entity-type'; 26 | const INPUT_KEY_SAVE_REWRITES_HISTORY = 'save-old-urls'; 27 | const INPUT_KEY_NO_REGEN_URL_KEY = 'no-regen-url-key'; 28 | const INPUT_KEY_NO_REINDEX = 'no-reindex'; 29 | const INPUT_KEY_NO_PROGRESS = 'no-progress'; 30 | const INPUT_KEY_NO_CACHE_FLUSH = 'no-cache-flush'; 31 | const INPUT_KEY_NO_CACHE_CLEAN = 'no-cache-clean'; 32 | const INPUT_KEY_CATEGORIES_RANGE = 'categories-range'; 33 | const INPUT_KEY_PRODUCTS_RANGE = 'products-range'; 34 | const INPUT_KEY_CATEGORY_ID = 'category-id'; 35 | const INPUT_KEY_PRODUCT_ID = 'product-id'; 36 | const INPUT_KEY_REGENERATE_ENTITY_TYPE_PRODUCT = 'product'; 37 | const INPUT_KEY_REGENERATE_ENTITY_TYPE_CATEGORY = 'category'; 38 | 39 | /** 40 | * @var ResourceConnection 41 | */ 42 | protected $_resource; 43 | 44 | /** 45 | * @var AppState $appState 46 | */ 47 | protected $_appState; 48 | 49 | /** 50 | * @var StoreManagerInterface 51 | */ 52 | protected $_storeManager; 53 | 54 | /** 55 | * @var RegenerateHelper 56 | */ 57 | protected $helper; 58 | 59 | /** 60 | * @var RegenerateProductRewrites 61 | */ 62 | protected $regenerateProductRewrites; 63 | 64 | /** 65 | * @var RegenerateCategoryRewrites 66 | */ 67 | protected $regenerateCategoryRewrites; 68 | 69 | /** 70 | * @var array 71 | */ 72 | protected $_commandOptions = []; 73 | 74 | /** 75 | * @var array 76 | */ 77 | protected $_errors = []; 78 | 79 | /** 80 | * @var array 81 | */ 82 | protected $_consoleMsg = []; 83 | 84 | /** 85 | * RegenerateUrlRewritesAbstract constructor 86 | * 87 | * @param ResourceConnection $resource 88 | * @param AppState\Proxy $appState 89 | * @param StoreManagerInterface $storeManager 90 | * @param RegenerateHelper $helper 91 | * @param RegenerateCategoryRewrites $regenerateCategoryRewrites 92 | * @param RegenerateProductRewrites $regenerateProductRewrites 93 | */ 94 | public function __construct( 95 | ResourceConnection $resource, 96 | AppState\Proxy $appState, 97 | StoreManagerInterface $storeManager, 98 | RegenerateHelper $helper, 99 | RegenerateCategoryRewrites $regenerateCategoryRewrites, 100 | RegenerateProductRewrites $regenerateProductRewrites 101 | ) 102 | { 103 | parent::__construct(); 104 | 105 | $this->_resource = $resource; 106 | $this->_appState = $appState; 107 | $this->_storeManager = $storeManager; 108 | $this->helper = $helper; 109 | $this->regenerateCategoryRewrites = $regenerateCategoryRewrites; 110 | $this->regenerateProductRewrites = $regenerateProductRewrites; 111 | 112 | // set default config values 113 | $this->_commandOptions['entityType'] = 'product'; 114 | $this->_commandOptions['saveOldUrls'] = false; 115 | $this->_commandOptions['runReindex'] = true; 116 | $this->_commandOptions['storesList'] = []; 117 | $this->_commandOptions['showProgress'] = true; 118 | $this->_commandOptions['runCacheClean'] = true; 119 | $this->_commandOptions['runCacheFlush'] = true; 120 | $this->_commandOptions['categoriesFilter'] = []; 121 | $this->_commandOptions['productsFilter'] = []; 122 | $this->_commandOptions['categoryId'] = null; 123 | $this->_commandOptions['productId'] = null; 124 | $this->_commandOptions['noRegenUrlKey'] = false; 125 | } 126 | 127 | /** 128 | * Display a support/donate information 129 | * 130 | * @return void 131 | */ 132 | protected function _showSupportMe(): void 133 | { 134 | $text = $this->helper->getSupportMeText(); 135 | 136 | $this->_output->writeln(''); 137 | $this->_output->writeln('----------------------------------------------------'); 138 | foreach ($text as $line) { 139 | $this->_output->writeln($line); 140 | } 141 | $this->_output->writeln('----------------------------------------------------'); 142 | $this->_output->writeln(''); 143 | } 144 | 145 | /** 146 | * Get a list of all stores id/code 147 | * 148 | * @return array 149 | */ 150 | protected function _getAllStoreIds(): array 151 | { 152 | $result = []; 153 | 154 | $sql = $this->_resource->getConnection()->select() 155 | ->from($this->_resource->getTableName('store'), array('store_id', 'code')) 156 | ->order('store_id', 'ASC'); 157 | 158 | $queryResult = $this->_resource->getConnection()->fetchAll($sql); 159 | 160 | foreach ($queryResult as $row) { 161 | $result[(int)$row['store_id']] = $row['code']; 162 | } 163 | 164 | return $result; 165 | } 166 | 167 | /** 168 | * Generate range of ID's 169 | * 170 | * @param string $idsRange 171 | * @param string $type 172 | * @return array 173 | */ 174 | protected function _generateIdsRangeArray(string $idsRange, string $type = 'product'): array 175 | { 176 | $result = $tmpIds = []; 177 | 178 | list($start, $end) = array_map('intval', explode('-', $idsRange, 2)); 179 | 180 | if ($end < $start) $end = $start; 181 | 182 | for ($id = $start; $id <= $end; $id++) { 183 | $tmpIds[] = $id; 184 | } 185 | 186 | // get existed ID's from this range in entity DB table 187 | $tableName = $this->_resource->getTableName('catalog_' . $type . '_entity'); 188 | $ids = implode(', ', $tmpIds); 189 | $sql = "SELECT entity_id FROM {$tableName} WHERE entity_id IN ({$ids}) ORDER BY entity_id"; 190 | 191 | $queryResult = $this->_resource->getConnection()->fetchAll($sql); 192 | 193 | foreach ($queryResult as $row) { 194 | $result[] = (int)$row['entity_id']; 195 | } 196 | 197 | // if not entity_id in this range - show error 198 | if (count($result) == 0) { 199 | $this->_addError(__("ERROR: %type ID's in this range not exists", ['type' => ucfirst($type)])); 200 | } 201 | 202 | return $result; 203 | } 204 | 205 | /** 206 | * @param Phrase|string $error 207 | * @return void 208 | */ 209 | protected function _addError(Phrase|string $error): void 210 | { 211 | $this->_errors[] = $error; 212 | } 213 | 214 | /** 215 | * Collect console messages 216 | * 217 | * @param Phrase|string $msg 218 | * @return void 219 | */ 220 | protected function _addConsoleMsg(Phrase|string $msg): void 221 | { 222 | if ($msg instanceof Phrase) { 223 | $msg = $msg->render(); 224 | } 225 | 226 | $this->_consoleMsg[] = (string)$msg; 227 | } 228 | 229 | /** 230 | * Display all console messages 231 | * 232 | * @return void 233 | */ 234 | protected function _displayConsoleMsg(): void 235 | { 236 | if (count($this->_consoleMsg) > 0) { 237 | $this->_output->writeln('[CONSOLE MESSAGES]'); 238 | foreach ($this->_consoleMsg as $msg) { 239 | $this->_output->writeln($msg); 240 | } 241 | $this->_output->writeln('[END OF CONSOLE MESSAGES]'); 242 | $this->_output->writeln(''); 243 | $this->_output->writeln(''); 244 | } 245 | } 246 | 247 | /** 248 | * Run re-indexation 249 | * @return void 250 | */ 251 | protected function _runReindexation(): void 252 | { 253 | if ($this->_commandOptions['runReindex']) { 254 | $this->_output->write('Reindexation...'); 255 | shell_exec('php bin/magento indexer:reindex'); 256 | $this->_output->writeln(' Done'); 257 | } 258 | } 259 | 260 | /** 261 | * Clear cache 262 | * 263 | * @return void 264 | */ 265 | protected function _runClearCache(): void 266 | { 267 | if ($this->_commandOptions['runCacheClean'] || $this->_commandOptions['runCacheFlush']) { 268 | $this->_output->write('Cache refreshing...'); 269 | if ($this->_commandOptions['runCacheClean']) { 270 | shell_exec('php bin/magento cache:clean'); 271 | } 272 | if ($this->_commandOptions['runCacheFlush']) { 273 | shell_exec('php bin/magento cache:flush'); 274 | } 275 | $this->_output->writeln(' Done'); 276 | $this->_output->writeln('If you use some external cache mechanisms (e.g.: Redis, Varnish, etc.) - please, refresh this external cache.'); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /Helper/Regenerate.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | namespace OlegKoval\RegenerateUrlRewrites\Helper; 12 | 13 | use Magento\Framework\App\Helper\AbstractHelper; 14 | use Magento\Framework\App\Helper\Context; 15 | use Magento\Store\Model\StoreManagerInterface; 16 | use Magento\Framework\App\Config\ScopeConfigInterface; 17 | 18 | class Regenerate extends AbstractHelper 19 | { 20 | /** 21 | * @var StoreManagerInterface 22 | */ 23 | protected $storeManager; 24 | 25 | /** 26 | * @var ScopeConfigInterface 27 | */ 28 | protected $scopeConfig; 29 | 30 | /** 31 | * Regenerate constructor. 32 | * @param Context $context 33 | * @param StoreManagerInterface $storeManager 34 | */ 35 | public function __construct( 36 | Context $context, 37 | StoreManagerInterface $storeManager 38 | ) 39 | { 40 | parent::__construct($context); 41 | $this->storeManager = $storeManager; 42 | $this->scopeConfig = $context->getScopeConfig(); 43 | } 44 | 45 | /** 46 | * Return array with "support me" text 47 | * 48 | * @return array 49 | */ 50 | public function getSupportMeText(): array 51 | { 52 | return [ 53 | 'Please, support me on:', 54 | 'PayPal: olegkoval.ca@gmail.com', 55 | 'https://www.paypal.com/donate/?hosted_button_id=995MLRKBNY9QQ', 56 | 'https://www.patreon.com/olegkoval', 57 | 'https://ko-fi.com/olegkoval77', 58 | ]; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getPurchaseProVersionMsg(): string 65 | { 66 | return __('To use this option you should purchase a Pro version.')->render(); 67 | } 68 | 69 | /** 70 | * @return bool 71 | */ 72 | public function isRegisteredProVersion(): bool 73 | { 74 | return true; 75 | } 76 | 77 | /** 78 | * Get store manager 79 | * 80 | * @return StoreManagerInterface 81 | */ 82 | public function getStoreManager(): StoreManagerInterface 83 | { 84 | return $this->storeManager; 85 | } 86 | 87 | /** 88 | * Get config value of "Use Categories Path for Product URLs" config option 89 | * 90 | * @param int|null $storeId 91 | * @return boolean 92 | */ 93 | public function useCategoriesPathForProductUrls(?int $storeId = null): bool 94 | { 95 | return (bool)$this->scopeConfig->getValue( 96 | 'catalog/seo/product_use_categories', 97 | \Magento\Store\Model\ScopeInterface::SCOPE_STORES, 98 | $storeId 99 | ); 100 | } 101 | 102 | /** 103 | * Sanitize product URL rewrites 104 | * 105 | * @param array $productUrlRewrites 106 | * @return array 107 | */ 108 | public function sanitizeProductUrlRewrites(array $productUrlRewrites): array 109 | { 110 | $paths = []; 111 | foreach ($productUrlRewrites as $key => $urlRewrite) { 112 | $path = $this->_clearRequestPath($urlRewrite->getRequestPath()); 113 | if (!in_array($path, $paths)) { 114 | $productUrlRewrites[$key]->setRequestPath($path); 115 | $paths[] = $path; 116 | } else { 117 | unset($productUrlRewrites[$key]); 118 | } 119 | } 120 | 121 | return $productUrlRewrites; 122 | } 123 | 124 | /** 125 | * Clear request path 126 | * @param string $requestPath 127 | * @return string 128 | */ 129 | protected function _clearRequestPath(string $requestPath): string 130 | { 131 | return str_replace(['//', './'], ['/', '/'], ltrim(ltrim($requestPath, '/'), '.')); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /LICENSE_AFL.txt: -------------------------------------------------------------------------------- 1 | Academic Free License v.3.0 (AFL-3.0) 2 | 3 | This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: 4 | 5 | Licensed under the Academic Free License version 3.0 6 | 7 | 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: 8 | 1. to reproduce the Original Work in copies, either alone or as part of a collective work; 9 | 10 | 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; 11 | 12 | 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; 13 | 14 | 4. to perform the Original Work publicly; and 15 | 16 | 5. to display the Original Work publicly. 17 | 18 | 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 19 | 20 | 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 21 | 22 | 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 23 | 24 | 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 25 | 26 | 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 27 | 28 | 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 29 | 30 | 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 31 | 32 | 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 33 | 34 | 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 35 | 36 | 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 37 | 38 | 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 39 | 40 | 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 41 | 42 | 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 43 | 44 | 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 45 | 46 | 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. 47 | -------------------------------------------------------------------------------- /LICENSE_OSL.txt: -------------------------------------------------------------------------------- 1 | Open Software License v.3.0 (OSL-3.0) 2 | 3 | This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: 4 | 5 | Licensed under the Open Software License version 3.0 6 | 7 | 1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: 8 | a) to reproduce the Original Work in copies, either alone or as part of a collective work; 9 | 10 | b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; 11 | 12 | c) to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; 13 | 14 | d) to perform the Original Work publicly; and 15 | 16 | e) to display the Original Work publicly. 17 | 18 | 2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 19 | 20 | 3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 21 | 22 | 4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 23 | 24 | 5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 25 | 26 | 6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 27 | 28 | 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 29 | 30 | 8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 31 | 32 | 9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 33 | 34 | 10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 35 | 36 | 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 37 | 38 | 12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 39 | 40 | 13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 41 | 42 | 14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 43 | 44 | 15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 45 | 46 | 16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. 47 | -------------------------------------------------------------------------------- /Model/AbstractRegenerateRewrites.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | namespace OlegKoval\RegenerateUrlRewrites\Model; 12 | 13 | use OlegKoval\RegenerateUrlRewrites\Helper\Regenerate as RegenerateHelper; 14 | use Magento\Framework\App\ResourceConnection; 15 | use Magento\UrlRewrite\Model\Storage\DbStorage; 16 | use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product as ProductUrlRewriteResource; 17 | 18 | abstract class AbstractRegenerateRewrites 19 | { 20 | /** 21 | * @var string 22 | */ 23 | protected $entityType = 'product'; 24 | 25 | /** 26 | * @var array 27 | */ 28 | protected $storeRootCategoryId = []; 29 | 30 | /** 31 | * @var integer 32 | */ 33 | protected $progressBarProgress = 0; 34 | 35 | /** 36 | * @var integer 37 | */ 38 | protected $progressBarTotal = 0; 39 | 40 | /** 41 | * @var string 42 | */ 43 | protected $mainDbTable; 44 | 45 | /** 46 | * @var string 47 | */ 48 | protected $secondaryDbTable; 49 | 50 | /** 51 | * @var string 52 | */ 53 | protected $categoryProductsDbTable; 54 | 55 | /** 56 | * Regenerate Rewrites custom options 57 | * @var array 58 | */ 59 | public $regenerateOptions = []; 60 | 61 | /** 62 | * @var RegenerateHelper 63 | */ 64 | protected $helper; 65 | 66 | /** 67 | * @var ResourceConnection 68 | */ 69 | protected $resourceConnection; 70 | 71 | /** 72 | * RegenerateAbstract constructor 73 | * 74 | * @param RegenerateHelper $helper 75 | * @param ResourceConnection $resourceConnection 76 | */ 77 | public function __construct( 78 | RegenerateHelper $helper, 79 | ResourceConnection $resourceConnection 80 | ) 81 | { 82 | $this->helper = $helper; 83 | $this->resourceConnection = $resourceConnection; 84 | 85 | // set default regenerate options 86 | $this->regenerateOptions['saveOldUrls'] = false; 87 | $this->regenerateOptions['categoriesFilter'] = []; 88 | $this->regenerateOptions['productsFilter'] = []; 89 | $this->regenerateOptions['categoryId'] = null; 90 | $this->regenerateOptions['productId'] = null; 91 | $this->regenerateOptions['checkUseCategoryInProductUrl'] = false; 92 | $this->regenerateOptions['noRegenUrlKey'] = false; 93 | $this->regenerateOptions['showProgress'] = false; 94 | } 95 | 96 | /** 97 | * Regenerate Url Rewrites in specific store 98 | * @param int $storeId 99 | * @return mixed 100 | */ 101 | abstract function regenerate(int $storeId = 0); 102 | 103 | /** 104 | * Return resource connection 105 | * @return ResourceConnection 106 | */ 107 | protected function _getResourceConnection(): ResourceConnection 108 | { 109 | return $this->resourceConnection; 110 | } 111 | 112 | /** 113 | * Save Url Rewrites 114 | * 115 | * @param array $urlRewrites 116 | * @param array $entityData 117 | * @return $this 118 | */ 119 | public function saveUrlRewrites(array $urlRewrites, array $entityData = []): static 120 | { 121 | $data = $this->_prepareUrlRewrites($urlRewrites); 122 | 123 | if (!$this->regenerateOptions['saveOldUrls']) { 124 | if (empty($entityData) && !empty($data)) { 125 | $entityData = $data; 126 | } 127 | $this->_deleteCurrentRewrites($entityData); 128 | } 129 | 130 | $this->_getResourceConnection()->getConnection()->beginTransaction(); 131 | try { 132 | $this->_getResourceConnection()->getConnection()->insertOnDuplicate( 133 | $this->_getMainTableName(), 134 | $data, 135 | ['request_path', 'metadata'] 136 | ); 137 | $this->_getResourceConnection()->getConnection()->commit(); 138 | 139 | } catch (\Exception $e) { 140 | $this->_getResourceConnection()->getConnection()->rollBack(); 141 | } 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Show a progress bar in the console 148 | * 149 | * @param int $size 150 | */ 151 | protected function _showProgress(int $size = 70): void 152 | { 153 | if (!$this->regenerateOptions['showProgress']) { 154 | return; 155 | } 156 | 157 | // if we go over our bound, just ignore it 158 | if ($this->progressBarProgress > $this->progressBarTotal) { 159 | return; 160 | } 161 | 162 | $perc = $this->progressBarTotal ? (double)($this->progressBarProgress / $this->progressBarTotal) : 1; 163 | $bar = floor($perc * $size); 164 | 165 | $status_bar = "\r["; 166 | $status_bar .= str_repeat('=', $bar); 167 | if ($bar < $size) { 168 | $status_bar .= '>'; 169 | $status_bar .= str_repeat(' ', $size - $bar); 170 | } else { 171 | $status_bar .= '='; 172 | } 173 | 174 | $disp = number_format($perc * 100, 0); 175 | 176 | $status_bar .= "] {$disp}% {$this->progressBarProgress}/{$this->progressBarTotal}"; 177 | 178 | echo $status_bar; 179 | flush(); 180 | 181 | // when done, send a newline 182 | if ($this->progressBarProgress == $this->progressBarTotal) { 183 | echo "\r\n"; 184 | } 185 | } 186 | 187 | /** 188 | * @return string 189 | */ 190 | protected function _getMainTableName(): string 191 | { 192 | if (empty($this->mainDbTable)) { 193 | $this->mainDbTable = $this->_getResourceConnection()->getTableName(DbStorage::TABLE_NAME); 194 | } 195 | 196 | return $this->mainDbTable; 197 | } 198 | 199 | /** 200 | * @return string 201 | */ 202 | protected function _getSecondaryTableName(): string 203 | { 204 | if (empty($this->secondaryDbTable)) { 205 | $this->secondaryDbTable = $this->_getResourceConnection()->getTableName(ProductUrlRewriteResource::TABLE_NAME); 206 | } 207 | 208 | return $this->secondaryDbTable; 209 | } 210 | 211 | /** 212 | * @return string 213 | */ 214 | protected function _getCategoryProductsTableName(): string 215 | { 216 | if (empty($this->categoryProductsDbTable)) { 217 | $this->categoryProductsDbTable = $this->_getResourceConnection()->getTableName('catalog_category_product'); 218 | } 219 | 220 | return $this->categoryProductsDbTable; 221 | } 222 | 223 | /** 224 | * Delete current Url Rewrites 225 | * 226 | * @param array $entitiesData 227 | * @return $this 228 | */ 229 | protected function _deleteCurrentRewrites(array $entitiesData = []): static 230 | { 231 | if (!empty($entitiesData)) { 232 | $whereConditions = []; 233 | foreach ($entitiesData as $entityData) { 234 | $whereConditions[] = sprintf( 235 | '(entity_type = \'%s\' AND entity_id = %d AND store_id = %d)', 236 | $entityData['entity_type'], $entityData['entity_id'], $entityData['store_id'] 237 | ); 238 | } 239 | $whereConditions = array_unique($whereConditions); 240 | 241 | $this->_getResourceConnection()->getConnection()->beginTransaction(); 242 | try { 243 | $this->_getResourceConnection()->getConnection()->delete( 244 | $this->_getMainTableName(), 245 | implode(' OR ', $whereConditions) 246 | ); 247 | $this->_getResourceConnection()->getConnection()->commit(); 248 | 249 | } catch (\Exception $e) { 250 | $this->_getResourceConnection()->getConnection()->rollBack(); 251 | } 252 | } 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * Update "catalog_url_rewrite_product_category" table 259 | * 260 | * @return $this 261 | */ 262 | protected function _updateSecondaryTable(): static 263 | { 264 | $this->_getResourceConnection()->getConnection()->beginTransaction(); 265 | try { 266 | $this->_getResourceConnection()->getConnection()->delete( 267 | $this->_getSecondaryTableName(), 268 | "url_rewrite_id NOT IN (SELECT url_rewrite_id FROM {$this->_getMainTableName()})" 269 | ); 270 | $this->_getResourceConnection()->getConnection()->commit(); 271 | 272 | } catch (\Exception $e) { 273 | $this->_getResourceConnection()->getConnection()->rollBack(); 274 | } 275 | 276 | $select = $this->_getResourceConnection()->getConnection()->select() 277 | ->from( 278 | $this->_getMainTableName(), 279 | [ 280 | 'url_rewrite_id', 281 | 'category_id' => new \Zend_Db_Expr( 282 | 'SUBSTRING_INDEX(SUBSTRING_INDEX(' . $this->_getMainTableName() . '.metadata, \'"\', -2), \'"\', 1)' 283 | ), 284 | 'product_id' => 'entity_id' 285 | ] 286 | ) 287 | ->where('metadata LIKE \'{"category_id":"%"}\'') 288 | ->where("url_rewrite_id NOT IN (SELECT url_rewrite_id FROM {$this->_getSecondaryTableName()})"); 289 | $data = $this->_getResourceConnection()->getConnection()->fetchAll($select); 290 | 291 | if (!empty($data)) { 292 | // I'm using row-by-row inserts because some products/categories not exists in entity tables but Url Rewrites 293 | // for this entity still exists in url_rewrite DB table. 294 | // This is the issue of Magento EE (Data integrity/assurance of the accuracy and consistency of data), 295 | // and this extension was made to not fix this; I just avoid this issue 296 | foreach ($data as $row) { 297 | $this->_getResourceConnection()->getConnection()->beginTransaction(); 298 | try { 299 | $this->_getResourceConnection()->getConnection()->insertOnDuplicate( 300 | $this->_getSecondaryTableName(), 301 | $row, 302 | ['product_id'] 303 | ); 304 | $this->_getResourceConnection()->getConnection()->commit(); 305 | 306 | } catch (\Exception $e) { 307 | $this->_getResourceConnection()->getConnection()->rollBack(); 308 | } 309 | } 310 | } 311 | 312 | return $this; 313 | } 314 | 315 | /** 316 | * @param array $urlRewrites 317 | * @return array 318 | */ 319 | protected function _prepareUrlRewrites(array $urlRewrites): array 320 | { 321 | $result = []; 322 | foreach ($urlRewrites as $urlRewrite) { 323 | $rewrite = $urlRewrite->toArray(); 324 | 325 | // check if the same Url Rewrite already exists 326 | $originalRequestPath = trim($rewrite['request_path']); 327 | 328 | // skip empty Url Rewrites - I don't know how this possible, but it happens in Magento: 329 | // maybe someone did import product programmatically and product(s) name(s) are empty 330 | if (empty($originalRequestPath)) continue; 331 | 332 | // split generated Url Rewrite into parts 333 | $pathParts = pathinfo($originalRequestPath); 334 | 335 | // remove leading/trailing slashes and dots from parts 336 | $pathParts['dirname'] = trim($pathParts['dirname'], './'); 337 | $pathParts['filename'] = trim($pathParts['filename'], './'); 338 | 339 | // If the last symbol was slash - let's use it as url suffix 340 | $urlSuffix = substr($originalRequestPath, -1) === '/' ? '/' : ''; 341 | 342 | // re-set Url Rewrite with sanitized parts 343 | $rewrite['request_path'] = $this->_mergePartsIntoRewriteRequest($pathParts, '', $urlSuffix); 344 | 345 | // check if we have a duplicate (maybe exists product with the same name => same Url Rewrite) 346 | // if exists then add additional index to avoid a duplicates 347 | $index = 0; 348 | while ($this->_urlRewriteExists($rewrite)) { 349 | $index++; 350 | $rewrite['request_path'] = $this->_mergePartsIntoRewriteRequest($pathParts, (string)$index, $urlSuffix); 351 | } 352 | 353 | $result[] = $rewrite; 354 | } 355 | 356 | return $result; 357 | } 358 | 359 | /** 360 | * Check if Url Rewrite with the same request path exists 361 | * 362 | * @param array $rewrite 363 | * @return string 364 | */ 365 | protected function _urlRewriteExists(array $rewrite): string 366 | { 367 | $select = $this->_getResourceConnection()->getConnection()->select() 368 | ->from($this->_getMainTableName(), ['url_rewrite_id']) 369 | ->where('entity_type = ?', $rewrite['entity_type']) 370 | ->where('request_path = ?', $rewrite['request_path']) 371 | ->where('store_id = ?', $rewrite['store_id']) 372 | ->where('entity_id != ?', $rewrite['entity_id']); 373 | return $this->_getResourceConnection()->getConnection()->fetchOne($select); 374 | } 375 | 376 | /** 377 | * Merge Url Rewrite parts into one string 378 | * 379 | * @param array $pathParts 380 | * @param string $index 381 | * @param string $urlSuffix 382 | * @return string 383 | */ 384 | protected function _mergePartsIntoRewriteRequest(array $pathParts, string $index = '', string $urlSuffix = ''): string 385 | { 386 | return (!empty($pathParts['dirname']) ? $pathParts['dirname'] . '/' : '') . $pathParts['filename'] 387 | . (!empty($index) ? '-' . $index : '') 388 | . (!empty($pathParts['extension']) ? '.' . $pathParts['extension'] : '') 389 | . ($urlSuffix ?: ''); 390 | } 391 | 392 | /** 393 | * Get root category I'd of specific store 394 | * 395 | * @param $storeId 396 | * @return int|null 397 | */ 398 | protected function _getStoreRootCategoryId($storeId): ?int 399 | { 400 | if (empty($this->storeRootCategoryId[$storeId])) { 401 | $value = null; 402 | try { 403 | $store = $this->helper->getStoreManager()->getStore($storeId); 404 | if ($store) { 405 | $value = $store->getRootCategoryId(); 406 | } 407 | } catch (\Exception $e) { 408 | } 409 | 410 | $this->storeRootCategoryId[$storeId] = $value; 411 | } 412 | 413 | return $this->storeRootCategoryId[$storeId]; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /Model/RegenerateCategoryRewrites.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | namespace OlegKoval\RegenerateUrlRewrites\Model; 12 | 13 | use Magento\Catalog\Model\ResourceModel\Category\Collection; 14 | use Magento\Framework\Exception\LocalizedException; 15 | use OlegKoval\RegenerateUrlRewrites\Helper\Regenerate as RegenerateHelper; 16 | use Magento\Framework\App\ResourceConnection; 17 | use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; 18 | use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; 19 | use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; 20 | use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; 21 | use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGeneratorFactory; 22 | use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; 23 | use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; 24 | use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; 25 | use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandlerFactory; 26 | use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; 27 | 28 | class RegenerateCategoryRewrites extends AbstractRegenerateRewrites 29 | { 30 | /** 31 | * @var string 32 | */ 33 | protected $entityType = 'category'; 34 | 35 | /** 36 | * @var int 37 | */ 38 | protected $categoriesCollectionPageSize = 100; 39 | 40 | /** 41 | * @var array 42 | */ 43 | protected $dataUrlRewriteClassNames = []; 44 | 45 | /** 46 | * @var DatabaseMapPool 47 | */ 48 | protected $databaseMapPool; 49 | 50 | /** 51 | * @var CategoryCollectionFactory 52 | */ 53 | protected $categoryCollectionFactory; 54 | 55 | /** 56 | * @var CategoryUrlPathGeneratorFactory 57 | */ 58 | protected $categoryUrlPathGeneratorFactory; 59 | 60 | /** 61 | * @var CategoryUrlPathGenerator 62 | */ 63 | protected $categoryUrlPathGenerator; 64 | 65 | /** 66 | * @var CategoryUrlRewriteGeneratorFactory 67 | */ 68 | protected $categoryUrlRewriteGeneratorFactory; 69 | 70 | /** 71 | * @var CategoryUrlRewriteGenerator 72 | */ 73 | protected $categoryUrlRewriteGenerator; 74 | 75 | /** 76 | * @var UrlRewriteHandlerFactory 77 | */ 78 | protected $urlRewriteHandlerFactory; 79 | 80 | /** 81 | * @var UrlRewriteHandler 82 | */ 83 | protected $urlRewriteHandler; 84 | 85 | /** 86 | * @var RegenerateProductRewrites 87 | */ 88 | protected $regenerateProductRewrites; 89 | 90 | /** 91 | * @param RegenerateHelper $helper 92 | * @param ResourceConnection $resourceConnection 93 | * @param CategoryCollectionFactory $categoryCollectionFactory 94 | * @param DatabaseMapPool\Proxy $databaseMapPool 95 | * @param CategoryUrlPathGeneratorFactory\Proxy $categoryUrlPathGeneratorFactory 96 | * @param CategoryUrlRewriteGeneratorFactory\Proxy $categoryUrlRewriteGeneratorFactory 97 | * @param UrlRewriteHandlerFactory\Proxy $urlRewriteHandlerFactory 98 | * @param RegenerateProductRewrites $regenerateProductRewrites 99 | */ 100 | public function __construct( 101 | RegenerateHelper $helper, 102 | ResourceConnection $resourceConnection, 103 | CategoryCollectionFactory $categoryCollectionFactory, 104 | DatabaseMapPool\Proxy $databaseMapPool, 105 | CategoryUrlPathGeneratorFactory\Proxy $categoryUrlPathGeneratorFactory, 106 | CategoryUrlRewriteGeneratorFactory\Proxy $categoryUrlRewriteGeneratorFactory, 107 | UrlRewriteHandlerFactory\Proxy $urlRewriteHandlerFactory, 108 | RegenerateProductRewrites $regenerateProductRewrites 109 | ) 110 | { 111 | parent::__construct($helper, $resourceConnection); 112 | 113 | $this->categoryCollectionFactory = $categoryCollectionFactory; 114 | $this->databaseMapPool = $databaseMapPool; 115 | $this->categoryUrlPathGeneratorFactory = $categoryUrlPathGeneratorFactory; 116 | $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; 117 | $this->urlRewriteHandlerFactory = $urlRewriteHandlerFactory; 118 | $this->regenerateProductRewrites = $regenerateProductRewrites; 119 | 120 | $this->dataUrlRewriteClassNames = [ 121 | DataCategoryUrlRewriteDatabaseMap::class, 122 | DataProductUrlRewriteDatabaseMap::class 123 | ]; 124 | } 125 | 126 | /** 127 | * Regenerate Categories and children (subcategories and related products) Url Rewrites in specific store 128 | * 129 | * @param int $storeId 130 | * @return $this 131 | */ 132 | public function regenerate(int $storeId = 0): static 133 | { 134 | if (count($this->regenerateOptions['categoriesFilter']) > 0) { 135 | $this->regenerateCategoriesRangeUrlRewrites( 136 | $this->regenerateOptions['categoriesFilter'], 137 | $storeId 138 | ); 139 | } elseif (!empty($this->regenerateOptions['categoryId'])) { 140 | $this->regenerateSpecificCategoryUrlRewrites( 141 | $this->regenerateOptions['categoryId'], 142 | $storeId 143 | ); 144 | } else { 145 | $this->regenerateAllCategoriesUrlRewrites($storeId); 146 | } 147 | return $this; 148 | } 149 | 150 | /** 151 | * Regenerate Url Rewrites of all categories 152 | * 153 | * @param int $storeId 154 | * @return $this 155 | */ 156 | public function regenerateAllCategoriesUrlRewrites(int $storeId = 0): static 157 | { 158 | $this->regenerateCategoriesRangeUrlRewrites([], $storeId); 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * Regenerate Url Rewrites of specific category 165 | * 166 | * @param int $categoryId 167 | * @param int $storeId 168 | * @return $this 169 | */ 170 | public function regenerateSpecificCategoryUrlRewrites(int $categoryId, int $storeId = 0): static 171 | { 172 | $this->regenerateCategoriesRangeUrlRewrites([$categoryId], $storeId); 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * Regenerate Url Rewrites of a category range 179 | * 180 | * @param array $categoriesFilter 181 | * @param int $storeId 182 | * @return $this 183 | */ 184 | public function regenerateCategoriesRangeUrlRewrites(array $categoriesFilter = [], int $storeId = 0): static 185 | { 186 | try { 187 | $categories = $this->_getCategoriesCollection($categoriesFilter, $storeId); 188 | 189 | $pageCount = $categories->getLastPageNumber(); 190 | $this->progressBarProgress = 0; 191 | $this->progressBarTotal = (int)$categories->getSize(); 192 | $currentPage = 1; 193 | 194 | $this->_showProgress(); 195 | while ($currentPage <= $pageCount) { 196 | $categories->clear(); 197 | $categories->setCurPage($currentPage); 198 | 199 | foreach ($categories as $category) { 200 | $this->categoryProcess($category, $storeId); 201 | $this->_showProgress(); 202 | } 203 | 204 | $currentPage++; 205 | } 206 | 207 | $this->_updateSecondaryTable(); 208 | } catch (LocalizedException $e) { 209 | // skip it 210 | } 211 | 212 | return $this; 213 | } 214 | 215 | /** 216 | * Process category Url Rewrites re-generation 217 | * 218 | * @param $category 219 | * @param int $storeId 220 | * @return $this 221 | */ 222 | protected function categoryProcess($category, int $storeId = 0): static 223 | { 224 | $category->setStoreId($storeId); 225 | 226 | if ($this->regenerateOptions['saveOldUrls']) { 227 | $category->setData('save_rewrites_history', true); 228 | } 229 | 230 | if (!$this->regenerateOptions['noRegenUrlKey']) { 231 | $category->setOrigData('url_key', null); 232 | $category->setUrlKey($this->_getCategoryUrlPathGenerator()->getUrlKey($category->setUrlKey(null))); 233 | $category->getResource()->saveAttribute($category, 'url_key'); 234 | } 235 | 236 | try { 237 | $urlPath = $this->_getCategoryUrlPathGenerator()->getUrlPath($category); 238 | } catch (LocalizedException $e) { 239 | $urlPath = null; 240 | } 241 | if (!empty($urlPath)) { 242 | $category->unsUrlPath(); 243 | $category->setUrlPath($urlPath); 244 | $category->getResource()->saveAttribute($category, 'url_path'); 245 | } 246 | 247 | $category->setChangedProductIds(true); 248 | 249 | try { 250 | $categoryUrlRewriteResult = $this->_getCategoryUrlRewriteGenerator()->generate($category, true); 251 | } catch (\Exception $e) { 252 | $categoryUrlRewriteResult = null; 253 | } 254 | if (!empty($categoryUrlRewriteResult)) { 255 | $this->saveUrlRewrites($categoryUrlRewriteResult); 256 | } 257 | 258 | // if config option "Use Category Path for Product URLs" is "Yes" then regenerate product urls 259 | if ($this->helper->useCategoriesPathForProductUrls($storeId)) { 260 | $productsIds = $this->_getCategoriesProductsIds($category->getAllChildren()); 261 | if (!empty($productsIds)) { 262 | $this->regenerateProductRewrites->regenerateOptions = $this->regenerateOptions; 263 | $this->regenerateProductRewrites->regenerateOptions['showProgress'] = false; 264 | $this->regenerateProductRewrites->regenerateProductsRangeUrlRewrites($productsIds, $storeId); 265 | } 266 | } 267 | 268 | //frees memory for maps that are self-initialized in multiple classes that were called by the generators 269 | $this->_resetUrlRewritesDataMaps($category); 270 | 271 | $this->progressBarProgress++; 272 | 273 | return $this; 274 | } 275 | 276 | /** 277 | * Get categories collection 278 | * 279 | * @param array $categoriesFilter 280 | * @param int $storeId 281 | * @return Collection 282 | * @throws LocalizedException 283 | */ 284 | protected function _getCategoriesCollection(array $categoriesFilter = [], int $storeId = 0): Collection 285 | { 286 | $categoriesCollection = $this->categoryCollectionFactory->create(); 287 | $categoriesCollection->addAttributeToSelect('name') 288 | ->addAttributeToSelect('url_key') 289 | ->addAttributeToSelect('url_path') 290 | ->setStoreId($storeId) 291 | // if we need to regenerate Url Rewrites for all categories, then we select only top level 292 | // and all subcategories (and products) will be regenerated as children 293 | ->addFieldToFilter('level', (count($categoriesFilter) > 0 ? ['gt' => '1'] : 2)) 294 | ->setOrder('level', 'ASC') 295 | // use limit to avoid an "eating" of a memory 296 | ->setPageSize($this->categoriesCollectionPageSize); 297 | 298 | $rootCategoryId = $this->_getStoreRootCategoryId($storeId); 299 | if ($rootCategoryId > 0) { 300 | // we use this filter instead of "->setStore()" - because "setStore()" is not working (another Magento issue) 301 | $categoriesCollection->addAttributeToFilter('path', array('like' => "1/{$rootCategoryId}/%")); 302 | } 303 | 304 | if (count($categoriesFilter) > 0) { 305 | $categoriesCollection->addIdFilter($categoriesFilter); 306 | } 307 | 308 | return $categoriesCollection; 309 | } 310 | 311 | /** 312 | * Get product Ids which are related to specific categories 313 | * 314 | * @param string $categoryIds 315 | * @return array 316 | */ 317 | protected function _getCategoriesProductsIds(string $categoryIds = ''): array 318 | { 319 | $result = []; 320 | 321 | if (!empty($categoryIds)) { 322 | $select = $this->_getResourceConnection()->getConnection()->select() 323 | ->from($this->_getCategoryProductsTableName(), ['product_id']) 324 | ->where("category_id IN ({$categoryIds})"); 325 | $rows = $this->_getResourceConnection()->getConnection()->fetchAll($select); 326 | 327 | foreach ($rows as $row) { 328 | $result[] = $row['product_id']; 329 | } 330 | } 331 | 332 | return $result; 333 | } 334 | 335 | /** 336 | * Get category Url Path generator 337 | * 338 | * @return CategoryUrlPathGenerator 339 | */ 340 | protected function _getCategoryUrlPathGenerator(): CategoryUrlPathGenerator 341 | { 342 | if (is_null($this->categoryUrlPathGenerator)) { 343 | $this->categoryUrlPathGenerator = $this->categoryUrlPathGeneratorFactory->create(); 344 | } 345 | 346 | return $this->categoryUrlPathGenerator; 347 | } 348 | 349 | /** 350 | * Get category Url Rewrite generator 351 | * 352 | * @return CategoryUrlRewriteGenerator 353 | */ 354 | protected function _getCategoryUrlRewriteGenerator(): CategoryUrlRewriteGenerator 355 | { 356 | if (is_null($this->categoryUrlRewriteGenerator)) { 357 | $this->categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); 358 | } 359 | 360 | return $this->categoryUrlRewriteGenerator; 361 | } 362 | 363 | /** 364 | * Get Url Rewrite handler 365 | * 366 | * @return UrlRewriteHandler 367 | */ 368 | protected function _getUrlRewriteHandler(): UrlRewriteHandler 369 | { 370 | if (is_null($this->urlRewriteHandler)) { 371 | $this->urlRewriteHandler = $this->urlRewriteHandlerFactory->create(); 372 | } 373 | 374 | return $this->urlRewriteHandler; 375 | } 376 | 377 | /** 378 | * Resets used data maps to free up memory and temporary tables 379 | * 380 | * @param $category 381 | * @return void 382 | */ 383 | protected function _resetUrlRewritesDataMaps($category): void 384 | { 385 | foreach ($this->dataUrlRewriteClassNames as $className) { 386 | $this->databaseMapPool->resetMap($className, $category->getEntityId()); 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /Model/RegenerateProductRewrites.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | namespace OlegKoval\RegenerateUrlRewrites\Model; 12 | 13 | use Magento\Catalog\Model\Product\Visibility; 14 | use Magento\Catalog\Model\ResourceModel\Product\Action; 15 | use Magento\Catalog\Model\ResourceModel\Product\Collection; 16 | use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; 17 | use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; 18 | use OlegKoval\RegenerateUrlRewrites\Helper\Regenerate as RegenerateHelper; 19 | use Magento\Framework\App\ResourceConnection; 20 | use Magento\Catalog\Model\ResourceModel\Product\ActionFactory as ProductActionFactory; 21 | use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGeneratorFactory; 22 | use Magento\CatalogUrlRewrite\Model\ProductUrlPathGeneratorFactory; 23 | use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; 24 | 25 | class RegenerateProductRewrites extends AbstractRegenerateRewrites 26 | { 27 | /** 28 | * @var string 29 | */ 30 | protected $entityType = 'product'; 31 | 32 | /** 33 | * @var int 34 | */ 35 | protected $productsCollectionPageSize = 1000; 36 | 37 | /** 38 | * @var ProductActionFactory 39 | */ 40 | protected $productActionFactory; 41 | 42 | /** 43 | * @var Action 44 | */ 45 | protected $productAction; 46 | 47 | /** 48 | * @var ProductUrlRewriteGeneratorFactory 49 | */ 50 | protected $productUrlRewriteGeneratorFactory; 51 | 52 | /** 53 | * @var ProductUrlRewriteGenerator 54 | */ 55 | protected $productUrlRewriteGenerator; 56 | 57 | /** 58 | * @var ProductUrlPathGeneratorFactory 59 | */ 60 | protected $productUrlPathGeneratorFactory; 61 | 62 | /** 63 | * @var ProductUrlPathGenerator 64 | */ 65 | protected $productUrlPathGenerator; 66 | 67 | /** 68 | * @var ProductCollectionFactoryy 69 | */ 70 | protected $productCollectionFactory; 71 | 72 | /** 73 | * RegenerateProductRewrites constructor. 74 | * 75 | * @param RegenerateHelper $helper 76 | * @param ResourceConnection $resourceConnection 77 | * @param ProductActionFactory $productActionFactory 78 | * @param ProductUrlRewriteGeneratorFactory\Proxy $productUrlRewriteGeneratorFactory 79 | * @param ProductUrlPathGeneratorFactory\Proxy $productUrlPathGeneratorFactory 80 | * @param ProductCollectionFactory $productCollectionFactory 81 | */ 82 | public function __construct( 83 | RegenerateHelper $helper, 84 | ResourceConnection $resourceConnection, 85 | ProductActionFactory $productActionFactory, 86 | ProductUrlRewriteGeneratorFactory\Proxy $productUrlRewriteGeneratorFactory, 87 | ProductUrlPathGeneratorFactory\Proxy $productUrlPathGeneratorFactory, 88 | ProductCollectionFactory $productCollectionFactory 89 | ) 90 | { 91 | parent::__construct($helper, $resourceConnection); 92 | 93 | $this->productActionFactory = $productActionFactory; 94 | $this->productUrlRewriteGeneratorFactory = $productUrlRewriteGeneratorFactory; 95 | $this->productUrlPathGeneratorFactory = $productUrlPathGeneratorFactory; 96 | $this->productCollectionFactory = $productCollectionFactory; 97 | } 98 | 99 | /** 100 | * Regenerate Products Url Rewrites in specific store 101 | * 102 | * @return $this 103 | */ 104 | public function regenerate(int $storeId = 0): static 105 | { 106 | if (count($this->regenerateOptions['productsFilter']) > 0) { 107 | $this->regenerateProductsRangeUrlRewrites( 108 | $this->regenerateOptions['productsFilter'], 109 | $storeId 110 | ); 111 | } elseif (!empty($this->regenerateOptions['productId'])) { 112 | $this->regenerateSpecificProductUrlRewrites( 113 | $this->regenerateOptions['productId'], 114 | $storeId 115 | ); 116 | } else { 117 | $this->regenerateAllProductsUrlRewrites($storeId); 118 | } 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * @param int $storeId 125 | * @return $this 126 | */ 127 | public function regenerateAllProductsUrlRewrites(int $storeId = 0): static 128 | { 129 | $this->regenerateProductsRangeUrlRewrites([], $storeId); 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Regenerate Url Rewrites for a specific product 136 | * 137 | * @param int $productId 138 | * @param int $storeId 139 | * @return $this 140 | */ 141 | public function regenerateSpecificProductUrlRewrites(int $productId, int $storeId = 0): static 142 | { 143 | $this->regenerateProductsRangeUrlRewrites([$productId], $storeId); 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Regenerate Url Rewrites for a product range 150 | * 151 | * @param array $productsFilter 152 | * @param int $storeId 153 | * @return $this 154 | */ 155 | public function regenerateProductsRangeUrlRewrites(array $productsFilter = [], int $storeId = 0): static 156 | { 157 | $products = $this->_getProductsCollection($productsFilter, $storeId); 158 | $pageCount = $products->getLastPageNumber(); 159 | $this->progressBarProgress = 1; 160 | $this->progressBarTotal = (int)$products->getSize(); 161 | $currentPage = 1; 162 | 163 | while ($currentPage <= $pageCount) { 164 | $products->clear(); 165 | $products->setCurPage($currentPage); 166 | 167 | foreach ($products as $product) { 168 | $this->_showProgress(); 169 | $this->processProduct($product, $storeId); 170 | } 171 | 172 | $currentPage++; 173 | } 174 | 175 | $this->_updateSecondaryTable(); 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * @param $entity 182 | * @param int $storeId 183 | * @return $this 184 | */ 185 | public function processProduct($entity, int $storeId = 0): static 186 | { 187 | $entity->setStoreId($storeId)->setData('url_path', null); 188 | 189 | if ($this->regenerateOptions['saveOldUrls']) { 190 | $entity->setData('save_rewrites_history', true); 191 | } 192 | 193 | // reset url_path to null, we need this to set a flag to use an Url Rewrites: 194 | // see logic in a core Product Url model: \Magento\Catalog\Model\Product\Url::getUrl() 195 | // if "request_path" is not null or equal to "false" then Magento do not search and do not use Url Rewrites 196 | $updateAttributes = ['url_path' => null]; 197 | if (!$this->regenerateOptions['noRegenUrlKey']) { 198 | $generatedKey = $this->_getProductUrlPathGenerator()->getUrlKey($entity->setUrlKey(null)); 199 | $updateAttributes['url_key'] = $generatedKey; 200 | } 201 | 202 | try { 203 | $this->_getProductAction()->updateAttributes( 204 | [$entity->getId()], 205 | $updateAttributes, 206 | $storeId 207 | ); 208 | 209 | $urlRewrites = $this->_getProductUrlRewriteGenerator()->generate($entity); 210 | $urlRewrites = $this->helper->sanitizeProductUrlRewrites($urlRewrites); 211 | 212 | if (!empty($urlRewrites)) { 213 | $this->saveUrlRewrites( 214 | $urlRewrites, 215 | [['entity_type' => $this->entityType, 'entity_id' => $entity->getId(), 'store_id' => $storeId]] 216 | ); 217 | } 218 | } catch (\Exception $e) { 219 | // go to the next product 220 | } 221 | 222 | $this->progressBarProgress++; 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * @return Action 229 | */ 230 | protected function _getProductAction(): Action 231 | { 232 | if (is_null($this->productAction)) { 233 | $this->productAction = $this->productActionFactory->create(); 234 | } 235 | 236 | return $this->productAction; 237 | } 238 | 239 | /** 240 | * @return ProductUrlRewriteGenerator 241 | */ 242 | protected function _getProductUrlRewriteGenerator(): ProductUrlRewriteGenerator 243 | { 244 | if (is_null($this->productUrlRewriteGenerator)) { 245 | $this->productUrlRewriteGenerator = $this->productUrlRewriteGeneratorFactory->create(); 246 | } 247 | 248 | return $this->productUrlRewriteGenerator; 249 | } 250 | 251 | /** 252 | * @return ProductUrlPathGenerator 253 | */ 254 | protected function _getProductUrlPathGenerator(): ProductUrlPathGenerator 255 | { 256 | if (is_null($this->productUrlPathGenerator)) { 257 | $this->productUrlPathGenerator = $this->productUrlPathGeneratorFactory->create(); 258 | } 259 | 260 | return $this->productUrlPathGenerator; 261 | } 262 | 263 | /** 264 | * Get products collection 265 | * 266 | * @param array $productsFilter 267 | * @param int $storeId 268 | * @return Collection 269 | */ 270 | protected function _getProductsCollection(array $productsFilter = [], int $storeId = 0): Collection 271 | { 272 | $productsCollection = $this->productCollectionFactory->create(); 273 | 274 | $productsCollection->setStore($storeId) 275 | ->addStoreFilter($storeId) 276 | ->addAttributeToSelect('name') 277 | ->addAttributeToSelect('visibility') 278 | ->addAttributeToSelect('url_key') 279 | ->addAttributeToSelect('url_path') 280 | ->addAttributeToFilter('visibility', ['neq' => Visibility::VISIBILITY_NOT_VISIBLE]) 281 | // use limit to avoid an "eating" of a memory 282 | ->setPageSize($this->productsCollectionPageSize); 283 | 284 | if (count($productsFilter) > 0) { 285 | $productsCollection->addIdFilter($productsFilter); 286 | } 287 | 288 | return $productsCollection; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | “Regenerate Url Rewrites” extension 2 | ===================== 3 | Magento 2 "Regenerate Url Rewrites" extension add a CLI feature which allow regenerating a Url rewrites of products/categories in all stores or specific store. 4 | Extension homepage: https://github.com/olegkoval/magento2-regenerate_url_rewrites 5 | 6 | ## CONTACTS 7 | * Email: olegkoval.ca@gmail.com 8 | * LinkedIn: https://www.linkedin.com/in/oleg-koval-85bb2314/ 9 | 10 | ## DONATIONS / SUPPORT ME ON 11 | * [PayPal](https://www.paypal.com/donate/?hosted_button_id=995MLRKBNY9QQ) 12 | * [Patreon](https://www.patreon.com/olegkoval) 13 | 14 | ## INSTALLATION 15 | 16 | ### COMPOSER INSTALLATION 17 | * run composer command: 18 | >`$> composer require olegkoval/magento2-regenerate-url-rewrites` 19 | 20 | ### MANUAL INSTALLATION 21 | * extract files from an archive 22 | 23 | * deploy files into Magento2 folder `app/code/OlegKoval/RegenerateUrlRewrites` 24 | 25 | ### ENABLE EXTENSION 26 | * enable extension (use Magento 2 command line interface \*): 27 | >`$> php bin/magento module:enable OlegKoval_RegenerateUrlRewrites` 28 | 29 | * to make sure that the enabled module is properly registered, run 'setup:upgrade': 30 | >`$> php bin/magento setup:upgrade` 31 | 32 | * [if needed] re-compile code and re-deploy static view files: 33 | >`$> php bin/magento setup:di:compile` 34 | >`$> php bin/magento setup:static-content:deploy` 35 | 36 | 37 | ## HOW TO USE IT: 38 | * to regenerate Url Rewrites of all products in all stores (only products) set entity type to "product": 39 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=product` 40 | 41 | because `product` entity type is default — you can skip it: 42 | >`$> php bin/magento ok:urlrewrites:regenerate` 43 | 44 | * to regenerate, Url Rewrites in the specific store view (e.g.: store view id is "2") use option `--store-id`: 45 | >`$> php bin/magento ok:urlrewrites:regenerate --store-id=2` 46 | 47 | * to regenerate Url Rewrites of some specific product, then use option `product-id` (e.g.: product ID is "122"): 48 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=product --product-id=122` 49 | 50 | or 51 | >`$> php bin/magento ok:urlrewrites:regenerate --product-id=122` 52 | 53 | * to regenerate Url Rewrites of specific products range then use option `products-range` (e.g.: regenerate for all products with ID between "101" and "152"): 54 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=product --products-range=101-152` 55 | 56 | \* if in the range you have a gap of ID's (in range 101-152 products with ID's 110, 124, 150 not exists) — do not worry, a script handles this. 57 | 58 | or 59 | >`$> php bin/magento ok:urlrewrites:regenerate --products-range=101-152` 60 | 61 | * to save a current Url Rewrites (you want to get a new URL rewites and save current) use option `--save-old-urls`: 62 | >`$> php bin/magento ok:urlrewrites:regenerate --save-old-urls` 63 | 64 | * to prevent regeneration of "url_key" values (use current "url_key" values) use option `--no-regen-url-key`: 65 | >`$> php bin/magento ok:urlrewrites:regenerate --no-regen-url-key` 66 | 67 | * if you do not want to run a full reindex at the end of Url Rewrites generation then use option `--no-reindex`: 68 | >`$> php bin/magento ok:urlrewrites:regenerate --no-reindex` 69 | 70 | * if you do not want to run cache:clean at the end of Url Rewrites generation then use option `--no-cache-clean`: 71 | >`$> php bin/magento ok:urlrewrites:regenerate --no-cache-clean` 72 | 73 | * if you do not want to run cache:flush at the end of Url Rewrites generation then use option `--no-cache-flush`: 74 | >`$> php bin/magento ok:urlrewrites:regenerate --no-cache-flush` 75 | 76 | * if you do not want to display a progress bar in the console then use option `--no-progress`: 77 | >`$> php bin/magento ok:urlrewrites:regenerate --no-progress` 78 | 79 | #### REGENERATE URL REWRITES OF CATEGORY 80 | * to regenerate Url Rewrites of all categories in all stores, set an entity type to "category": 81 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=category` 82 | 83 | * to regenerate Url Rewrites of some specific category, then use option `category-id` (e.g.: category ID is "15"): 84 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=category --category-id=15` 85 | 86 | * to regenerate Url Rewrites of specific categories range then use option `categories-range` (e.g.: regenerate for all categories with ID between "4" and "12"): 87 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=category --categories-range=4-12` 88 | 89 | \* if in the range you have a gap of ID's (in range 4-12 category with ID "6" not exists) — do not worry, a script handles this. 90 | 91 | \*\* If you use options `--category-id` or `--categories-range` then you can skip option `--entity-type=category` - extension will understand that you want to use a category entity. 92 | 93 | ### YOU CAN COMBINE OPTIONS 94 | >`$> php bin/magento ok:urlrewrites:regenerate --store-id=2 --save-old-urls --no-regen-url-key --no-reindex` 95 | 96 | ### YOU CANNOT COMBINE THESE OPTIONS 97 | * `--entity-type=product` and `--category-id`/`--categories-range` 98 | * `--entity-type=category` and `--product-id`/`--products-range` 99 | * `--category-id` and/or `--categories-range` and/or `--product-id` and/or `--products-range` 100 | 101 | ### DEPRECATED OPTIONS 102 | * `--check-use-category-in-product-url` — extension uses a built-in Magento Url Rewrites generator which check this option in any way. 103 | 104 | ### EXAMPLES OF USAGE 105 | * Regenerate Url Rewrites for product with ID "38" in store with ID "3": 106 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=product --store-id=3 --product-id=38` 107 | 108 | or 109 | >`$> php bin/magento ok:urlrewrites:regenerate --store-id=3 --product-id=38` 110 | 111 | * Regenerate Url Rewrites for products with ID's 5,6,7,8,9,10,11,12 in store with ID "2" and do not run full reindex at the end of process: 112 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=product --store-id=2 --products-range=5-12 --no-reindex` 113 | 114 | * Regenerate Url Rewrites for category with ID "22" in all stores and save current Url Rewrites: 115 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=category --category-id=22 --save-old-urls` 116 | 117 | * Regenerate Url Rewrites for categories with ID's 21,22,23,24,25 in store with ID "2": 118 | >`$> php bin/magento ok:urlrewrites:regenerate --entity-type=category --categories-range=21-25 --store-id=2` 119 | 120 | Enjoy! 121 | 122 | Best regards, 123 | Oleg Koval 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "olegkoval/magento2-regenerate-url-rewrites", 3 | "description": "Add into Magento 2 a CLI feature which allow to regenerate a Url Rewrites of products and categories", 4 | "keywords": ["magento", "magento2 extension", "magento 2 extension", "extension", "module", "magento2 module", "magento 2 module"], 5 | "type": "magento2-module", 6 | "homepage": "https://github.com/olegkoval/magento2-regenerate_url_rewrites", 7 | "authors": [ 8 | { 9 | "name": "Oleg Koval", 10 | "email": "olegkoval.ca@gmail.com" 11 | } 12 | ], 13 | "license": [ 14 | "OSL-3.0", 15 | "AFL-3.0" 16 | ], 17 | "require": { 18 | }, 19 | "autoload": { 20 | "files": [ 21 | "registration.php" 22 | ], 23 | "psr-4": { 24 | "OlegKoval\\RegenerateUrlRewrites\\": "" 25 | } 26 | }, 27 | "minimum-stability": "alpha", 28 | "prefer-stable": true 29 | } 30 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | OlegKoval\RegenerateUrlRewrites\Console\Command\RegenerateUrlRewrites 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2017-2067 Oleg Koval 8 | * @license OSL-3.0, AFL-3.0 9 | */ 10 | 11 | use Magento\Framework\Component\ComponentRegistrar; 12 | 13 | ComponentRegistrar::register( 14 | ComponentRegistrar::MODULE, 15 | 'OlegKoval_RegenerateUrlRewrites', 16 | __DIR__ 17 | ); 18 | --------------------------------------------------------------------------------