├── .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 | [](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 | 
74 |
75 | ## Store 2: site_nl
76 | Update your base url to `https://my.domain.tld/nl/`
77 | 
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 | 
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 |