├── .gitignore ├── .travis.yml ├── Plugin └── Frontend │ └── Magento │ └── Framework │ └── App │ └── Request │ └── PathInfo.php ├── README.md ├── Test └── Unit │ └── PluginPathInfoTest.php ├── composer.json ├── etc ├── di.xml └── module.xml ├── example-baseurl-en.png ├── example-baseurl-nl.png ├── example-baseurl-static-media.png ├── phpunit.xml.dist └── registration.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.3' 4 | - '7.4' 5 | 6 | cache: vendor 7 | 8 | before_script: 9 | - composer config http-basic.$COMPOSER_MAGENTO_REPO_HOST $COMPOSER_MAGENTO_REPO_USERNAME $COMPOSER_MAGENTO_REPO_PASSWORD 10 | - composer install --prefer-dist -o 11 | 12 | script: "vendor/bin/phpunit --config phpunit.xml.dist --coverage-text" 13 | 14 | -------------------------------------------------------------------------------- /Plugin/Frontend/Magento/Framework/App/Request/PathInfo.php: -------------------------------------------------------------------------------- 1 | scopeConfig = $scopeConfig; 36 | $this->httpRequest = $httpRequest; 37 | 38 | $this->resolveBaseUrlFromStoreConfig(); 39 | } 40 | 41 | 42 | public function beforeGetPathInfo( 43 | PathInfoRequest $subject, 44 | string $requestUri, 45 | string $baseUrl 46 | ) { 47 | if ($baseUrl === '') { 48 | $baseUrl = $this->baseUrl; 49 | } 50 | 51 | return [ 52 | $requestUri, 53 | $baseUrl 54 | ]; 55 | } 56 | 57 | /** 58 | * Allow to setup a deeper path in Magento backend to resolve the frontend 59 | * 60 | * @return string 61 | */ 62 | public function resolveBaseUrlFromStoreConfig(): void 63 | { 64 | // Because config isn't loaded yet, we need to resolve this with the Magento run code 65 | $mageRunCode = $this->httpRequest->getServerValue('MAGE_RUN_CODE', false); 66 | if (! $mageRunCode) { 67 | return; 68 | } 69 | 70 | $unsecureBaseUrl = $this->scopeConfig->getValue('web/unsecure/base_url', ScopeInterface::SCOPE_STORE, $mageRunCode); 71 | $secureBaseUrl = $this->scopeConfig->getValue('web/secure/base_url', ScopeInterface::SCOPE_STORE, $mageRunCode); 72 | if (null === $unsecureBaseUrl || null === $secureBaseUrl) { 73 | return; 74 | } 75 | 76 | $unsecurePath = parse_url($unsecureBaseUrl, PHP_URL_PATH); 77 | $securePath = parse_url($secureBaseUrl, PHP_URL_PATH); 78 | 79 | // Only allow if set for both, secure and unsecure to avoid conflicts 80 | if ($unsecurePath !== $securePath) { 81 | return; 82 | } 83 | 84 | if (! $unsecurePath) { 85 | return; 86 | } 87 | 88 | if ($unsecurePath === '/') { 89 | return; 90 | } 91 | 92 | $this->baseUrl = '/' . trim($unsecurePath, '/'); 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/elgentos/magento2-baseurlpath.svg?branch=main)](https://travis-ci.org/elgentos/magento2-baseurlpath) 2 | # Magento 2 - Elgentos_BaseUrlPath 3 | Allow setting up a base url in system config other than root. 4 | 5 | If you want to add `https://my.domain.tld/mypseronalurl/` after the baseurl Magento currently doesn't allow this. 6 | You are left with adding the storecode to the path or creating subdirectories with extra files. 7 | 8 | This can get ugly if you just want to add language codes(other than locale-codes) to your domain, 9 | for example `https://my.domain.tld/en/` and `https://my.domain.tld/nl/` 10 | 11 | ## Installation: part 1 - Module via composer 12 | Install the module via composer by running 13 | 14 | ```bash 15 | composer require elgentos/module-baseurlpath 16 | 17 | bin/magento module:enable Elgentos_BaseUrlPath 18 | bin/magento cache:flush 19 | ``` 20 | 21 | ## Installation: part 2 - Nginx MAGE_RUN_CODE 22 | Make sure you setup your `MAGE_RUN_CODE` mapping correctly. By default in Nginx this is only done based on **$host** name. 23 | 24 | We will add the **$request_uri** to the mapping. 25 | 26 | ```nginx 27 | # https://{host}/{language}/{magento_request}/ 28 | # Most specific match goes first 29 | map $host$request_uri $mageRunCode { 30 | hostnames; 31 | default default; 32 | ~other.domain.tld/nl/ other_nl; 33 | ~other.domain.tld other_en; 34 | ~my.domain.tld/en/ site_en; 35 | ~my.domain.tld site_nl; 36 | } 37 | 38 | # fastcgi section 39 | fastcgi_param MAGE_RUN_CODE $mageRunCode; 40 | ``` 41 | 42 | ### Nginx without mapping 43 | Or if Nginx mapping isn't available for you. 44 | 45 | ```nginx 46 | # https://{host}/{language}/{magento_request}/ 47 | # Most specific match goes last 48 | # this one is untested thought(sorry for that) 49 | 50 | set $mageRunCode "default"; 51 | if ($host$request_uri ~ other.domain.tld/) { 52 | set $mageRunCode "other_en"; 53 | } 54 | if ($host$request_uri ~ other.domain.tld/nl/) { 55 | set $mageRunCode "other_nl"; 56 | } 57 | if ($host$request_uri ~ my.domain.tld/) { 58 | set $mageRunCode "site_nl"; 59 | } 60 | if ($host$request_uri ~ my.domain.tld/en/) { 61 | set $mageRunCode "site_en"; 62 | } 63 | ``` 64 | 65 | ## Configuration 66 | After installation goto `Stores` / `Configuration` -> `General` / `Web` -> `Base URLs ((Un)Secure)` 67 | 68 | **My strong advise setting these settings on website or store view level 69 | because making errors could potentially lock you out from the admin pages** 70 | 71 | ## Store 1: site_en 72 | Update your base url to `https://my.domain.tld/en/` 73 | ![example baseurl setting english store](example-baseurl-en.png) 74 | 75 | ## Store 2: site_nl 76 | Update your base url to `https://my.domain.tld/nl/` 77 | ![example baseurl setting dutch store](example-baseurl-nl.png) 78 | 79 | ## Static content and media 80 | To make static content work with these settings, you explicitly need to setup these paths to the root(`/`) of your site. 81 | 82 | ![example baseurl setting static media parameters](example-baseurl-static-media.png) 83 | 84 | ## Authors 85 | 86 | - [Jeroen Boersma](https://github.com/jeroenboersma) 87 | - [Wouter Steenmeijer](https://github.com/woutersteen) 88 | - [Peter Jaap Blaakmeer](https://github.com/peterjaap) 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Test/Unit/PluginPathInfoTest.php: -------------------------------------------------------------------------------- 1 | createMock(ScopeConfigInterface::class); 31 | $httpRequestMock = $this->getMockBuilder(Http::class) 32 | ->setMethods(['getServerValue']) 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $pathInfoRequest = $this->createMock(\Magento\Framework\App\Request\PathInfo::class); 37 | 38 | 39 | $this->scopeConfigMock = $scopeConfigMock; 40 | $this->httpRequestMock = $httpRequestMock; 41 | $this->pathInfoRequest = $pathInfoRequest; 42 | } 43 | 44 | public function testShouldNotInitializeBaseUrlIfMageRunCodeIsNotSet() 45 | { 46 | $scopeConfigMock = $this->scopeConfigMock; 47 | $httpRequestMock = $this->httpRequestMock; 48 | $pathInfoRequest = $this->pathInfoRequest; 49 | 50 | $httpRequestMock->expects($this->once()) 51 | ->method('getServerValue') 52 | ->with('MAGE_RUN_CODE', false) 53 | ->willReturn(false); 54 | 55 | $scopeConfigMock->expects($this->never()) 56 | ->method('getValue'); 57 | 58 | $pathInfo = new PathInfo( 59 | $scopeConfigMock, 60 | $httpRequestMock 61 | ); 62 | 63 | [, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', ''); 64 | $this->assertSame('', $baseUrl); 65 | } 66 | 67 | public function testShouldNotSetBaseUrlIfUnsecureAndSecureBaseUrlPathAreNotTheSame() 68 | { 69 | $scopeConfigMock = $this->scopeConfigMock; 70 | $httpRequestMock = $this->httpRequestMock; 71 | $pathInfoRequest = $this->pathInfoRequest; 72 | 73 | $httpRequestMock->expects($this->once()) 74 | ->method('getServerValue') 75 | ->willReturn('default'); 76 | 77 | $scopeConfigMock->expects($this->exactly(2)) 78 | ->method('getValue') 79 | ->withConsecutive(['web/unsecure/base_url', ScopeInterface::SCOPE_STORE, 'default'], ['web/secure/base_url', ScopeInterface::SCOPE_STORE, 'default']) 80 | ->willReturn('http://localhost.local.host/one', 'http://localhost.local.host/two'); 81 | 82 | $pathInfo = new PathInfo( 83 | $scopeConfigMock, 84 | $httpRequestMock 85 | ); 86 | 87 | [, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', ''); 88 | $this->assertSame('', $baseUrl); 89 | } 90 | 91 | public function testShouldNotSetBaseUrlIfUnsecureBaseUrlPathIsEmpty() 92 | { 93 | $scopeConfigMock = $this->scopeConfigMock; 94 | $httpRequestMock = $this->httpRequestMock; 95 | $pathInfoRequest = $this->pathInfoRequest; 96 | 97 | $httpRequestMock->expects($this->once()) 98 | ->method('getServerValue') 99 | ->willReturn('default'); 100 | 101 | $scopeConfigMock->expects($this->exactly(2)) 102 | ->method('getValue') 103 | ->withConsecutive(['web/unsecure/base_url', ScopeInterface::SCOPE_STORE, 'default'], ['web/secure/base_url', ScopeInterface::SCOPE_STORE, 'default']) 104 | ->willReturn('', ''); 105 | 106 | $pathInfo = new PathInfo( 107 | $scopeConfigMock, 108 | $httpRequestMock 109 | ); 110 | 111 | [, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', ''); 112 | $this->assertSame('', $baseUrl); 113 | } 114 | 115 | public function testShouldSetBaseUrlPathIfDomainsDifferAndPathIsSame() 116 | { 117 | $scopeConfigMock = $this->scopeConfigMock; 118 | $httpRequestMock = $this->httpRequestMock; 119 | $pathInfoRequest = $this->pathInfoRequest; 120 | 121 | $httpRequestMock->expects($this->once()) 122 | ->method('getServerValue') 123 | ->willReturn('default'); 124 | 125 | $scopeConfigMock->expects($this->exactly(2)) 126 | ->method('getValue') 127 | ->withConsecutive(['web/unsecure/base_url', ScopeInterface::SCOPE_STORE, 'default'], ['web/secure/base_url', ScopeInterface::SCOPE_STORE, 'default']) 128 | ->willReturn('http://localhost.local.host/one', 'http://localhost.remote.host/one'); 129 | 130 | $pathInfo = new PathInfo( 131 | $scopeConfigMock, 132 | $httpRequestMock 133 | ); 134 | 135 | [, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', ''); 136 | $this->assertSame('/one', $baseUrl); 137 | } 138 | 139 | public function testShouldTrimBaseUrlPathFromFinalSlash() 140 | { 141 | $scopeConfigMock = $this->scopeConfigMock; 142 | $httpRequestMock = $this->httpRequestMock; 143 | $pathInfoRequest = $this->pathInfoRequest; 144 | 145 | $httpRequestMock->expects($this->once()) 146 | ->method('getServerValue') 147 | ->willReturn('default'); 148 | 149 | $scopeConfigMock->expects($this->exactly(2)) 150 | ->method('getValue') 151 | ->withConsecutive(['web/unsecure/base_url', ScopeInterface::SCOPE_STORE, 'default'], ['web/secure/base_url', ScopeInterface::SCOPE_STORE, 'default']) 152 | ->willReturn('http://localhost.local.host/one/', 'http://localhost.remote.host/one/'); 153 | 154 | $pathInfo = new PathInfo( 155 | $scopeConfigMock, 156 | $httpRequestMock 157 | ); 158 | 159 | [, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', ''); 160 | $this->assertSame('/one', $baseUrl); 161 | } 162 | 163 | public function testShouldNeverReturnASlash() 164 | { 165 | $scopeConfigMock = $this->scopeConfigMock; 166 | $httpRequestMock = $this->httpRequestMock; 167 | $pathInfoRequest = $this->pathInfoRequest; 168 | 169 | $httpRequestMock->expects($this->once()) 170 | ->method('getServerValue') 171 | ->willReturn('default'); 172 | 173 | $scopeConfigMock->expects($this->exactly(2)) 174 | ->method('getValue') 175 | ->withConsecutive(['web/unsecure/base_url', ScopeInterface::SCOPE_STORE, 'default'], ['web/secure/base_url', ScopeInterface::SCOPE_STORE, 'default']) 176 | ->willReturn('http://localhost.local.host/', 'http://localhost.remote.host/'); 177 | 178 | $pathInfo = new PathInfo( 179 | $scopeConfigMock, 180 | $httpRequestMock 181 | ); 182 | 183 | [, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', ''); 184 | $this->assertSame('', $baseUrl); 185 | } 186 | 187 | public function testShouldNotChangeBaseUrlIfNotEmptyAndShouldLeaveRequestUriUnchanged() 188 | { 189 | $scopeConfigMock = $this->scopeConfigMock; 190 | $httpRequestMock = $this->httpRequestMock; 191 | $pathInfoRequest = $this->pathInfoRequest; 192 | 193 | $httpRequestMock->method('getServerValue') 194 | ->willReturn('default'); 195 | 196 | $scopeConfigMock->method('getValue') 197 | ->withConsecutive(['web/unsecure/base_url', ScopeInterface::SCOPE_STORE, 'default'], ['web/secure/base_url', ScopeInterface::SCOPE_STORE, 'default']) 198 | ->willReturn('http://localhost.local.host/', 'http://localhost.remote.host/'); 199 | 200 | $pathInfo = new PathInfo( 201 | $scopeConfigMock, 202 | $httpRequestMock 203 | ); 204 | 205 | [$requestUri, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '', '/magento'); 206 | $this->assertSame('', $requestUri); 207 | $this->assertSame('/magento', $baseUrl); 208 | 209 | [$requestUri, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '/request', '/test'); 210 | $this->assertSame('/request', $requestUri); 211 | $this->assertSame('/test', $baseUrl); 212 | 213 | [$requestUri, $baseUrl] = $pathInfo->beforeGetPathInfo($pathInfoRequest, '/request', ''); 214 | $this->assertSame('/request', $requestUri); 215 | $this->assertSame('', $baseUrl); 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elgentos/module-baseurlpath", 3 | "description": "Allow Magento config to have a base url path instead of root without moving Magento 2 into a subdirectory", 4 | "type": "magento2-module", 5 | "license": "GPL-3.0", 6 | "authors": [ 7 | { 8 | "name": "Jeroen Boersma", 9 | "email": "jeroen@elgentos.nl" 10 | } 11 | ], 12 | "require": { 13 | "magento/framework": "*", 14 | "magento/module-store": "*" 15 | }, 16 | "autoload": { 17 | "files": [ 18 | "registration.php" 19 | ], 20 | "psr-4": { 21 | "Elgentos\\BaseUrlPath\\": "" 22 | } 23 | }, 24 | "repositories": [ 25 | { 26 | "type": "composer", 27 | "url": "https://repo.magento.com/" 28 | } 29 | ], 30 | "require-dev": { 31 | "phpunit/phpunit": "~9.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example-baseurl-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elgentos/magento2-baseurlpath/8c99c628eee4e52dce21b3a3d300e7becd37df3e/example-baseurl-en.png -------------------------------------------------------------------------------- /example-baseurl-nl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elgentos/magento2-baseurlpath/8c99c628eee4e52dce21b3a3d300e7becd37df3e/example-baseurl-nl.png -------------------------------------------------------------------------------- /example-baseurl-static-media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elgentos/magento2-baseurlpath/8c99c628eee4e52dce21b3a3d300e7becd37df3e/example-baseurl-static-media.png -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | * 12 | 13 | 14 | Test 15 | vendor 16 | 17 | 18 | 19 | ./Test/Unit 20 | 21 | 22 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 |