torann/device-view
Laravel package that detects the visitor's device type and serves different views for mobile, tablet, and desktop. Register device-specific view paths and fall back cleanly when no matching view exists. Small and config-driven.
Install with an explicit version. This is a 0.0.x line; a caret constraint behaves as a pin, and nothing will float forward. Be deliberate:
composer require "torann/device-view:0.0.4"
Register the provider manually. Laravel 5.4 predates package auto-discovery. Add it to config/app.php:
'providers' => [
// ...
Torann\DeviceView\DeviceViewServiceProvider::class,
],
If a later upgrade to 5.5+ brings auto-discovery and the package declares an extra.laravel block, remove this entry in the same commit as the upgrade or you get double registration.
Publish and actually read the config. It is the contract for device→suffix mapping and view paths:
php artisan vendor:publish --provider="Torann\DeviceView\DeviceViewServiceProvider"
Then read src/ end to end before you write application code. The whole package is small, the changelog is a single line, and the README will not tell you the resolution order or which Blade directives it registers (grep the provider for Blade::directive and view.finder). You need to know whether it decorates or replaces the file view finder — that is the only piece you might later reimplement.
First use case: prove the resolution, then build one page. Do not adopt this across an app before you have watched it resolve a path:
resources/views/welcome.blade.php and the mobile variant the config expects.php artisan tinker, read the absolute resolved path:
app('view')->getFinder()->find('welcome');
app('view')->exists('welcome.mobile');
curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 ...)" on the route — and confirm the path changes.php artisan config:cache and repeat. Packages of this vintage routinely call env() outside config/*.php and only break in cached production deploys. Test this on day one, not in release week.Where to look first, in order: the service provider (what it binds, and whether it decorates the finder), the device classifier (bundled regexes vs. a Mobile_Detect-style dependency — check that dependency's license and maintenance), then the config.
Leave controllers alone — that is the entire value proposition. Existing return view('pages.home', $data) calls resolve to a variant without an if ($mobile) tree. If you find yourself writing device checks in controllers anyway, you have paid the dependency cost and received nothing.
Choose layout swap before per-page variants.
layouts.app, add layouts.app.mobile. One file per device class, tiny QA surface, no drift. Every page inherits the device shell automatically.pages.home / pages.home.mobile. Powerful, but each variant is a new artifact to maintain, translate, and regression-test.Start with the layout swap and escalate page-by-page only where the module list genuinely diverges (mobile-lite checkout, reduced nav) — not where the page merely looks different. This keeps the QA multiplier bounded instead of tripling the template tree.
Drive the suffix from config, not from string literals. The suffix convention is a config concern. Keep .mobile out of application code so that changing the mapping is one edit, not a grep-and-replace across twenty templates.
Wrap it behind your own interface on day one. No application code should import Torann\... directly:
// app/Support/DeviceViewResolver.php
namespace App\Support;
interface DeviceViewResolver
{
public function device(): string; // 'mobile' | 'tablet' | 'desktop'
public function view(string $base): string; // resolved view name
}
Bind it in your own AppServiceProvider and delegate to the package. Replacement later becomes a one-file change instead of an archaeology project.
Use the Blade directives for narrow conditionals, not whole pages. Directives are the right tool for an app-download banner, a lite nav, or a "tap to call" block inside a shared template. They are the wrong tool for anything that makes a desktop template read like two templates glued together.
Audit variants in CI, not in code review. Silent fallback means a missing variant is a design regression, not a 500. Make the absence loud:
// app/Console/Commands/AuditDeviceViews.php
foreach (File::glob(resource_path('views/**/*.blade.php')) as $file) {
// derive base name; assert each configured device class has a variant
// or is on an explicit allow-list of "desktop-only" pages
}
The allow-list matters: not every page needs a mobile variant, and the command should fail on unexpected gaps only.
Verify view composers against the resolved name. A composer registered on layouts.app may not fire for layouts.app.mobile if the package resolves to a different name than you registered against. Test it explicitly, or register shared bindings against * and accept the cost.
Build the desktop escape hatch yourself. A "view full site" link is a product requirement on any dynamic-serving site and the package does not provide one. Add middleware that reads a cookie or query param and pins resolution to desktop, and make sure your cache varies on that input too.
Ship Vary: User-Agent as part of the integration:
// app/Http/Middleware/VaryUserAgent.php
public function handle($request, Closure $next)
{
$response = $next($request);
$response->headers->set('Vary', 'User-Agent', false);
return $response;
}
Confirm your CDN cache key includes User-Agent (or the package's device cookie). If the CDN keys on URL alone, this package is net-negative — stop there.
Test matrix: one feature test per device class per critical page, with an explicit UA. Do not rely on the test client's default UA; it resolves to desktop and your mobile tests will pass without ever exercising the path.
Treat it as a legacy artifact, not a dependency. Last release January 2017, existing solely to declare Laravel 5.4 support. 14 stars, zero dependents, single maintainer. On 5.5+ the capability is roughly 100 lines: decorate the file view finder, suffix the view name, view()->exists() before returning it. BSD-2-Clause permits vendoring with the license notice retained, which removes the upgrade-cadence risk for hours of work. Build or vendor; do not depend.
Silent fallback is the failure mode, not the safety net. Everything degrades gracefully to desktop, which in production reads as a broken mobile layout someone screenshots — not an exception someone gets paged on. Instrument it: log device !== 'desktop' && ! view()->exists($variant) at warning level, and run the audit command in CI.
Dot-notation collision. Laravel maps view('home.mobile') to home/mobile.blade.php. If the package's convention is a dot suffix, a variant file and a nested directory become indistinguishable. Confirm which one the finder actually resolves before you build a directory structure around it — this bites during a refactor, not during the demo.
Tablet detection is fundamentally unreliable. iPadOS Safari reports a desktop UA, so tablet variants may simply never fire on modern iPads. Large phones, small laptops, and touch convertibles all misclassify. Never put business logic — pricing, feature gating, entitlement — behind the device class. Presentation only. If you have no real tablet markup, map tablet to mobile in config rather than shipping dead paths.
@include goes through the same finder. Useful (you get partials/nav.mobile for free) and dangerous (a stray file named like a variant changes behaviour globally). Know this before it surprises you during a routine partial extraction.
Watch for view-finder collisions. The finder is a shared extension point. Theming packages, multi-tenant template overrides, and anything else rebinding view.finder will interact with this in an order you did not choose. If you already decorate the finder, resolve the order explicitly and write a test asserting both still work.
Namespaced views and view()->first() need dedicated tests. view('vendor::home') with a suffix inserted in the wrong position produces a "view not found" that only appears on mobile. view()->first([...]) may bypass or double-apply the decoration depending on implementation.
Don't use it for mail, PDF, or API responses. There is no meaningful UA, so you fall through to the default — harmless, but confusing for whoever debugs the mail template.
Debugging one-liners worth keeping (behind local on a single route, not sprinkled as dd()):
app('view')->getFinder()->find('pages.home'); // absolute resolved path
app('view')->exists('pages.home.mobile'); // does the variant exist?
Run php artisan view:clear after renaming variants. Compiled Blade is fine, but stale finder hints and opcache on the views path cause confusing "it worked in staging" reports.
SEO sign-off is a precondition. Same URL, divergent HTML is dynamic serving. Ensure content parity on revenue and indexable pages, keep Vary: User-Agent correct, and get a named owner for the mobile-first-indexing implications. Three template trees drift, and drift on a money page is expensive.
Pin and track it. Record the exact version and a named replacement date in the dependency register. A caret constraint on 0.0.x means nothing floats forward — which is safe, and also means the package will never tell you when it has become dead weight.
How can I help you explore Laravel packages today?