Deteccao de Dispositivos no Laravel: Mobile, Tablet, Desktop e Capacitor com Heuristicas Conservadoras em PHP
Introducao
As vezes o responsive CSS basta. As vezes nao.
Se a sua aplicacao Laravel precisa esconder uma acao exclusiva de desktop, adaptar um layout do Filament ou distinguir um shell nativo em Capacitor de um navegador mobile comum, voce precisa de uma forma backend-friendly de classificar o request atual.
Este artigo mostra uma implementacao pratica de DeviceUtil em PHP que retorna um tipo canonico de dispositivo:
desktopmobiletabletcapacitor
A principal decisao de design aqui e ser conservador. Como a MDN observa em seu guia sobre browser detection using the user agent, user-agent sniffing e fragil, entao a estrategia mais segura e usar primeiro sinais mais fortes da aplicacao e deixar as heuristicas de UA como fallback.
Quando a Deteccao no Backend Vale a Pena
A deteccao do lado do servidor e util quando o backend precisa tomar uma decisao antes de o JavaScript rodar:
- renderizar blocos diferentes de navegacao ou layout
- desabilitar recursos que nao fazem sentido dentro do shell nativo
- marcar o documento HTML com um tipo estavel de dispositivo
- expor helpers de conveniencia para Blade, Livewire ou Filament
Ela e menos util para comportamento puramente visual que CSS e deteccao de capacidades no cliente conseguem resolver.
TIP
Se voce controla o cliente, prefira sinais explicitos em vez de adivinhar. Um header ou cookie customizado geralmente e mais confiavel do que interpretar um user agent.
Estrutura da Utilidade
A utilidade expoe metodos pequenos com responsabilidade unica:
DeviceUtil::isMobile();
DeviceUtil::isTablet();
DeviceUtil::isDesktop();
DeviceUtil::isCapacitor();
DeviceUtil::getDeviceType();Ela tambem aceita um objeto Request opcional, o que facilita bastante os testes:
public static function isMobile(?Request $request = null): bool
public static function isTablet(?Request $request = null): bool
public static function isDesktop(?Request $request = null): bool
public static function isCapacitor(?Request $request = null): bool
public static function getDeviceType(?Request $request = null): stringFazendo Cache do User-Agent por Request
Se varios componentes consultam o tipo de dispositivo durante o mesmo request, ler e interpretar o mesmo header repetidamente e desnecessario. Um pequeno cache estatico mantem a implementacao simples:
private static ?string $userAgent = null;
private static function getUserAgent(?Request $request = null): string
{
if ($request !== null) {
return $request->header('User-Agent') ?? '';
}
if (self::$userAgent === null) {
$req = RequestFacade::getFacadeRoot();
self::$userAgent = $req->header('User-Agent') ?? '';
}
return self::$userAgent;
}Esse padrao entrega duas propriedades uteis:
- instancias explicitas de
Requestignoram o cache, o que mantem os testes isolados - o fluxo normal da aplicacao le o header da facade apenas uma vez
Detectando Requests Mobile e Tablet
A utilidade usa expressoes regulares para uma deteccao ampla de mobile e um conjunto mais restrito para tablets:
public static function isMobile(?Request $request = null): bool
{
$userAgent = self::getUserAgent($request);
if (! $userAgent) {
return false;
}
$mobilePatterns = [
'/iPhone|iPod|iPad|Android|webOS|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i',
'/\\b(?:a(?:ndroid|vantgo)|b(?:lackberry|olt|o?ost)|cricket|docomo|hiptop|i(?:emobile|p[ao]d)|kitkat|m(?:ini|obi)|palm|(?:i|smart|windows )phone|symbian|up\\.(?:browser|link)|tablet(?: browser| pc)|(?:hp-|rim |sony )tablet|w(?:ebos|indows ce|os))/i',
];
foreach ($mobilePatterns as $pattern) {
if (preg_match($pattern, $userAgent)) {
return true;
}
}
return false;
}
public static function isTablet(?Request $request = null): bool
{
$userAgent = self::getUserAgent($request);
if (! $userAgent) {
return false;
}
$tabletPatterns = [
'/iPad|Android.*Tablet|Tablet.*Android|Kindle|Silk|Galaxy Tab/i',
];
foreach ($tabletPatterns as $pattern) {
if (preg_match($pattern, $userAgent)) {
return true;
}
}
return false;
}Existe um tradeoff intencional aqui: alguns tablets ainda vao parecer dispositivos mobile genericos se nao expuserem um marcador reconhecivel de tablet. Isso e melhor do que classificar trafego de desktop ou telefone de forma agressiva e incorreta.
Tornando a Deteccao de Capacitor Mais Confiavel
A parte mais interessante dessa utilidade e que ela nao depende primeiro do user agent. Ela usa tres camadas, em ordem da mais forte para a mais fraca:
- um header customizado
X-Capacitor-App - um cookie
capacitor_app=true - heuristicas restritivas de WebView
public static function isCapacitor(?Request $request = null): bool
{
$req = $request ?? RequestFacade::getFacadeRoot();
if ($req->header('X-Capacitor-App') === 'true') {
return true;
}
if ($req->cookie('capacitor_app') === 'true') {
return true;
}
$userAgent = self::getUserAgent($request);
if (! $userAgent) {
return false;
}
if (str_contains($userAgent, 'Android')) {
if (str_contains($userAgent, 'wv') && str_contains($userAgent, 'Version/')) {
return true;
}
}
if (str_contains($userAgent, 'iPhone') || str_contains($userAgent, 'iPad')) {
if (str_contains($userAgent, 'Capacitor') || str_contains($userAgent, 'Cordova')) {
return true;
}
$hasSafari = str_contains($userAgent, 'Safari/');
$hasVersion = str_contains($userAgent, 'Version/');
$hasMobile = str_contains($userAgent, 'Mobile/');
$hasChrome = str_contains($userAgent, 'CriOS');
$hasFirefox = str_contains($userAgent, 'FxiOS');
$hasEdge = str_contains($userAgent, 'EdgiOS');
if ($hasSafari && $hasMobile && ! $hasVersion && ! $hasChrome && ! $hasFirefox && ! $hasEdge) {
foreach (['Vivaldi', 'OPR', 'Opera', 'DuckDuckGo', 'Brave'] as $browser) {
if (str_contains($userAgent, $browser)) {
return false;
}
}
return true;
}
}
return false;
}Este e um bom exemplo de logica defensiva no backend:
- sinais controlados pela aplicacao vencem
- heuristicas de plataforma sao estreitas
- navegadores alternativos comuns sao excluidos explicitamente
Importante
Nao assuma que todo request iOS sem Version/ e um shell nativo. Restrinja a heuristica e continue excluindo assinaturas conhecidas de navegadores, senao os falsos positivos vao se acumular rapidamente.
Retornando Um Tipo Canonico de Dispositivo
Em vez de obrigar o restante da aplicacao a combinar varios checks booleanos, exponha um unico valor canonico:
public static function getDeviceType(?Request $request = null): string
{
if (self::isCapacitor($request)) {
return 'capacitor';
} elseif (self::isTablet($request)) {
return 'tablet';
} elseif (self::isMobile($request)) {
return 'mobile';
}
return 'desktop';
}A prioridade importa:
capacitorsobrescreve categorias de navegadortabletsobrescrevemobile- todo o resto cai em
desktop
Isso remove ambiguidade da logica de views e condicionais de template.
Usando em Views Laravel
Um padrao util e expor o resultado diretamente no elemento raiz do HTML:
<html data-device-type="{{ \App\Utils\DeviceUtil::getDeviceType() }}">Assim voce pode consumir esse valor em CSS ou JavaScript sem executar novamente a deteccao no cliente.
Tambem funciona bem para condicionais do lado do servidor:
->visible(fn () => ! DeviceUtil::isCapacitor())Isso e especialmente util em paineis Filament quando uma determinada acao nao deve aparecer dentro do shell nativo.
Testando os Casos Delicados
A utilidade se torna bem mais confiavel quando voce cobre amostras reais de user agent nos testes:
it('detecta Chrome no Windows como desktop', function () {
$request = createRequestWithHeaders(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
expect(DeviceUtil::isDesktop($request))->toBeTrue();
expect(DeviceUtil::getDeviceType($request))->toBe('desktop');
});
it('detecta iPhone Safari como mobile', function () {
$request = createRequestWithHeaders(
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) ' .
'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1'
);
expect(DeviceUtil::isMobile($request))->toBeTrue();
expect(DeviceUtil::isCapacitor($request))->toBeFalse();
});
it('detecta Capacitor por header customizado', function () {
$request = createRequestWithHeaders('Mozilla/5.0 (...)', 'true', null);
expect(DeviceUtil::isCapacitor($request))->toBeTrue();
expect(DeviceUtil::getDeviceType($request))->toBe('capacitor');
});Alguns casos merecem testes explicitos:
- navegadores desktop em Windows e macOS
- iPhone Safari versus iPhone WebView
- Android Chrome versus Android WebView
- tablets que tambem batem com padroes mobile
- user agents nulos, vazios e bots
Troubleshooting
Tablets Android estao sendo classificados como mobile
Isso pode acontecer se o user agent contiver Android, mas nao um marcador especifico de tablet como Tablet ou Galaxy Tab. Se a sua audiencia depende muito de tablets Android, adicione padroes baseados nos dispositivos que voce realmente suporta e cubra isso com testes.
Navegadores alternativos no iOS estao sendo marcados como Capacitor
Endureca a lista de exclusao. Assinaturas como CriOS, FxiOS, EdgiOS, OPR ou Brave devem continuar fora do grupo de app nativo.
Os testes influenciam uns aos outros
Limpe o estado estatico entre os testes:
beforeEach(function () {
DeviceUtil::clearCache();
});Isso reinicia o user agent armazenado em cache e qualquer valor fake usado na suite de testes.
Conclusao
Uma boa utilidade de deteccao de dispositivos nao precisa ser magica. Ela precisa ser explicita, previsivel e facil de testar.
Para aplicacoes Laravel, uma base forte e:
- fazer cache do user agent do request atual
- preferir headers e cookies customizados para shells nativos
- manter as heuristicas de UA conservadoras
- retornar um unico tipo canonico de dispositivo para o resto da aplicacao
Essa abordagem nao vai classificar perfeitamente todos os casos extremos, mas vai te dar uma utilidade que pode evoluir com seguranca conforme novos navegadores, tablets e comportamentos de WebView aparecerem.
