├── Helper ├── Detect.php ├── MobileDetectModifier.php └── Redirect.php ├── README.md ├── Test └── Unit │ └── MobiledetectPluginTest.php ├── View └── Plugin │ └── DesignExceptions.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ ├── menu.xml │ └── system.xml ├── config.xml ├── di.xml └── module.xml └── registration.php /Helper/Detect.php: -------------------------------------------------------------------------------- 1 | mobileDetect = $mobileDetect; 57 | parent::__construct($context); 58 | } 59 | 60 | /** 61 | * @return bool 62 | */ 63 | public function isDetected() 64 | { 65 | return $this->detected; 66 | } 67 | 68 | /** 69 | * If is mobile device 70 | * @return bool 71 | */ 72 | public function isMobile() 73 | { 74 | $this->detected = self::EA_MOBILE; 75 | return $this->mobileDetect->isMobile(); 76 | } 77 | 78 | /** 79 | * If is a tablet 80 | * @return bool 81 | */ 82 | public function isTablet() 83 | { 84 | $this->detected = self::EA_TABLET; 85 | return $this->mobileDetect->isTablet(); 86 | } 87 | 88 | /** 89 | * If is desktop device 90 | * @return bool 91 | */ 92 | public function isDesktop() 93 | { 94 | if ($this->isMobile()) { 95 | return false; 96 | } 97 | $this->detected = self::EA_DESKTOP; 98 | return true; 99 | } 100 | 101 | /** 102 | * The mobile detect instance to be able to use all the functionality 103 | * @return MobileDetect 104 | */ 105 | public function getMobileDetect() 106 | { 107 | return $this->mobileDetect; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Helper/MobileDetectModifier.php: -------------------------------------------------------------------------------- 1 | config = $context->getScopeConfig(); 69 | $this->responseFactory = $responseFactory; 70 | $this->validator = $validator; 71 | parent::__construct($context); 72 | } 73 | 74 | /** 75 | * Get config value 76 | * 77 | * @param string $configPath 78 | * @return string 79 | */ 80 | public function getConfig($configPath) 81 | { 82 | return $this->config->getValue( 83 | $configPath, 84 | ScopeInterface::SCOPE_STORE 85 | ); 86 | } 87 | 88 | /** 89 | * Check if module is enable 90 | * 91 | * @return boolean 92 | */ 93 | public function isEnable() 94 | { 95 | return $this->getConfig(self::MOBILEDETECT_ENABLED); 96 | } 97 | 98 | /** 99 | * Redirect to tablet url 100 | */ 101 | public function redirectTablet() 102 | { 103 | 104 | $tablet = $this->getConfig(self::MOBILEDETECT_TABLET); 105 | 106 | if ($this->validator->isValid($tablet)) { 107 | $this->responseFactory->create()->setRedirect($tablet)->sendResponse(); 108 | } 109 | } 110 | 111 | /** 112 | * Redirect to mobile url 113 | */ 114 | public function redirectMobile() 115 | { 116 | 117 | $mobile = $this->getConfig(self::MOBILEDETECT_MOBILE); 118 | 119 | if ($this->validator->isValid($mobile)) { 120 | $this->responseFactory->create()->setRedirect($mobile)->sendResponse(); 121 | } 122 | } 123 | 124 | /** 125 | * Redirect to desktop url 126 | */ 127 | public function redirectDesktop() 128 | { 129 | 130 | $desktop = $this->getConfig(self::MOBILEDETECT_TABLET); 131 | 132 | if ($this->validator->isValid($desktop)) { 133 | $this->responseFactory->create()->setRedirect($desktop)->sendResponse(); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Mobile Detect Theme Change 2 | 3 | [![Build Status](https://travis-ci.org/EaDesgin/magento2-mobiledetect.svg?branch=master)](https://travis-ci.org/EaDesgin/magento2-mobiledetect) 4 | 5 | Magento 2 Mobile detect system can be used to load different themes base on the client device (desktop, tablet, mobile). 6 | It uses the library https://github.com/serbanghita/Mobile-Detect. 7 | 8 | # How to use the module 9 | 10 | The main configuration can be done under the Content > Design > Configuration. There (Design Rule > User Agent Rules) you can add user agent expressions. 11 | 12 | * add `eadesign_is_mobile` to load a theme for mobile 13 | * add `eadesign_is_tablet` to load a theme for tablet 14 | * add `eadesign_is_desktop` to load a theme for desktop 15 | 16 | Under system configurations you need to enable the extension. Also there you will find 3 fields for redirects. 17 | If you add a url to the mobile field for example the user will be redirected to the url in there. 18 | This can be useful if you want to use a different website/store view url for the mobile theme. 19 | 20 | 21 | # Installation. 22 | 23 | You can install the module via composer or manually by adding it to the app/code directory. The module is available on [packagist.org](https://packagist.org/packages/eadesignro/module-mobiledetect). 24 | 25 | Via composer 26 | 27 | ``` bash 28 | composer config repositories.magento2-mobiledetect git git@github.com:EaDesgin/magento2-mobiledetect; 29 | ``` 30 | 31 | ``` bash 32 | composer require eadesignro/module-mobiledetect; 33 | ``` 34 | 35 | ``` bash 36 | php bin/magento setup:upgrade; 37 | ``` 38 | 39 | # Uninstall 40 | 41 | You need to remove the module. 42 | 43 | ``` bash 44 | composer remove eadesignro/module-mobiledetect; 45 | ``` 46 | -------------------------------------------------------------------------------- /Test/Unit/MobiledetectPluginTest.php: -------------------------------------------------------------------------------- 1 | objectManager = new ObjectManager($this); 76 | 77 | $this->requestMock = $this->getMockBuilder(Http::class) 78 | ->disableOriginalConstructor() 79 | ->getMock(); 80 | 81 | $this->detectHelper = $this->getMockBuilder(Detect::class) 82 | ->setMethods(['getMobileDetect', 'isMobile', 'isTablet','isDetected']) 83 | ->disableOriginalConstructor() 84 | ->getMock(); 85 | 86 | $this->redirectHelper = $this->getMockBuilder(Redirect::class) 87 | ->disableOriginalConstructor() 88 | ->getMock(); 89 | 90 | $this->scopeConfigInterface = $this->getMockBuilder(ScopeConfigInterface::class) 91 | ->disableOriginalConstructor() 92 | ->getMock(); 93 | 94 | $this->subject = $this->objectManager->getObject(InitialDesignExceptions::class); 95 | 96 | $unSerialize = new Unserialize(); 97 | 98 | $this->designExceptions = new DesignExceptions( 99 | $this->scopeConfigInterface, 100 | $this->exceptionConfigPath, 101 | $this->scopeType, 102 | $this->detectHelper, 103 | $this->redirectHelper, 104 | $unSerialize 105 | ); 106 | } 107 | 108 | /** 109 | * @param string $userAgent 110 | * @param bool $useConfig 111 | * @param bool|string $result 112 | * @param array $expressions 113 | * @dataProvider getThemeByRequestDataProvider 114 | */ 115 | public function testIfTheModuleIsNotEnabledStandardSystem($userAgent, $useConfig, $result, $expressions = []) 116 | { 117 | $this->redirectHelper 118 | ->method('isEnable') 119 | ->willReturn(false); 120 | 121 | $this->requestMock->expects($this->once()) 122 | ->method('getServer') 123 | ->with($this->equalTo('HTTP_USER_AGENT')) 124 | ->will($this->returnValue($userAgent)); 125 | 126 | if ($useConfig) { 127 | $this->scopeConfigInterface->expects($this->any()) 128 | ->method('getValue') 129 | ->with($this->equalTo($this->exceptionConfigPath), $this->equalTo($this->scopeType)) 130 | ->will($this->returnValue(serialize($expressions))); 131 | } 132 | 133 | $subject = $this->subject; 134 | 135 | $proceed = function () use ($result) { 136 | return $result; 137 | }; 138 | 139 | $this->assertSame( 140 | $result, 141 | $this->designExceptions->aroundGetThemeByRequest($subject, $proceed, $this->requestMock) 142 | ); 143 | } 144 | 145 | /** 146 | * @return array 147 | */ 148 | public function getThemeByRequestDataProvider() 149 | { 150 | return [ 151 | [false, false, false], 152 | ['iphone', false, false], 153 | ['iphone', true, false], 154 | ['iphone', true, 'matched', [['regexp' => '/iphone/', 'value' => 'matched']]], 155 | ['explorer', true, false, [['regexp' => '/iphone/', 'value' => 'matched']]], 156 | ]; 157 | } 158 | 159 | /** 160 | * @param $userAgent 161 | * @param $case 162 | * @param $ifMobileString 163 | * @param $useConfig 164 | * @param $result 165 | * @param array $expressions 166 | * @dataProvider getIfModuleIfMobileException 167 | */ 168 | public function testIfModuleIfMobileException( 169 | $userAgent, 170 | $case, 171 | $ifMobileString, 172 | $useConfig, 173 | $result, 174 | $expressions = [] 175 | ) { 176 | 177 | $this->redirectHelper 178 | ->method('isEnable') 179 | ->willReturn(true); 180 | 181 | $this->requestMock->method('getServer') 182 | ->with($this->equalTo('HTTP_USER_AGENT')) 183 | ->will($this->returnValue($userAgent)); 184 | 185 | $mobileDetectModifier = $this->getMockBuilder(MobileDetectModifier::class) 186 | ->setMethods(['setHttpHeaders','setUserAgent']) 187 | ->disableOriginalConstructor() 188 | ->getMock(); 189 | 190 | $this->detectHelper->method('getMobileDetect') 191 | ->will($this->returnValue($mobileDetectModifier)); 192 | 193 | $this->detectHelper->method('is' . $case) 194 | ->willReturn(true); 195 | 196 | $this->detectHelper->expects($this->once()) 197 | ->method('isDetected') 198 | ->willReturn($ifMobileString); 199 | 200 | if ($useConfig) { 201 | $this->scopeConfigInterface 202 | ->method('getValue') 203 | ->with($this->equalTo($this->exceptionConfigPath), $this->equalTo($this->scopeType)) 204 | ->will($this->returnValue(serialize($expressions))); 205 | } 206 | 207 | $subject = $this->subject; 208 | 209 | $proceed = function () use ($result) { 210 | return $result; 211 | }; 212 | 213 | $this->assertSame( 214 | $result, 215 | $this->designExceptions->aroundGetThemeByRequest($subject, $proceed, $this->requestMock) 216 | ); 217 | } 218 | 219 | /** 220 | * @return array 221 | */ 222 | public function getIfModuleIfMobileException() 223 | { 224 | return [ 225 | ['iphone', 'Mobile', Detect::EA_MOBILE, false, false], 226 | ['iphone', 'Mobile', Detect::EA_MOBILE, true, false], 227 | ['iphone', 'Tablet', Detect::EA_TABLET, false, false], 228 | ['iphone', 'Tablet', Detect::EA_TABLET, true, false], 229 | ['iphone', 'Desktop', Detect::EA_DESKTOP, false, false], 230 | ['iphone', 'Desktop', Detect::EA_DESKTOP, true, false], 231 | ['iphone', 'Mobile', Detect::EA_MOBILE, true, 'matched', 232 | [['regexp' => '/' . Detect::EA_MOBILE . '/', 'value' => 'matched']] 233 | ], 234 | ['explorer', 'Tablet', Detect::EA_TABLET, true, 'matched', 235 | [['regexp' => '/' . Detect::EA_TABLET . '/', 'value' => 'matched']] 236 | ], 237 | ['explorer', 'Desktop', Detect::EA_DESKTOP, true, 'matched', 238 | [['regexp' => '/' . Detect::EA_DESKTOP . '/', 'value' => 'matched']] 239 | ], 240 | ]; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /View/Plugin/DesignExceptions.php: -------------------------------------------------------------------------------- 1 | detect = $detect; 70 | $this->redirect = $redirect; 71 | $this->unSerialize = $unSerialize; 72 | } 73 | 74 | /** 75 | * @param $subject 76 | * @param $proceed 77 | * @param HttpRequest $request 78 | * @return bool|string 79 | * @SuppressWarnings("unused") 80 | */ 81 | // @codingStandardsIgnoreLine 82 | public function aroundGetThemeByRequest($subject, callable $proceed, HttpRequest $request) 83 | { 84 | $rules = $proceed($request); 85 | 86 | $userAgent = $request->getServer('HTTP_USER_AGENT'); 87 | 88 | if (empty($userAgent)) { 89 | return $rules; 90 | } 91 | 92 | $this->userAgent = $userAgent; 93 | 94 | if (!$this->redirect->isEnable()) { 95 | return $rules; 96 | } 97 | 98 | $mobileDetect = $this->detect->getMobileDetect(); 99 | $mobileDetect->setHttpHeaders($request->getHeaders()); 100 | $mobileDetect->setUserAgent($userAgent); 101 | 102 | $exception = $this->ifThemeChange(); 103 | 104 | if (!$exception) { 105 | return $rules; 106 | } 107 | 108 | $expressions = $this->scopeConfig->getValue( 109 | $this->exceptionConfigPath, 110 | $this->scopeType 111 | ); 112 | 113 | if (!$expressions) { 114 | return $rules; 115 | } 116 | 117 | $expressions = $this->unSerialize->unserialize($expressions); 118 | 119 | foreach ($expressions as $rule) { 120 | if (preg_match($rule['regexp'], $exception)) { 121 | return $rule['value']; 122 | } 123 | } 124 | 125 | return $rules; 126 | } 127 | 128 | /** 129 | * The tablet is overwritten by the mobile 130 | * 131 | * @return bool 132 | */ 133 | private function ifThemeChange() 134 | { 135 | if ($this->detect->isTablet()) { 136 | $this->redirect->redirectTablet(); 137 | } 138 | 139 | if ($this->detect->isMobile()) { 140 | $this->redirect->redirectMobile(); 141 | } 142 | 143 | if ($this->detect->isDesktop()) { 144 | $this->redirect->redirectDesktop(); 145 | } 146 | 147 | $exception = $this->detect->isDetected(); 148 | 149 | return $exception; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eadesignro/module-mobiledetect", 3 | "description": "Magento 2 EaDesign Mobile Detect System", 4 | "type": "magento2-module", 5 | "version": "1.1.8", 6 | "autoload": { 7 | "files": [ 8 | "registration.php" 9 | ], 10 | "psr-4": { 11 | "Eadesigndev\\Mobiledetect\\": "" 12 | } 13 | }, 14 | "repositories": [ 15 | { 16 | "type": "composer", 17 | "url": "https://repo.magento.com/" 18 | } 19 | ], 20 | "license": [ 21 | "Apache-2.0" 22 | ], 23 | "authors": [ 24 | { 25 | "name": "EaDesign", 26 | "email": "office@eadesign.ro", 27 | "homepage": "https://www.eadesign.ro/", 28 | "role": "Developer" 29 | } 30 | ], 31 | "require": { 32 | "php": "~5.6.5|7.0.2|7.0.4|~7.0.6|7.0.25|~7.1.3|~7.2.0", 33 | "magento/module-backend": "100.1.*|101.0.*|100.2.*", 34 | "magento/framework": "100.1.*|100.2.*|101.0.*|102.0.*", 35 | "magento/module-theme": "100.1.*|100.2.*|101.0.*", 36 | "eadesignro/module-eacore": "^0.2.6", 37 | "mobiledetect/mobiledetectlib": "2.8.*" 38 | }, 39 | "require-dev": { 40 | "friendsofphp/php-cs-fixer": "~2.13.0", 41 | "lusitanian/oauth": "~0.8.10", 42 | "magento/magento2-functional-testing-framework": "2.3.9", 43 | "pdepend/pdepend": "2.5.2", 44 | "phpmd/phpmd": "@stable", 45 | "phpunit/phpunit": "~6.5.0", 46 | "sebastian/phpcpd": "~3.0.0", 47 | "squizlabs/php_codesniffer": "3.3.1" 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true 51 | } 52 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /etc/adminhtml/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 29 | 30 | 37 | 38 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 |
24 | 25 | eadesign_extensions 26 | Eadesigndev_Mobiledetect::config_system 27 | 28 | 29 | 30 | 31 | Enables or disables extension. If this is set to yes and you get the default template from Magento then you did something wrong. 32 | Magento\Config\Model\Config\Source\Yesno 33 | 34 | 35 | 36 | If the user comes from mobile 37 | 38 | 39 | 40 | If the user comes from tablet 41 | 42 | 43 | 44 | If the user comes from desktop 45 | 46 | 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 24 | 25 | 1 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 |