├── CHANGELOG.md ├── phpstan.neon ├── ecs.php ├── src ├── translations │ └── en │ │ └── route-map.php ├── services │ ├── ServicesTrait.php │ └── Routes.php ├── helpers │ └── Field.php ├── icon.svg ├── RouteMap.php ├── controllers │ └── RoutesController.php └── variables │ └── RouteMapVariable.php ├── Makefile ├── LICENSE.md ├── composer.json └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Route Map Changelog 2 | 3 | ## 5.0.0 - 2024.10.14 4 | ### Added 5 | * Initial Craft CMS 5 release 6 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/translations/en/route-map.php: -------------------------------------------------------------------------------- 1 | 'Route Map Cache', 18 | '{name} plugin loaded' => '{name} plugin loaded', 19 | ]; 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAJOR_VERSION?=5 2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/ 3 | VENDOR?=nystudio107 4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR)) 5 | 6 | .PHONY: dev docs release 7 | 8 | # Start up the buildchain dev server 9 | dev: 10 | # Start up the docs dev server 11 | docs: 12 | ${MAKE} -C docs/ dev 13 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release 14 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build 15 | # The internal targets used by the dev & release targets 16 | --buildchain-clean-build: 17 | --code-quality: 18 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix 19 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon 20 | --code-tests: 21 | --docs-clean-build: 22 | ${MAKE} -C docs/ clean 23 | ${MAKE} -C docs/ image-build 24 | ${MAKE} -C docs/ fix 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) nystudio107 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/services/ServicesTrait.php: -------------------------------------------------------------------------------- 1 | [ 35 | 'routes' => RoutesService::class, 36 | ], 37 | ]; 38 | } 39 | 40 | // Public Methods 41 | // ========================================================================= 42 | 43 | /** 44 | * Returns the routes service 45 | * 46 | * @return RoutesService The helper service 47 | * @throws InvalidConfigException 48 | */ 49 | public function getRoutes(): RoutesService 50 | { 51 | return $this->get('routes'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/craft-routemap", 3 | "description": "Returns a list of Craft/Vue/React route rules and element URLs for ServiceWorkers from Craft entries", 4 | "type": "craft-plugin", 5 | "version": "5.0.0", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "vue", 12 | "react", 13 | "routes", 14 | "serviceworker", 15 | "route map" 16 | ], 17 | "support": { 18 | "docs": "https://nystudio107.com/docs/route-map/", 19 | "issues": "https://nystudio107.com/plugins/routemap/support", 20 | "source": "https://github.com/nystudio107/craft-routemap" 21 | }, 22 | "license": "MIT", 23 | "authors": [ 24 | { 25 | "name": "nystudio107", 26 | "homepage": "https://nystudio107.com/" 27 | } 28 | ], 29 | "require": { 30 | "craftcms/cms": "^5.0.0" 31 | }, 32 | "require-dev": { 33 | "craftcms/cloud": "^1.41.0", 34 | "craftcms/ecs": "dev-main", 35 | "craftcms/phpstan": "dev-main", 36 | "craftcms/rector": "dev-main" 37 | }, 38 | "scripts": { 39 | "phpstan": "phpstan --ansi --memory-limit=1G", 40 | "check-cs": "ecs check --ansi", 41 | "fix-cs": "ecs check --fix --ansi" 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "craftcms/plugin-installer": true, 46 | "yiisoft/yii2-composer": true 47 | }, 48 | "optimize-autoloader": true, 49 | "sort-packages": true 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "nystudio107\\routemap\\": "src/" 54 | } 55 | }, 56 | "extra": { 57 | "class": "nystudio107\\routemap\\RouteMap", 58 | "handle": "route-map", 59 | "name": "Route Map" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/badges/quality-score.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/?branch=v5) [![Code Coverage](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/badges/coverage.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/?branch=v5) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/badges/build.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/build-status/v5) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/craft-routemap/badges/code-intelligence.svg?b=v5)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | # Route Map plugin for Craft CMS 5.x 4 | 5 | Returns a list of Craft/Vue/React route rules and element URLs for ServiceWorkers from Craft entries 6 | 7 | ![Screenshot](./docs/docs/resources/img/plugin-logo.png) 8 | 9 | ## Requirements 10 | 11 | This plugin requires Craft CMS 5.0.0 or later. 12 | 13 | ## Installation 14 | 15 | To install the plugin, follow these instructions. 16 | 17 | 1. Open your terminal and go to your Craft project: 18 | 19 | cd /path/to/project 20 | 21 | 2. Then tell Composer to load the plugin: 22 | 23 | composer require nystudio107/craft-routemap 24 | 25 | 3. Install the plugin via `./craft install/plugin route-map` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Route Map. 26 | 27 | You can also install Route Map via the **Plugin Store** in the Craft Control Panel. 28 | 29 | ## Documentation 30 | 31 | Click here -> [Route Map Documentation](https://nystudio107.com/plugins/routemap/documentation) 32 | 33 | ## Route Map Roadmap 34 | 35 | Some things to do, and ideas for potential features: 36 | 37 | * Add support for Commerce Products / Variant URLs 38 | 39 | Brought to you by [nystudio107](https://nystudio107.com) 40 | -------------------------------------------------------------------------------- /src/helpers/Field.php: -------------------------------------------------------------------------------- 1 | getFieldLayout(); 41 | if (!$layout instanceof FieldLayout) { 42 | return []; 43 | } 44 | 45 | $fields = $layout->getCustomFields(); 46 | /** @var BaseField $field */ 47 | foreach ($fields as $field) { 48 | if ($field instanceof $fieldType) { 49 | $foundFields[] = $field->handle; 50 | } 51 | } 52 | 53 | return $foundFields; 54 | } 55 | 56 | /** 57 | * Return all the fields in the $matrixBlock of the type $fieldType class 58 | * 59 | * @param Entry $matrixEntry 60 | * @param string $fieldType 61 | * @return ?array 62 | */ 63 | public static function matrixFieldsOfType(Entry $matrixEntry, string $fieldType): ?array 64 | { 65 | $foundFields = []; 66 | 67 | try { 68 | $matrixEntryTypeModel = $matrixEntry->getType(); 69 | } catch (InvalidConfigException $e) { 70 | $matrixEntryTypeModel = null; 71 | } 72 | if ($matrixEntryTypeModel) { 73 | $fields = $matrixEntryTypeModel->getCustomFields(); 74 | /** @var BaseField $field */ 75 | foreach ($fields as $field) { 76 | if ($field instanceof $fieldType) { 77 | $foundFields[$field->handle] = $field->name; 78 | } 79 | } 80 | } 81 | 82 | return $foundFields; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 12 | 34 | 35 | 41 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/RouteMap.php: -------------------------------------------------------------------------------- 1 | sender; 83 | $variable->set('routeMap', RouteMapVariable::class); 84 | } 85 | ); 86 | 87 | // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT 88 | Event::on( 89 | Elements::class, 90 | Elements::EVENT_AFTER_SAVE_ELEMENT, 91 | static function(ElementEvent $event): void { 92 | Craft::debug( 93 | 'Elements::EVENT_AFTER_SAVE_ELEMENT', 94 | __METHOD__ 95 | ); 96 | /** @var Element $element */ 97 | $element = $event->element; 98 | $bustCache = true; 99 | // Only bust the cache if the element is ENABLED or LIVE 100 | if (($element->getStatus() !== Element::STATUS_ENABLED) 101 | && ($element->getStatus() !== Entry::STATUS_LIVE) 102 | ) { 103 | $bustCache = false; 104 | } 105 | 106 | if ($bustCache) { 107 | Craft::debug( 108 | 'Cache busted due to saving: ' . $element::class . ' - ' . $element->title, 109 | __METHOD__ 110 | ); 111 | RouteMap::$plugin->routes->invalidateCache(); 112 | } 113 | } 114 | ); 115 | 116 | // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS 117 | Event::on( 118 | ClearCaches::class, 119 | ClearCaches::EVENT_REGISTER_CACHE_OPTIONS, 120 | static function(RegisterCacheOptionsEvent $event): void { 121 | $event->options[] = [ 122 | 'key' => 'route-map', 123 | 'label' => Craft::t('route-map', 'Route Map Cache'), 124 | 'action' => function(): void { 125 | RouteMap::$plugin->routes->invalidateCache(); 126 | }, 127 | ]; 128 | } 129 | ); 130 | 131 | Craft::info( 132 | Craft::t( 133 | 'route-map', 134 | '{name} plugin loaded', 135 | ['name' => $this->name] 136 | ), 137 | __METHOD__ 138 | ); 139 | } 140 | 141 | // Protected Methods 142 | // ========================================================================= 143 | } 144 | -------------------------------------------------------------------------------- /src/controllers/RoutesController.php: -------------------------------------------------------------------------------- 1 | asJson(RouteMap::$plugin->routes->getAllUrls($criteria, $siteId)); 45 | } 46 | 47 | /** 48 | * Return the public URLs for a section 49 | */ 50 | public function actionGetSectionUrls(string $section, array $criteria = [], ?int $siteId = null): Response 51 | { 52 | return $this->asJson(RouteMap::$plugin->routes->getSectionUrls($section, $criteria, $siteId)); 53 | } 54 | 55 | /** 56 | * Return the public URLs for a category 57 | */ 58 | public function actionGetCategoryUrls(string $category, array $criteria = [], ?int $siteId = null): Response 59 | { 60 | return $this->asJson(RouteMap::$plugin->routes->getCategoryUrls($category, $criteria, $siteId)); 61 | } 62 | 63 | /** 64 | * Return all the section route rules 65 | * 66 | * @param string $format 'Craft'|'React'|'Vue' 67 | * @param ?int $siteId 68 | * @return Response 69 | */ 70 | public function actionGetAllRouteRules(string $format = 'Craft', ?int $siteId = null): Response 71 | { 72 | return $this->asJson(RouteMap::$plugin->routes->getAllRouteRules($format, $siteId)); 73 | } 74 | 75 | /** 76 | * Return the route rules for a specific section 77 | * 78 | * @param string $section 79 | * @param string $format 'Craft'|'React'|'Vue' 80 | * @param ?int $siteId 81 | * @return Response 82 | */ 83 | public function actionGetSectionRouteRules(string $section, string $format = 'Craft', ?int $siteId = null): Response 84 | { 85 | return $this->asJson(RouteMap::$plugin->routes->getSectionRouteRules($section, $format, $siteId)); 86 | } 87 | 88 | /** 89 | * Return the route rules for a specific category 90 | * 91 | * @param string $category 92 | * @param string $format 'Craft'|'React'|'Vue' 93 | * @param ?int $siteId 94 | * @return Response 95 | */ 96 | public function actionGetCategoryRouteRules(string $category, string $format = 'Craft', ?int $siteId = null): Response 97 | { 98 | return $this->asJson(RouteMap::$plugin->routes->getCategoryRouteRules($category, $format, $siteId)); 99 | } 100 | 101 | /** 102 | * Return the Craft Control Panel and `routes.php` rules 103 | * 104 | * @param ?int $siteId 105 | * @param bool $includeGlobal 106 | * @return Response 107 | */ 108 | public function actionGetRouteRules(?int $siteId = null, bool $includeGlobal = true): Response 109 | { 110 | return $this->asJson(RouteMap::$plugin->routes->getRouteRules($siteId, $includeGlobal)); 111 | } 112 | 113 | /** 114 | * Get all the assets of the type $assetTypes that are used in the Entry 115 | * that matches the $url 116 | * 117 | * @param string $url 118 | * @param array $assetTypes 119 | * @param ?int $siteId 120 | * @return Response 121 | */ 122 | public function actionGetUrlAssetUrls(string $url, array $assetTypes = ['image'], ?int $siteId = null): Response 123 | { 124 | return $this->asJson(RouteMap::$plugin->routes->getUrlAssetUrls($url, $assetTypes, $siteId)); 125 | } 126 | 127 | /** 128 | * Returns all of the URLs for the given $elementType based on the passed in 129 | * $criteria and $siteId 130 | * 131 | * @param string|ElementInterface $elementType 132 | * @param array $criteria 133 | * @param ?int $siteId 134 | * @return Response 135 | */ 136 | public function actionGetElementUrls(string|ElementInterface $elementType, array $criteria = [], ?int $siteId = null): Response 137 | { 138 | return $this->asJson(RouteMap::$plugin->routes->getElementUrls($elementType, $criteria, $siteId)); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/variables/RouteMapVariable.php: -------------------------------------------------------------------------------- 1 | routes->getAllUrls($criteria, $siteId); 35 | } 36 | 37 | /** 38 | * Return all the section and category route rules 39 | * 40 | * @param string $format 'Craft'|'React'|'Vue' 41 | * @param ?int $siteId 42 | * @return array 43 | */ 44 | public function getAllRouteRules(string $format = 'Craft', ?int $siteId = null): array 45 | { 46 | return RouteMap::$plugin->routes->getAllRouteRules($format, $siteId); 47 | } 48 | 49 | 50 | /** 51 | * Return the public URLs for a section 52 | * 53 | * @param string $section 54 | * @param array $criteria 55 | * @param ?int $siteId 56 | * @return array 57 | */ 58 | public function getSectionUrls(string $section, array $criteria = [], ?int $siteId = null): array 59 | { 60 | return RouteMap::$plugin->routes->getSectionUrls($section, $criteria, $siteId); 61 | } 62 | 63 | 64 | /** 65 | * Return all the section route rules 66 | * 67 | * @param string $format 'Craft'|'React'|'Vue' 68 | * @param ?int $siteId 69 | * @return array 70 | */ 71 | public function getAllSectionRouteRules(string $format = 'Craft', ?int $siteId = null): array 72 | { 73 | return RouteMap::$plugin->routes->getAllSectionRouteRules($format, $siteId); 74 | } 75 | 76 | /** 77 | * Return the route rules for a specific section 78 | * 79 | * @param string $section 80 | * @param string $format 'Craft'|'React'|'Vue' 81 | * @param ?int $siteId 82 | * @return array 83 | */ 84 | public function getSectionRouteRules(string $section, string $format = 'Craft', ?int $siteId = null): array 85 | { 86 | return RouteMap::$plugin->routes->getSectionRouteRules($section, $format, $siteId); 87 | } 88 | 89 | /** 90 | * Return the public URLs for a category group 91 | * 92 | * @param string $category 93 | * @param array $criteria 94 | * @param ?int $siteId 95 | * @return array 96 | */ 97 | public function getCategoryUrls(string $category, array $criteria = [], ?int $siteId = null): array 98 | { 99 | return RouteMap::$plugin->routes->getCategoryUrls($category, $criteria, $siteId); 100 | } 101 | 102 | /** 103 | * Return all the cateogry group route rules 104 | * 105 | * @param string $format 'Craft'|'React'|'Vue' 106 | * @param ?int $siteId 107 | * @return array 108 | */ 109 | public function getAllCategoryRouteRules(string $format = 'Craft', ?int $siteId = null): array 110 | { 111 | return RouteMap::$plugin->routes->getAllCategoryRouteRules($format, $siteId); 112 | } 113 | 114 | /** 115 | * Return the route rules for a specific category 116 | * 117 | * @param string $category 118 | * @param string $format 'Craft'|'React'|'Vue' 119 | * @param ?int $siteId 120 | * @return array 121 | */ 122 | public function getCategoryRouteRules(string $category, string $format = 'Craft', ?int $siteId = null): array 123 | { 124 | return RouteMap::$plugin->routes->getCategoryRouteRules($category, $format, $siteId); 125 | } 126 | 127 | 128 | /** 129 | * Get all the assets of the type $assetTypes that are used in the Entry 130 | * that matches the $url 131 | * 132 | * @param string $url 133 | * @param array $assetTypes 134 | * @param ?int $siteId 135 | * @return array 136 | */ 137 | public function getUrlAssetUrls(string $url, array $assetTypes = ['image'], ?int $siteId = null): array 138 | { 139 | return RouteMap::$plugin->routes->getUrlAssetUrls($url, $assetTypes, $siteId); 140 | } 141 | 142 | /** 143 | * Returns all the URLs for the given $elementType based on the passed in 144 | * $criteria and $siteId 145 | * 146 | * @param string|ElementInterface $elementType 147 | * @param array $criteria 148 | * @param ?int $siteId 149 | * @return array 150 | */ 151 | public function getElementUrls(string|ElementInterface $elementType, array $criteria = [], ?int $siteId = null): array 152 | { 153 | return RouteMap::$plugin->routes->getElementUrls($elementType, $criteria, $siteId); 154 | } 155 | 156 | /** 157 | * Get all routes rules defined in the config/routes.php file and CMS 158 | * 159 | * @param bool $incGlobalRules - merge global routes with the site rules 160 | * @param ?int $siteId 161 | * @return array 162 | */ 163 | public function getRouteRules(?int $siteId = null, bool $incGlobalRules = true): array 164 | { 165 | return RouteMap::$plugin->routes->getRouteRules($siteId, $incGlobalRules); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/services/Routes.php: -------------------------------------------------------------------------------- 1 | getElements(); 109 | $elementTypes = $elements->getAllElementTypes(); 110 | foreach ($elementTypes as $elementType) { 111 | /** @noinspection SlowArrayOperationsInLoopInspection */ 112 | $urls = array_merge($urls, $this->getElementUrls($elementType, $criteria, $siteId)); 113 | } 114 | 115 | return $urls; 116 | } 117 | 118 | /** 119 | * Return all the section route rules 120 | * 121 | * @param string $format 'Craft'|'React'|'Vue' 122 | * @param ?int $siteId 123 | * @return array 124 | */ 125 | public function getAllRouteRules(string $format = 'Craft', ?int $siteId = null): array 126 | { 127 | // Get all the sections 128 | $sections = $this->getAllSectionRouteRules($format, $siteId); 129 | $categories = $this->getAllCategoryRouteRules($format, $siteId); 130 | $rules = $this->getRouteRules($siteId); 131 | 132 | return [ 133 | 'sections' => $sections, 134 | 'categories' => $categories, 135 | 'rules' => $rules, 136 | ]; 137 | } 138 | 139 | /** 140 | * Return the public URLs for a section 141 | * 142 | * @param string $section 143 | * @param array $criteria 144 | * @param ?int $siteId 145 | * @return array 146 | */ 147 | public function getSectionUrls(string $section, array $criteria = [], ?int $siteId = null): array 148 | { 149 | $criteria = array_merge([ 150 | 'section' => $section, 151 | 'status' => 'enabled', 152 | ], $criteria); 153 | 154 | return $this->getElementUrls(Entry::class, $criteria, $siteId); 155 | } 156 | 157 | /** 158 | * Return all the section route rules 159 | * 160 | * @param string $format 'Craft'|'React'|'Vue' 161 | * @param ?int $siteId 162 | * @return array 163 | */ 164 | public function getAllSectionRouteRules(string $format = 'Craft', ?int $siteId = null): array 165 | { 166 | $routeRules = []; 167 | // Get all the sections 168 | $sections = Craft::$app->getEntries()->getAllSections(); 169 | foreach ($sections as $section) { 170 | $routes = $this->getSectionRouteRules($section->handle, $format, $siteId); 171 | if (!empty($routes)) { 172 | $routeRules[$section->handle] = $routes; 173 | } 174 | } 175 | 176 | return $routeRules; 177 | } 178 | 179 | /** 180 | * Return the route rules for a specific section 181 | * 182 | * @param string $section 183 | * @param string $format 'Craft'|'React'|'Vue' 184 | * @param ?int $siteId 185 | * @return array 186 | */ 187 | public function getSectionRouteRules(string $section, string $format = 'Craft', ?int $siteId = null): array 188 | { 189 | $devMode = Craft::$app->getConfig()->getGeneral()->devMode; 190 | $cache = Craft::$app->getCache(); 191 | 192 | // Set up our cache criteria 193 | $cacheKey = $this->getCacheKey($this::ROUTEMAP_SECTION_RULES, [$section, $format, $siteId]); 194 | $duration = $devMode ? $this::DEVMODE_ROUTEMAP_CACHE_DURATION : $this::ROUTEMAP_CACHE_DURATION; 195 | $dependency = new TagDependency([ 196 | 'tags' => [ 197 | $this::ROUTEMAP_CACHE_TAG, 198 | ], 199 | ]); 200 | 201 | // Just return the data if it's already cached 202 | return $cache->getOrSet($cacheKey, function() use ($section, $format, $siteId): array { 203 | Craft::info( 204 | 'Route Map cache miss: ' . $section, 205 | __METHOD__ 206 | ); 207 | $resultingRoutes = []; 208 | 209 | $section = Craft::$app->getEntries()->getSectionByHandle($section); 210 | if ($section) { 211 | $sites = $section->getSiteSettings(); 212 | 213 | foreach ($sites as $site) { 214 | if ($site->hasUrls && ($siteId === null || (int)$site->siteId === $siteId)) { 215 | // Get section data to return 216 | $route = [ 217 | 'handle' => $section->handle, 218 | 'siteId' => $site->siteId, 219 | 'type' => $section->type, 220 | 'url' => $site->uriFormat, 221 | 'template' => $site->template, 222 | ]; 223 | 224 | // Normalize the routes based on the format 225 | $resultingRoutes[$site->siteId] = $this->normalizeFormat($format, $route); 226 | } 227 | } 228 | } 229 | 230 | // If there's only one siteId for this section, just return it 231 | if (count($resultingRoutes) === 1) { 232 | $resultingRoutes = reset($resultingRoutes); 233 | } 234 | 235 | return $resultingRoutes; 236 | }, $duration, $dependency); 237 | } 238 | 239 | /** 240 | * Return the public URLs for a category 241 | * 242 | * @param string $category 243 | * @param array $criteria 244 | * @param ?int $siteId 245 | * 246 | * @return array 247 | */ 248 | public function getCategoryUrls(string $category, array $criteria = [], ?int $siteId = null): array 249 | { 250 | $criteria = array_merge([ 251 | 'group' => $category, 252 | ], $criteria); 253 | 254 | return $this->getElementUrls(Category::class, $criteria, $siteId); 255 | } 256 | 257 | /** 258 | * Return all the cateogry group route rules 259 | * 260 | * @param string $format 'Craft'|'React'|'Vue' 261 | * @param ?int $siteId 262 | * @return array 263 | */ 264 | public function getAllCategoryRouteRules(string $format = 'Craft', ?int $siteId = null): array 265 | { 266 | $routeRules = []; 267 | // Get all the sections 268 | $groups = Craft::$app->getCategories()->getAllGroups(); 269 | foreach ($groups as $group) { 270 | $routes = $this->getCategoryRouteRules($group->handle, $format, $siteId); 271 | if (!empty($routes)) { 272 | $routeRules[$group->handle] = $routes; 273 | } 274 | } 275 | 276 | return $routeRules; 277 | } 278 | 279 | /** 280 | * Return the route rules for a specific category 281 | * 282 | * @param int|string $category 283 | * @param string $format 'Craft'|'React'|'Vue' 284 | * @param ?int $siteId 285 | * @return array 286 | */ 287 | public function getCategoryRouteRules(int|string $category, string $format = 'Craft', ?int $siteId = null): array 288 | { 289 | $devMode = Craft::$app->getConfig()->getGeneral()->devMode; 290 | $cache = Craft::$app->getCache(); 291 | 292 | if (is_int($category)) { 293 | $categoryGroup = Craft::$app->getCategories()->getGroupById($category); 294 | if ($categoryGroup === null) { 295 | return []; 296 | } 297 | 298 | $handle = $categoryGroup->handle; 299 | } else { 300 | $handle = $category; 301 | } 302 | 303 | if ($handle === null) { 304 | return []; 305 | } 306 | 307 | // Set up our cache criteria 308 | $cacheKey = $this->getCacheKey($this::ROUTEMAP_CATEGORY_RULES, [$category, $handle, $format, $siteId]); 309 | $duration = $devMode ? $this::DEVMODE_ROUTEMAP_CACHE_DURATION : $this::ROUTEMAP_CACHE_DURATION; 310 | $dependency = new TagDependency([ 311 | 'tags' => [ 312 | $this::ROUTEMAP_CACHE_TAG, 313 | ], 314 | ]); 315 | // Just return the data if it's already cached 316 | return $cache->getOrSet($cacheKey, function() use ($category, $handle, $format, $siteId): array { 317 | Craft::info( 318 | 'Route Map cache miss: ' . $category, 319 | __METHOD__ 320 | ); 321 | $resultingRoutes = []; 322 | $category = Craft::$app->getCategories()->getGroupByHandle($handle); 323 | if ($category) { 324 | $sites = $category->getSiteSettings(); 325 | 326 | foreach ($sites as $site) { 327 | if ($site->hasUrls && ($siteId === null || (int)$site->siteId === $siteId)) { 328 | // Get section data to return 329 | $route = [ 330 | 'handle' => $category->handle, 331 | 'siteId' => $site->siteId, 332 | 'url' => $site->uriFormat, 333 | 'template' => $site->template, 334 | ]; 335 | 336 | // Normalize the routes based on the format 337 | $resultingRoutes[$site->siteId] = $this->normalizeFormat($format, $route); 338 | } 339 | } 340 | } 341 | 342 | // If there's only one siteId for this section, just return it 343 | if (count($resultingRoutes) === 1) { 344 | $resultingRoutes = reset($resultingRoutes); 345 | } 346 | 347 | return $resultingRoutes; 348 | }, $duration, $dependency); 349 | } 350 | 351 | /** 352 | * Get all the assets of the type $assetTypes that are used in the Entry 353 | * that matches the $url 354 | * 355 | * @param string $url 356 | * @param array $assetTypes 357 | * @param ?int $siteId 358 | * @return array 359 | */ 360 | public function getUrlAssetUrls(string $url, array $assetTypes = ['image'], ?int $siteId = null): array 361 | { 362 | $devMode = Craft::$app->getConfig()->getGeneral()->devMode; 363 | $cache = Craft::$app->getCache(); 364 | 365 | // Extract a URI from the URL 366 | $uri = parse_url($url, PHP_URL_PATH); 367 | $uri = ltrim($uri, '/'); 368 | // Set up our cache criteria 369 | $cacheKey = $this->getCacheKey($this::ROUTEMAP_ASSET_URLS, [$uri, $assetTypes, $siteId]); 370 | $duration = $devMode ? $this::DEVMODE_ROUTEMAP_CACHE_DURATION : $this::ROUTEMAP_CACHE_DURATION; 371 | $dependency = new TagDependency([ 372 | 'tags' => [ 373 | $this::ROUTEMAP_CACHE_TAG, 374 | ], 375 | ]); 376 | 377 | // Just return the data if it's already cached 378 | return $cache->getOrSet($cacheKey, function() use ($uri, $assetTypes, $siteId): array { 379 | Craft::info( 380 | 'Route Map cache miss: ' . $uri, 381 | __METHOD__ 382 | ); 383 | $resultingAssetUrls = []; 384 | 385 | // Find the element that matches this URI 386 | /** @var ?Entry $element */ 387 | $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, true); 388 | if ($element) { 389 | // Iterate any Assets fields for this entry 390 | $assetFields = FieldHelper::fieldsOfType($element, AssetsField::class); 391 | foreach ($assetFields as $assetField) { 392 | /** @var Asset[] $assets */ 393 | $assets = $element[$assetField]->all(); 394 | foreach ($assets as $asset) { 395 | /** @var $asset Asset */ 396 | if (in_array($asset->kind, $assetTypes, true) 397 | && !in_array($asset->getUrl(), $resultingAssetUrls, true)) { 398 | $resultingAssetUrls[] = $asset->getUrl(); 399 | } 400 | } 401 | } 402 | 403 | // Iterate through any Assets embedded in Matrix fields 404 | $matrixFields = FieldHelper::fieldsOfType($element, MatrixField::class); 405 | foreach ($matrixFields as $matrixField) { 406 | /** @var Entry[] $matrixEntries */ 407 | $matrixEntries = $element[$matrixField]->all(); 408 | foreach ($matrixEntries as $matrixEntry) { 409 | $assetFields = FieldHelper::matrixFieldsOfType($matrixEntry, AssetsField::class); 410 | foreach ($assetFields as $assetField) { 411 | foreach ($matrixEntry[$assetField] as $asset) { 412 | /** @var $asset Asset */ 413 | if (in_array($asset->kind, $assetTypes, true) 414 | && !in_array($asset->getUrl(), $resultingAssetUrls, true)) { 415 | $resultingAssetUrls[] = $asset->getUrl(); 416 | } 417 | } 418 | } 419 | } 420 | } 421 | } 422 | 423 | return $resultingAssetUrls; 424 | }, $duration, $dependency); 425 | } 426 | 427 | /** 428 | * Returns all the URLs for the given $elementType based on the passed in 429 | * $criteria and $siteId 430 | * 431 | * @param string|ElementInterface $elementType 432 | * @param array $criteria 433 | * @param ?int $siteId 434 | * @return array 435 | */ 436 | public function getElementUrls(string|ElementInterface $elementType, array $criteria = [], ?int $siteId = null): array 437 | { 438 | $devMode = Craft::$app->getConfig()->getGeneral()->devMode; 439 | $cache = Craft::$app->getCache(); 440 | 441 | // Merge in the $criteria passed in 442 | $criteria = array_merge([ 443 | 'siteId' => $siteId, 444 | 'limit' => null, 445 | ], $criteria); 446 | // Set up our cache criteria 447 | /* @var ElementInterface $elementInterface */ 448 | $elementInterface = is_object($elementType) ? $elementType : new $elementType(); 449 | $cacheKey = $this->getCacheKey($this::ROUTEMAP_ELEMENT_URLS, [$elementInterface, $criteria, $siteId]); 450 | $duration = $devMode ? $this::DEVMODE_ROUTEMAP_CACHE_DURATION : $this::ROUTEMAP_CACHE_DURATION; 451 | $dependency = new TagDependency([ 452 | 'tags' => [ 453 | $this::ROUTEMAP_CACHE_TAG, 454 | ], 455 | ]); 456 | 457 | // Just return the data if it's already cached 458 | return $cache->getOrSet($cacheKey, function() use ($elementInterface, $criteria): array { 459 | Craft::info( 460 | 'Route Map cache miss: ' . $elementInterface::class, 461 | __METHOD__ 462 | ); 463 | $resultingUrls = []; 464 | 465 | // Get all of the entries in the section 466 | $query = $this->getElementQuery($elementInterface, $criteria); 467 | $elements = $query->all(); 468 | 469 | // Iterate through the elements and grab their URLs 470 | foreach ($elements as $element) { 471 | if ($element instanceof Element 472 | && $element->uri !== null 473 | && !in_array($element->uri, $resultingUrls, true) 474 | ) { 475 | $uri = $this->normalizeUri($element->uri); 476 | $resultingUrls[] = $uri; 477 | } 478 | } 479 | 480 | return $resultingUrls; 481 | }, $duration, $dependency); 482 | } 483 | 484 | /** 485 | * Invalidate the RouteMap caches 486 | */ 487 | public function invalidateCache(): void 488 | { 489 | $cache = Craft::$app->getCache(); 490 | TagDependency::invalidate($cache, self::ROUTEMAP_CACHE_TAG); 491 | Craft::info( 492 | 'Route Map cache cleared', 493 | __METHOD__ 494 | ); 495 | } 496 | 497 | /** 498 | * Get all routes rules defined in the config/routes.php file and CMS 499 | * 500 | * @return array 501 | * @property ?int $siteId 502 | * 503 | * @property bool $includeGlobal - merge global routes with the site rules 504 | */ 505 | public function getRouteRules(?int $siteId = null, bool $includeGlobal = true): array 506 | { 507 | $globalRules = []; 508 | 509 | $siteRoutes = $this->getDbRoutes($siteId); 510 | 511 | return array_merge( 512 | Craft::$app->getRoutes()->getConfigFileRoutes(), 513 | $globalRules, 514 | $siteRoutes 515 | ); 516 | } 517 | 518 | // Protected Methods 519 | // ========================================================================= 520 | /** 521 | * Query the database for db routes 522 | * 523 | * @param ?int $siteId 524 | * @return array 525 | */ 526 | protected function getDbRoutes(?int $siteId = null): array 527 | { 528 | return Craft::$app->getRoutes()->getProjectConfigRoutes(); 529 | } 530 | 531 | /** 532 | * Normalize the routes based on the format 533 | * 534 | * @param string $format 'Craft'|'React'|'Vue' 535 | * @param array $route 536 | * @return array 537 | */ 538 | protected function normalizeFormat(string $format, array $route): array 539 | { 540 | // Normalize the URL 541 | $route['url'] = $this->normalizeUri($route['url']); 542 | // Transform the URLs depending on the format requested 543 | switch ($format) { 544 | // React & Vue routes have a leading / and {slug} -> :slug 545 | case $this::ROUTE_FORMAT_REACT: 546 | case $this::ROUTE_FORMAT_VUE: 547 | $matchRegEx = '`{(.*?)}`'; 548 | $replaceRegEx = ':$1'; 549 | $route['url'] = preg_replace($matchRegEx, $replaceRegEx, $route['url']); 550 | // Add a leading / 551 | $route['url'] = '/' . ltrim($route['url'], '/'); 552 | break; 553 | 554 | // Craft-style URLs don't need to be changed 555 | case $this::ROUTE_FORMAT_CRAFT: 556 | default: 557 | // Do nothing 558 | break; 559 | } 560 | 561 | return $route; 562 | } 563 | 564 | /** 565 | * Normalize the URI 566 | * 567 | * @param string $url 568 | * @return string 569 | */ 570 | protected function normalizeUri(string $url): string 571 | { 572 | // Handle the special '__home__' URI 573 | if ($url === '__home__') { 574 | $url = '/'; 575 | } 576 | 577 | return $url; 578 | } 579 | 580 | /** 581 | * Generate a cache key with the combination of the $prefix and an md5() 582 | * hashed version of the flattened $args array 583 | */ 584 | protected function getCacheKey(string $prefix, array $args = []): string 585 | { 586 | $cacheKey = $prefix; 587 | $flattenedArgs = ''; 588 | // If an array of $args is passed in, flatten it into a concatenated string 589 | if (!empty($args)) { 590 | foreach ($args as $arg) { 591 | if ((is_object($arg) || is_array($arg)) && !empty($arg)) { 592 | $flattenedArgs .= http_build_query($arg); 593 | } 594 | 595 | if (is_string($arg)) { 596 | $flattenedArgs .= $arg; 597 | } 598 | } 599 | 600 | // Make an md5 hash out of it 601 | $flattenedArgs = md5($flattenedArgs); 602 | } 603 | 604 | return $cacheKey . $flattenedArgs; 605 | } 606 | 607 | /** 608 | * Returns the element query based on $elementType and $criteria 609 | * 610 | * @param array $criteria 611 | * @param ElementInterface $elementType 612 | * @return ElementQueryInterface 613 | */ 614 | protected function getElementQuery(ElementInterface $elementType, array $criteria): ElementQueryInterface 615 | { 616 | $query = $elementType::find(); 617 | Craft::configure($query, $criteria); 618 | 619 | return $query; 620 | } 621 | } 622 | --------------------------------------------------------------------------------