tattali/mobile-detect-bundle
Symfony bundle that wraps the Mobile Detect library to identify mobile phones, tablets, and desktop browsers from user agents, with service and Twig integration for device-aware routing, views, and responsive experiences.
Install with Composer; Flex registers the bundle automatically (otherwise add MobileDetectBundle\MobileDetectBundle to config/bundles.php):
composer require tattali/mobile-detect-bundle
Where to look first: the bundle's README.md config block, then bin/console debug:container mobile_detect and bin/console debug:twig to see what is actually registered in your installed version — the config tree has changed between majors, so trust the installed source over blog posts.
First use case — inject and branch. Nothing needs configuring for detection; the services are available out of the box:
use MobileDetectBundle\DeviceDetector\MobileDetectorInterface;
final class BillingController
{
public function __construct(private MobileDetectorInterface $mobileDetector) {}
public function checkout(): Response
{
if ($this->mobileDetector->isMobile() && !$this->mobileDetector->isTablet()) {
// lighter payload, fewer upsells, Apple/Google Pay
}
}
}
In Twig the extension is registered for you:
{% if is_mobile() %}...{% endif %}
{% if is_tablet() %}...{% endif %}
First "real" feature is usually the redirect: send phone visitors to a mobile subdomain, tablet visitors to a tablet one, then widen that into view switching later.
1. Subdomain redirect (the bundle's headline feature). This is the pattern 90% of installs want — no controller code at all:
# config/packages/mobile_detect.yaml
mobile_detect:
redirect:
mobile:
host: m.example.com
action: redirect # redirect | no_redirect | allow
status_code: 302
tablet:
host: t.example.com
action: redirect
status_code: 302
A kernel.request listener inspects the User-Agent and issues the redirect before your controllers run. Use 301 only if the mapping is permanent (it is not — devices change); 302 is the sane default.
2. View switching on a single domain (responsive + separate templates). Enable the switcher so ?device_view=mobile|tablet|full is honoured and persisted in a cookie:
mobile_detect:
switch_device_view:
save_referrer: true
cookie_key: device_view
Then build a "view this site as: Mobile / Tablet / Desktop" footer with {{ path('homepage', {device_view: 'mobile'}) }}. Pair this with action: allow on the redirect config so an explicit choice isn't immediately undone by the auto-redirect — that combination is the whole reason allow exists.
3. Template selection instead of redirects. In a base controller or a kernel.controller listener, pick the template directory:
$view = $deviceView->getDeviceView(); // full / mobile / tablet
$request->attributes->set('_view', $view);
Then render("pages/{$_view}/index.html.twig"). Keeps one URL per page (good for SEO and analytics) while still shipping a distinct mobile layout.
4. Twig-level nudges. The cheapest win: gate app-store banners, sticky "call us" bars, and <video> autoplay behind is_mobile(). Do this before you commit to a second hostname — it is often all you actually need.
5. Service-layer policy. Inject MobileDetectorInterface into services that need degradation logic (image transcoding, feature flags). Avoid injecting it into entities or value objects; pass a boolean in instead.
6. Testing. Set the UA in functional tests — no mocking needed:
$client->request('GET', '/', [], [], [
'HTTP_USER_AGENT' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ...',
]);
For unit tests, mock MobileDetectorInterface or alias the service:
# config/services_test.yaml
services:
MobileDetectBundle\DeviceDetector\MobileDetectorInterface: '@App\Tests\Fake\AlwaysMobileDetector'
Keep a small set of canned UAs (iPhone, iPad, Android tablet, Android phone, desktop Chrome) in a fixture file and use them everywhere — otherwise you get flaky assertions from hand-typed UA strings.
m.example.com serves the same Symfony app with the same config, verify the listener doesn't bounce the user again. Guard in your web server / front controller by only enabling the redirect config on the desktop host, or lean on action: allow plus the view cookie.POST to a desktop URL to redirect mid-flight — redirecting a POST loses the body. Redirect before forms, not after.Vary: User-Agent (and Vary: Cookie once view switching is on) or your CDN/Varnish will serve a mobile page to desktop users. Set this explicitly on the redirect response and on any cached page whose template depends on is_mobile(). Better still: keep device-dependent markup out of the anonymous, long-lived cache and load it via ESI/JSON.Mobile; some Android phones include it. Test isTablet() carefully — "not mobile and not desktop" is a common source of surprise. Client Hints (Sec-CH-UA-Mobile) are not consulted by this bundle; if you need them, decorate the detector.cookie_expire is an integer whose unit has differed across versions (minutes in current docs). Confirm in vendor/tattali/mobile-detect-bundle before shipping — and remember the cookie only exists once switch_device_view is enabled; a getDeviceView() call without it always returns the full view.save_referrer is what makes "back to full site" work. Turn it on if you expose a switcher; without it the user is stranded on the mobile view.host vs. full URL. host: expects the target host; if you need a path prefix, check whether your version accepts a full URL. A missing scheme or a trailing slash is the most common silent misconfiguration — dump the resolved config with bin/console debug:config mobile_detect after every change.mobile_detect to add your own isRobot()-style methods (handy for excluding bots from redirects — bots following a 302 to a mobile host is a real SEO problem), or provide your own detector via the bundle's service config key so the Twig helpers use your implementation.composer update away from a broken Twig function name. Pin the major, and write one smoke test per Twig helper you use (is_mobile, is_tablet, is_full_view) so an upgrade failure shows up in CI rather than in production.How can I help you explore Laravel packages today?