├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Torann │ └── DeviceView │ │ ├── DeviceViewServiceProvider.php │ │ ├── Facades │ │ └── DeviceView.php │ │ ├── FileViewFinder.php │ │ └── Middleware │ │ ├── DesktopRedirect.php │ │ ├── MobileRedirect.php │ │ └── SubdomainRedirect.php └── config │ └── device-view.php └── tests └── ThemeFileViewFinderTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | 7 | before_script: 8 | - curl -s http://getcomposer.org/installer | php 9 | - php composer.phar install --dev 10 | 11 | script: phpunit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 2-Clause License 2 | Copyright (c) 2015, Daniel Stainback 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Device View 2 | 3 | [![Total Downloads](https://poser.pugx.org/torann/geoip/downloads.png)](https://packagist.org/packages/torann/device-view) 4 | [![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/torann) 5 | [![Donate weekly to this project using Gratipay](https://img.shields.io/badge/gratipay-donate-yellow.svg)](https://gratipay.com/~torann) 6 | [![Donate to this project using Flattr](https://img.shields.io/badge/flattr-donate-yellow.svg)](https://flattr.com/profile/torann) 7 | [![Donate to this project using Paypal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4CJA2A97NPYVU) 8 | 9 | - [Device View on Packagist](https://packagist.org/packages/torann/device-view) 10 | - [Device View on GitHub](https://github.com/Torann/device-view) 11 | 12 | Dynamically change Laravel views based on the visitor's device. 13 | 14 | ## Installation 15 | 16 | ### Composer 17 | 18 | From the command line run: 19 | 20 | ``` 21 | composer require torann/device-view 22 | ``` 23 | 24 | ### Setup 25 | 26 | This package extends Laravel's built in `ViewServiceProvider`, so that provider must be replaced in `app/app.php`. 27 | Replace the instance of `'Illuminate\View\ViewServiceProvider',` with `'Torann\DeviceView\DeviceViewServiceProvider',`. 28 | 29 | 30 | ### Publish the configurations 31 | 32 | Run this on the command line from the root of your project: 33 | 34 | ``` 35 | $ php artisan vendor:publish --provider="Torann\DeviceView\DeviceViewServiceProvider" 36 | ``` 37 | 38 | A configuration file will be publish to `config/device-view.php`. 39 | 40 | 41 | ## Configuration 42 | 43 | The default settings are for the device views to be in the `views` directory in `resources/` with the default theme called `default`. 44 | 45 | ``` 46 | resources/ 47 | views/ 48 | default/ 49 | mobile/ 50 | tablet/ 51 | ``` 52 | 53 | ## Usage 54 | 55 | A standard call to `View::make('index')` will look for an index view in `resources/views/default/`. However, if a theme is specified with 56 | `$app['view.finder']->setDeviceView('mobile');` prior to calling `View::make()` then the view will first be looked for in `resources/views/mobile/views`. 57 | If the view is not found for the current theme the default theme will then be searched. 58 | 59 | ### Facade 60 | 61 | The `DeviceView` facade can also be used if preferred `DeviceView::setDeviceView('mobile')` by adding an entry for `Torann\DeviceView\Facades\DeviceView` to `config/app.php`. 62 | 63 | ### Helper Methods 64 | 65 | **DeviceView::getPlatform()** 66 | 67 | Return the user's operating system. 68 | 69 | ## Example 70 | 71 | Given a directory structure of: 72 | 73 | ``` 74 | resources/ 75 | views/ 76 | default/ 77 | layout.blade.php 78 | admin.blade.php 79 | mobile/ 80 | layout.blade.php 81 | ``` 82 | 83 | ``` 84 | View::make('layout'); // Loads resources/views/default/layout.blade.php 85 | 86 | $app['view.finder']->setDeviceView('default'); 87 | 88 | View::make('layout'); // Loads resources/views/mobile/layout.blade.php 89 | View::make('admin'); // Loads resources/views/default/admin.blade.php 90 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torann/device-view", 3 | "description": "Provides support for device based view layouts in Laravel.", 4 | "keywords": ["laravel", "view", "themes"], 5 | "license": "BSD-2-Clause", 6 | "authors": [ 7 | { 8 | "name": "Daniel Stainback", 9 | "email": "torann@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.5.9", 14 | "illuminate/support": "~5.1", 15 | "mobiledetect/mobiledetectlib": "2.*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~5.0", 19 | "mockery/mockery": "^0.9.4" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Torann\\DeviceView\\": "src/Torann/DeviceView" 24 | } 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "0.1-dev" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Torann/DeviceView/DeviceViewServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('view.finder', function($app) { 25 | return new FileViewFinder($app['files'], $app['config']['view'], null, $app); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Torann/DeviceView/Facades/DeviceView.php: -------------------------------------------------------------------------------- 1 | 'default' 31 | ]; 32 | 33 | /** 34 | * Default layout 35 | * 36 | * @var string 37 | */ 38 | protected $defaultView = 'default'; 39 | 40 | /** 41 | * User platform 42 | * 43 | * @var string 44 | */ 45 | protected $userPlatform; 46 | 47 | /** 48 | * Session instance 49 | * 50 | * @var array 51 | */ 52 | protected $session; 53 | 54 | /** 55 | * Request instance 56 | * 57 | * @var array 58 | */ 59 | protected $request; 60 | 61 | /** 62 | * Create a new file view loader instance. 63 | * 64 | * @param \Illuminate\Filesystem\Filesystem $files 65 | * @param array $config 66 | * @param array $extensions 67 | * @param \Illuminate\Foundation\Application $app 68 | */ 69 | public function __construct(Filesystem $files, array $config, array $extensions = null, Application $app) 70 | { 71 | parent::__construct($files, $config['paths'], $extensions); 72 | 73 | // Set session instance 74 | $this->session = $app['session']; 75 | 76 | // Set request instance 77 | $this->request = $app['request']; 78 | 79 | // Set default view 80 | $this->defaultView = array_get($config, 'default', $this->defaultView); 81 | 82 | // Set valid devices 83 | $this->devices = array_get($config, 'devices', array()); 84 | 85 | // Set default device 86 | $this->devices['default'] = array_get($config, 'default', 'default'); 87 | 88 | // Set location 89 | $this->viewPath = array_get($config, 'path'); 90 | } 91 | 92 | /** 93 | * Add a location to the finder. 94 | * 95 | * @param string $location 96 | * @return void 97 | */ 98 | public function addLocation($location) 99 | { 100 | array_unshift($this->paths, $location); 101 | } 102 | 103 | /** 104 | * Get the fully qualified location of the view. 105 | * 106 | * @param string $name 107 | * @return string 108 | */ 109 | public function find($name) 110 | { 111 | // Detect device type 112 | $this->detectDevice(); 113 | 114 | return parent::find($name); 115 | } 116 | 117 | /** 118 | * Find the given view in the list of paths. 119 | * 120 | * @param string $name 121 | * @param array $paths 122 | * @return string 123 | * 124 | * @throws \InvalidArgumentException 125 | */ 126 | protected function findInPaths($name, $paths) 127 | { 128 | try 129 | { 130 | return parent::findInPaths($name, $paths); 131 | } 132 | catch (InvalidArgumentException $e) 133 | { 134 | $name = $this->deviceView ? "{$this->deviceView}.$name" : $name; 135 | 136 | throw new InvalidArgumentException("View [$name] not found."); 137 | } 138 | } 139 | 140 | /** 141 | * Get the device view type. 142 | * 143 | * @return string 144 | */ 145 | public function getDevice() 146 | { 147 | if (! $this->deviceView) { 148 | $this->detectDevice(); 149 | } 150 | 151 | return $this->deviceView; 152 | } 153 | 154 | /** 155 | * Set the view to be used over the default view. 156 | * 157 | * @param string $view 158 | * @return void 159 | */ 160 | public function setDeviceView($view) 161 | { 162 | $this->deviceView = $view; 163 | 164 | $this->addLocation("{$this->viewPath}/{$this->deviceView}"); 165 | } 166 | 167 | /** 168 | * Determine if the device is valid. 169 | * 170 | * @param string $device 171 | * @return bool 172 | */ 173 | public function validDevice($device) 174 | { 175 | return array_key_exists($device, $this->devices); 176 | } 177 | 178 | /** 179 | * Return user's platform. 180 | * 181 | * @return string 182 | */ 183 | public function getPlatform() 184 | { 185 | if (! $this->userPlatform) 186 | { 187 | $userAgent = $_SERVER['HTTP_USER_AGENT']; 188 | 189 | $os_array = [ 190 | 'iphone' => 'ios', 191 | 'ipod' => 'ios', 192 | 'ipad' => 'ios', 193 | 'windows' => 'windows', 194 | 'macintosh|mac os x' => 'os-x', 195 | 'mac_powerpc' => 'os-9', 196 | 'linux' => 'linux', 197 | 'ubuntu' => 'ubuntu', 198 | 'android' => 'android', 199 | 'blackberry' => 'blackberry', 200 | 'webos' => 'webos' 201 | ]; 202 | 203 | foreach ($os_array as $regex => $value) 204 | { 205 | if ((bool) preg_match(sprintf('#%s#is', $regex), $userAgent, $matches)) 206 | { 207 | $this->userPlatform = $value; 208 | break; 209 | } 210 | } 211 | } 212 | 213 | return $this->userPlatform; 214 | } 215 | 216 | /** 217 | * Detect which view to show based on device. 218 | * 219 | * @return void 220 | */ 221 | public function detectDevice() 222 | { 223 | // Already set 224 | if ($this->deviceView) { 225 | return; 226 | } 227 | 228 | // Allow user to override 229 | if($device = $this->request->get('dv')) { 230 | if ($this->validDevice($device)) { 231 | $this->session->put('device-view', $device); 232 | } 233 | } 234 | 235 | // Get params 236 | $device = $this->session->get('device-view'); 237 | 238 | if (! $device) 239 | { 240 | // Get device 241 | $detect = new Mobile_Detect; 242 | $device = $detect->isTablet() ? 'tablet' : ($detect->isMobile() ? 'mobile' : $this->defaultView); 243 | 244 | // Validate device 245 | $device = $this->validDevice($device) ? $this->devices[$device] : $this->defaultView; 246 | } 247 | 248 | // Set view 249 | $this->setDeviceView($device); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/Torann/DeviceView/Middleware/DesktopRedirect.php: -------------------------------------------------------------------------------- 1 | view = $view; 28 | } 29 | 30 | /** 31 | * Handle an incoming request. 32 | * 33 | * @param \Illuminate\Http\Request $request 34 | * @param \Closure $next 35 | * @param string $routeString 36 | * @return mixed 37 | */ 38 | public function handle($request, Closure $next, $routeString) 39 | { 40 | if (! $request->ajax()) { 41 | if ($this->view->getFinder()->getDevice() === 'default') { 42 | return redirect()->route($routeString); 43 | } 44 | } 45 | 46 | return $next($request); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Torann/DeviceView/Middleware/MobileRedirect.php: -------------------------------------------------------------------------------- 1 | view = $view; 28 | } 29 | 30 | /** 31 | * Handle an incoming request. 32 | * 33 | * @param \Illuminate\Http\Request $request 34 | * @param \Closure $next 35 | * @param string $routeString 36 | * @return mixed 37 | */ 38 | public function handle($request, Closure $next, $routeString) 39 | { 40 | if (! $request->ajax()) { 41 | if ($this->view->getFinder()->getDevice() === 'mobile') { 42 | return redirect()->route($routeString); 43 | } 44 | } 45 | 46 | return $next($request); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Torann/DeviceView/Middleware/SubdomainRedirect.php: -------------------------------------------------------------------------------- 1 | ajax()) 19 | { 20 | $subdomain = $this->getSubdomain($request->getHost()); 21 | //return redirect()->guest('auth/login'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | 27 | /** 28 | * Returns the subdomain portion of provided host. 29 | * 30 | * @param string $host host 31 | * 32 | * @return mixed 33 | */ 34 | private function getSubdomain($host) 35 | { 36 | $registerableDomain = $this->getRegisterableDomain($host); 37 | 38 | if ($registerableDomain === null || $host === $registerableDomain) { 39 | return; 40 | } 41 | 42 | $registerableDomainParts = array_reverse(explode('.', $registerableDomain)); 43 | $hostParts = array_reverse(explode('.', $host)); 44 | $subdomainParts = array_slice($hostParts, count($registerableDomainParts)); 45 | 46 | return implode('.', array_reverse($subdomainParts)); 47 | } 48 | 49 | /** 50 | * Returns registerable domain portion of provided host. 51 | * 52 | * @param string $host 53 | * 54 | * @return mixed 55 | */ 56 | private function getRegisterableDomain($host) 57 | { 58 | $parts = array_reverse(explode('.', $host)); 59 | $publicSuffix = array_shift($parts); 60 | 61 | if ($publicSuffix === null || $host == $publicSuffix) { 62 | return; 63 | } 64 | 65 | $publicSuffixParts = array_reverse(explode('.', $publicSuffix)); 66 | $hostParts = array_reverse(explode('.', $host)); 67 | $registerableDomainParts = $publicSuffixParts + array_slice($hostParts, 0, count($publicSuffixParts) + 1); 68 | 69 | return implode('.', array_reverse($registerableDomainParts)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/config/device-view.php: -------------------------------------------------------------------------------- 1 | realpath(base_path('resources/views/themes')), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Default Layout 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Here you may define devices to the proper layout. 22 | | 23 | */ 24 | 25 | 'default' => 'default', 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Devices 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Here you may define devices to the proper layout. 33 | | 34 | */ 35 | 36 | 'devices' => [ 37 | 38 | 'desktop' => 'default', 39 | 40 | 'mobile' => 'default', 41 | 42 | 'tablet' => 'default', 43 | ] 44 | ); 45 | -------------------------------------------------------------------------------- /tests/ThemeFileViewFinderTest.php: -------------------------------------------------------------------------------- 1 | getFinder(); 15 | 16 | $finder->setThemesLocation(__DIR__); 17 | 18 | $this->assertEquals(__DIR__, $finder->getThemesLocation()); 19 | } 20 | 21 | public function testSettingDefaultTheme() 22 | { 23 | $finder = $this->getFinder(); 24 | 25 | $finder->setDefaultTheme('default'); 26 | 27 | $this->assertEquals('default', $finder->getDefaultTheme()); 28 | } 29 | 30 | public function testSettingCurrentTheme() 31 | { 32 | $finder = $this->getFinder(); 33 | 34 | $finder->setCurrentTheme('foo'); 35 | 36 | $this->assertEquals('foo', $finder->getCurrentTheme()); 37 | } 38 | 39 | public function testGettingCurrentThemePath() 40 | { 41 | $finder = $this->getFinder(); 42 | 43 | $finder->setThemesLocation(__DIR__); 44 | $finder->setCurrentTheme('foo'); 45 | 46 | $this->assertEquals(__DIR__ . '/foo', $finder->getCurrentThemePath()); 47 | } 48 | 49 | public function testGettingAvailableThemes() 50 | { 51 | $finder = $this->getFinder(); 52 | 53 | $finder->setThemesLocation(__DIR__); 54 | 55 | $finder->getFilesystem()->shouldReceive('directories')->once()->with(__DIR__)->andReturn(array( 56 | __DIR__ . '/themes/default', 57 | __DIR__ . '/themes/empty', 58 | __DIR__ . '/themes/foo' 59 | )); 60 | $finder->getFilesystem()->shouldReceive('isDirectory')->once()->with(__DIR__ . '/themes/default/views')->andReturn(true); 61 | $finder->getFilesystem()->shouldReceive('isDirectory')->once()->with(__DIR__ . '/themes/empty/views')->andReturn(false); 62 | $finder->getFilesystem()->shouldReceive('isDirectory')->once()->with(__DIR__ . '/themes/foo/views')->andReturn(true); 63 | 64 | $this->assertEquals(array('default', 'foo'), $finder->getAvailableThemes()); 65 | } 66 | 67 | public function testNotThemedViewFinding() 68 | { 69 | $finder = $this->getFinder(); 70 | 71 | $finder->setThemesLocation(__DIR__); 72 | 73 | $finder->setDefaultTheme('default'); 74 | 75 | $finder->setCurrentTheme('current'); 76 | 77 | $finder->getFilesystem()->shouldReceive('exists')->once()->with(__DIR__ . '/current/views/index.blade.php')->andReturn(false); 78 | $finder->getFilesystem()->shouldReceive('exists')->once()->with(__DIR__ . '/current/views/index.php')->andReturn(false); 79 | $finder->getFilesystem()->shouldReceive('exists')->once()->with(__DIR__ . '/default/views/index.blade.php')->andReturn(true); 80 | 81 | $this->assertEquals(__DIR__ . '/default/views/index.blade.php', $finder->find('index')); 82 | } 83 | 84 | public function testThemedViewFinding() 85 | { 86 | $finder = $this->getFinder(); 87 | 88 | $finder->setThemesLocation(__DIR__); 89 | 90 | $finder->setDefaultTheme('default'); 91 | 92 | $finder->setCurrentTheme('current'); 93 | 94 | $finder->getFilesystem()->shouldReceive('exists')->once()->with(__DIR__ . '/current/views/index.blade.php')->andReturn(true); 95 | 96 | $this->assertEquals(__DIR__ . '/current/views/index.blade.php', $finder->find('index')); 97 | } 98 | 99 | protected function getFinder() 100 | { 101 | return new AlexWhitman\ViewThemes\ThemeFileViewFinder(m::mock('Illuminate\Filesystem\Filesystem'), array(__DIR__)); 102 | } 103 | 104 | } 105 | --------------------------------------------------------------------------------