Skip to content

Commit

Permalink
Setup ClientHints (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
renanbr authored Apr 15, 2022
1 parent c117c2b commit 1519354
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/phpunit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
- { dd: '^5.0', sf: '^6.0', php: '8.0' }
- { dd: '5.0.*', sf: '6.0.*', php: '8.1' }
- { dd: '^5.0', sf: '^6.0', php: '8.1' }
- { dd: '6.0.*', sf: '6.0.*', php: '8.1' }
- { dd: '^6.0', sf: '^6.0', php: '8.1' }

steps:

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ cs-check: ## Check for coding standards violations
.PHONY: cs-check

phpunit: ## Run PHPUnit tests
php -d pcov.enabled=1 ./vendor/bin/phpunit --testdox --coverage-text --verbose
php -d pcov.enabled=1 ./vendor/bin/phpunit --testdox --coverage-text --coverage-html var/phpunit/code-coverage-html --verbose
.PHONY: phpunit

## Fixers
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

The `AcsiomaticDeviceDetectorBundle` provides integration of the [DeviceDetector] library into the [Symfony] framework.

> [DeviceDetector] is a Universal Device Detection library that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, feed readers, media players, PIMs, ...), operating systems, brands and models.
> [DeviceDetector] is a Universal Device Detection library that parses User Agents and Browser Client Hints to detect devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, feed readers, media players, PIMs, ...), operating systems, brands and models.
>
> From https://github.com/matomo-org/device-detector
Expand All @@ -20,7 +20,7 @@ This bundle provides the [DeviceDetector class] as a [service], and a [Twig glob

## Installation

This bundle is compatible with [Symfony] from `3.4` to `6.x`, and [DeviceDetector] from `4.0` to `5.x`.
This bundle is compatible with [Symfony] from `3.4` to `6.x`, and [DeviceDetector] from `4.0` to `6.x`.

You can install the bundle using Symfony Flex:

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@
"require": {
"php": "^7.2 || ^8.0",
"friendsofphp/proxy-manager-lts": "^1.0",
"matomo/device-detector": "^4.0 || ^5.0",
"matomo/device-detector": "^4.0 || ^5.0 || ^6.0",
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.2",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"rector/rector": "^0.12.13",
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0"
Expand Down
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ parameters:
}
'''
tmpDir: var/phpstan

includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
11 changes: 11 additions & 0 deletions src/Contracts/ClientHintsFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Acsiomatic\DeviceDetectorBundle\Contracts;

use DeviceDetector\ClientHints;
use Symfony\Component\HttpFoundation\Request;

interface ClientHintsFactoryInterface
{
public function createClientHintsFromRequest(Request $request): ClientHints;
}
3 changes: 3 additions & 0 deletions src/Contracts/DeviceDetectorFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
namespace Acsiomatic\DeviceDetectorBundle\Contracts;

use DeviceDetector\DeviceDetector;
use Symfony\Component\HttpFoundation\Request;

interface DeviceDetectorFactoryInterface
{
public function createDeviceDetector(): DeviceDetector;

public function createDeviceDetectorFromRequest(Request $request): DeviceDetector;
}
18 changes: 16 additions & 2 deletions src/DependencyInjection/AcsiomaticDeviceDetectorExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Acsiomatic\DeviceDetectorBundle\DependencyInjection;

use Acsiomatic\DeviceDetectorBundle\CacheWarmer\ProxyCacheWarmer;
use Acsiomatic\DeviceDetectorBundle\Contracts\ClientHintsFactoryInterface;
use Acsiomatic\DeviceDetectorBundle\Contracts\DeviceDetectorFactoryInterface;
use Acsiomatic\DeviceDetectorBundle\Factory\ClientHintsFactory;
use Acsiomatic\DeviceDetectorBundle\Factory\DeviceDetectorFactory;
use Acsiomatic\DeviceDetectorBundle\Factory\DeviceDetectorProxyFactory;
use Acsiomatic\DeviceDetectorBundle\Twig\TwigExtension;
Expand Down Expand Up @@ -50,7 +52,8 @@ public function load(array $configs, ContainerBuilder $container): void

$this->setupParsers($container);
$this->setupProxy($container, $config);
$this->setupFactory($container, $config);
$this->setupClientHintsFactory($container, $config);
$this->setupDeviceDetectorFactory($container, $config);
$this->setupDeviceDetector($container);
$this->setupTwig($container, $config);
}
Expand Down Expand Up @@ -84,14 +87,25 @@ private function setupProxy(ContainerBuilder $container, array $config): void
/**
* @param BundleConfigArray $config
*/
private function setupFactory(ContainerBuilder $container, array $config): void
private function setupClientHintsFactory(ContainerBuilder $container, array $config): void
{
$container
->register(ClientHintsFactoryInterface::class, ClientHintsFactory::class)
->setPublic(false);
}

/**
* @param BundleConfigArray $config
*/
private function setupDeviceDetectorFactory(ContainerBuilder $container, array $config): void
{
$container
->register(DeviceDetectorFactoryInterface::class, DeviceDetectorFactory::class)
->setPublic(false)
->setArguments([
$config['bot']['skip_detection'],
$config['bot']['discard_information'],
new Reference(ClientHintsFactoryInterface::class),
$config['cache']['pool'] !== null ? new Reference($config['cache']['pool']) : null,
$config['auto_parse'] ? new Reference(DeviceDetectorProxyFactory::class) : null,
new TaggedIteratorArgument(self::BOT_PARSER_TAG),
Expand Down
23 changes: 23 additions & 0 deletions src/Factory/ClientHintsFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Acsiomatic\DeviceDetectorBundle\Factory;

use Acsiomatic\DeviceDetectorBundle\Contracts\ClientHintsFactoryInterface;
use DeviceDetector\ClientHints;
use Symfony\Component\HttpFoundation\Request;

/**
* @internal
*/
final class ClientHintsFactory implements ClientHintsFactoryInterface
{
public function createClientHintsFromRequest(Request $request): ClientHints
{
$headers = [];
foreach ($request->headers->keys() as $key) {
$headers[$key] = $request->headers->get($key);
}

return ClientHints::factory($headers);
}
}
32 changes: 27 additions & 5 deletions src/Factory/DeviceDetectorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

namespace Acsiomatic\DeviceDetectorBundle\Factory;

use Acsiomatic\DeviceDetectorBundle\Contracts\ClientHintsFactoryInterface;
use Acsiomatic\DeviceDetectorBundle\Contracts\DeviceDetectorFactoryInterface;
use DeviceDetector\Cache\PSR6Bridge;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;
use DeviceDetector\Parser\AbstractBotParser;
use DeviceDetector\Parser\Client\AbstractClientParser;
use DeviceDetector\Parser\Device\AbstractDeviceParser;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
Expand All @@ -26,6 +29,11 @@ final class DeviceDetectorFactory implements DeviceDetectorFactoryInterface
*/
private $discardBotInformation = false;

/**
* @var ClientHintsFactoryInterface
*/
private $clientHintsFactory;

/**
* @var CacheItemPoolInterface|null
*/
Expand Down Expand Up @@ -59,6 +67,7 @@ final class DeviceDetectorFactory implements DeviceDetectorFactoryInterface
public function __construct(
bool $skipBotDetection,
bool $discardBotInformation,
ClientHintsFactoryInterface $clientHintsFactory,
?CacheItemPoolInterface $cache,
?DeviceDetectorProxyFactory $proxyFactory,
iterable $botParsers,
Expand All @@ -67,6 +76,7 @@ public function __construct(
) {
$this->skipBotDetection = $skipBotDetection;
$this->discardBotInformation = $discardBotInformation;
$this->clientHintsFactory = $clientHintsFactory;
$this->cache = $cache;
$this->proxyFactory = $proxyFactory;
$this->botParsers = $botParsers;
Expand Down Expand Up @@ -102,21 +112,33 @@ public function createDeviceDetector(): DeviceDetector
return $detector;
}

public function createDeviceDetectorFromRequest(Request $request): DeviceDetector
{
$detector = $this->createDeviceDetector();

$userAgent = $request->headers->get('user-agent', '');
$detector->setUserAgent((string) $userAgent);

if (class_exists(ClientHints::class) && method_exists($detector, 'setClientHints')) {
$clientHints = $this->clientHintsFactory->createClientHintsFromRequest($request);
$detector->setClientHints($clientHints);
}

return $detector;
}

public static function createDeviceDetectorFromRequestStack(
DeviceDetectorFactoryInterface $factory,
RequestStack $requestStack
): DeviceDetector {
$detector = $factory->createDeviceDetector();

$request = method_exists($requestStack, 'getMasterRequest')
? $requestStack->getMasterRequest() // BC for Symfony 5.2 and older
: $requestStack->getMainRequest();

if ($request) {
$userAgent = $request->headers->get('user-agent', '');
$detector->setUserAgent((string) $userAgent);
return $factory->createDeviceDetectorFromRequest($request);
}

return $detector;
return $factory->createDeviceDetector();
}
}
80 changes: 80 additions & 0 deletions tests/ClientHintsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Acsiomatic\DeviceDetectorBundle\Tests;

use Acsiomatic\DeviceDetectorBundle\AcsiomaticDeviceDetectorBundle;
use Acsiomatic\DeviceDetectorBundle\Contracts\ClientHintsFactoryInterface;
use Acsiomatic\DeviceDetectorBundle\Tests\Util\Compiler\CompilerPassFactory;
use Acsiomatic\DeviceDetectorBundle\Tests\Util\HttpKernel\Kernel;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

final class ClientHintsTest extends TestCase
{
protected function setUp(): void
{
if (!method_exists(DeviceDetector::class, 'getClientHints')) {
static::markTestSkipped('The MySQLi extension is not available.');
}
}

public function testDeviceDetectorReceivesClientHints(): void
{
$kernel = new Kernel('test', true);
$kernel->appendBundle(new FrameworkBundle());
$kernel->appendBundle(new AcsiomaticDeviceDetectorBundle());
$kernel->appendExtensionConfiguration('framework', ['test' => true, 'secret' => '53CR37']);
$kernel->appendCompilerPass(CompilerPassFactory::createPublicAlias('device_detector.public', DeviceDetector::class));
$kernel->appendCompilerPass(CompilerPassFactory::createPublicAlias('request_stack.public', RequestStack::class));

$kernel->boot();

$request = new Request();
$request->headers->set('Sec-CH-UA-Platform', 'Windows');
$request->headers->set('Sec-CH-UA-Platform-Version', '10.0.0');

/** @var RequestStack $requestStack */
$requestStack = $kernel->getContainer()->get('request_stack.public');
$requestStack->push($request);

/** @var DeviceDetector $deviceDetector */
$deviceDetector = $kernel->getContainer()->get('device_detector.public');
$clientHints = $deviceDetector->getClientHints();

static::assertInstanceOf(ClientHints::class, $clientHints);
static::assertSame('Windows', $clientHints->getOperatingSystem());
static::assertSame('10.0.0', $clientHints->getOperatingSystemVersion());
}

public function testFactoryCreatesClientHintsFromRequest(): void
{
$kernel = new Kernel('test', true);
$kernel->appendBundle(new FrameworkBundle());
$kernel->appendBundle(new AcsiomaticDeviceDetectorBundle());
$kernel->appendExtensionConfiguration('framework', ['test' => true, 'secret' => '53CR37']);
$kernel->appendCompilerPass(
CompilerPassFactory::createPublicAlias(
'client_hints_factory.public',
ClientHintsFactoryInterface::class
)
);

$kernel->boot();

/** @var ClientHintsFactoryInterface $clientHintsFactory */
$clientHintsFactory = $kernel->getContainer()->get('client_hints_factory.public');

$request = new Request();
$request->headers->set('Sec-CH-UA-Platform', 'Windows');
$request->headers->set('Sec-CH-UA-Platform-Version', '10.0.0');

$clientHints = $clientHintsFactory->createClientHintsFromRequest($request);

static::assertSame('Windows', $clientHints->getOperatingSystem());
static::assertSame('10.0.0', $clientHints->getOperatingSystemVersion());
}
}

0 comments on commit 1519354

Please sign in to comment.