Skip to content

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:

  • desktop
  • mobile
  • tablet
  • capacitor

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:

php
DeviceUtil::isMobile();
DeviceUtil::isTablet();
DeviceUtil::isDesktop();
DeviceUtil::isCapacitor();
DeviceUtil::getDeviceType();

Ela tambem aceita um objeto Request opcional, o que facilita bastante os testes:

php
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): string

Fazendo 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:

php
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 Request ignoram 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:

php
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:

  1. um header customizado X-Capacitor-App
  2. um cookie capacitor_app=true
  3. heuristicas restritivas de WebView
php
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:

php
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:

  • capacitor sobrescreve categorias de navegador
  • tablet sobrescreve mobile
  • 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:

blade
<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:

php
->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:

php
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.

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:

php
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.