How to generate Absolute URL from Early/Auto loaded Symfony 3 router - php

I am writing Guards to handle OAuth for my Symfony 3 application.
As part of it, in one of my services, I need to generate an absolute URL to send to Twitter as the Callback URL.
#services.yml
...
app.twitter_client:
class: MyApiBundle\Services\TwitterClient
arguments:
- %twitter_client_id%
- %twitter_client_secret%
- connect_twitter_check
- '#request_stack'
- '#router'
- '#logger'
app.twitter_authenticator:
class: MyApiBundle\Security\TwitterAuthenticator
arguments:
- '#app.twitter_client'
- '#logger'
The logger is temporary for debugging.
#TwitterClient.php service
...
use Monolog\Logger;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
class TwitterClient
{
/**
* TwitterClient constructor.
* #param string $identifier
* #param string $secret
* #param string $callbackUrl
* #param RequestStack $requestStack
* #param RouterInterface $router
* #param Logger $logger
*/
public function __construct(
string $identifier,
string $secret,
string $callbackUrl,
RequestStack $requestStack,
RouterInterface $router,
Logger $logger
) {
$callbackUrl = $router->generate($callbackUrl, [], RouterInterface::ABSOLUTE_URL);
$logger->info($callbackUrl);
...
}
And when it outputs the $callbackUrl to the logs, it just says localhost, even though I am accessing it using a different URL.
http://localhost/connect/twitter/check
But if I do the same from my a controller, it outputs the full proper URL.
#TwitterController.php
...
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
class TwitterController extends Controller
{
/**
* #Route("/connect/twitter")
* #return RedirectResponse
*/
public function connectAction()
{
/** #var RouterInterface $router */
$router = $this->container->get('router');
$callbackUrl = $router->generate('connect_twitter_check', [], RouterInterface::ABSOLUTE_URL);
/** #var Logger $logger */
$logger = $this->container->get('logger');
$this->container->get('logger')
->info($callbackUrl);
...
This outputs:
https://dev.myapi.com:8082/app_dev.php/connect/twitter/check
The dev.myapi.com domain is set up in my hosts file and points to localhost, to make it easier to differentiate between my local apps and also to make it easier to integrate with OAuth services.

I managed to solve this by setting the Router context.
I already loaded in the RequestStack to access the user's IP, so I used it to set the context of the Route and then the router worked as expected.
I ran into problems trying to use the RequestStack in the constructor, so I ended up refactoring it so that it used the RequestStack as late as possible.
class TwitterClient
{
private $identifier;
private $secret;
private $callbackUrl;
private $requestStack;
private $router;
private $server;
/**
* TwitterClient constructor.
*
* #param string $identifier
* #param string $secret
* #param string $callbackUrl
* #param RequestStack $requestStack
* #param RouterInterface $router
*/
public function __construct(
string $identifier,
string $secret,
string $callbackUrl,
RequestStack $requestStack,
RouterInterface $router
) {
$this->identifier = $identifier;
$this->secret = $secret;
$this->callbackUrl = $callbackUrl;
$this->requestStack = $requestStack;
$this->router = $router;
}
/**
* Wait until the last moment to create the Twitter object to make sure that the requestStack is loaded.
*
* #return \League\OAuth1\Client\Server\Twitter
*/
private function getServer()
{
if (!$this->server) {
$context = new RequestContext();
$context->fromRequest($this->requestStack->getCurrentRequest());
$this->router->setContext($context);
$callbackUrl = $this->router->generate($this->callbackUrl, [], RouterInterface::ABSOLUTE_URL);
$this->server = new Twitter(
[
'identifier' => $this->identifier,
'secret' => $this->secret,
'callback_uri' => $callbackUrl,
]
);
}
return $this->server;
}
...
}

Related

How to handle routing with locale in Symfony 4.3 KernelTestCase?

I need to test (PHPUnit) a service in Symfony 4.3 application and that service depends on the framework. It uses Translator and Router and it also depends on User. This means I want to write a KernelTestCase with User (done). The problem is the Router fails because the Routes need _locale. How can I address the issue?
1) App\Tests\blahblah\MenuFactoryTest::testMenuItemsByRoles with data set "ROLE_ADMIN" (array('ROLE_ADMIN'), array(array('Menu Label 1', 'Menu Label 2')))
Symfony\Component\Routing\Exception\MissingMandatoryParametersException: Some mandatory parameters are missing ("_locale") to generate a URL for route "default_dashboard".
class MenuFactoryTest extends KernelTestCase
{
use LogUserTrait; // my trait allowing to emulate a user with particular roles
/** #var MenuFactory */
private $menuFactory;
protected function setUp(): void
{
static::bootKernel();
$this->menuFactory = static::$container->get(MenuFactory::class);
}
// ...
/**
* #param array $roles
* #param array $menuLabels
*
* #dataProvider provideMenuItemsByRoles
*/
public function testMenuItemsByRoles(array $roles, array $menuLabels): void
{
$this->logIn($roles);
$this->assertMenuItemsHaveLabels(
$menuLabels,
$this->menuFactory->getMenuItems()
);
}
// ...
}
class MenuFactory implements MenuFactoryInterface
{
/** #var RouterInterface */
private $router;
/** #var AuthorizationCheckerInterface */
private $securityChecker;
/** #var TranslatorInterface */
private $translator;
public function __construct(
RouterInterface $router,
AuthorizationCheckerInterface $securityChecker,
TranslatorInterface $translator
) {
$this->router = $router;
$this->securityChecker = $securityChecker;
$this->translator = $translator;
}
public function getMenuItems(string $appMenuName = null): array
{
$menuItems = [];
$dashboardMenu = new DefaultMenuItem(
$this->translator->trans('menu.dashboard'),
$this->router->generate('default_dashboard'),
'fa fa-dashboard'
);
$menuItems[] = $dashboardMenu;
// ...
return $menuItems;
}
}

Symfony 3 captcha bundle without forms

I need to add a simple captcha to my Symfony login form and currently I am searching for correct bundle for it. I dont need any API/ajax js support, just a bundle which generates an image on the server and then performs user input validation.
Moreover I am not using forms in my project, so I need to render an image in my loginAction and after that perform manual validation somewhere.
Firstly I tried captcha.com bundle, but as far as I understand it is not free and also it required login to git.captcha.com when performing composer require ...
After that I tried to use Gregwar/CaptchaBundle but its docs contain only examples with form while I need something without them. Is there any way to use Gregwar/CaptchaBundle without forms?
Any advice would be welcome, thank you.
I've used Gregwar/CaptchaBundle like this:
namespace AppBundle\Security\Captcha;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Templating\EngineInterface;
use Gregwar\CaptchaBundle\Generator\CaptchaGenerator as BaseCaptchaGenerator;
class CaptchaGenerator
{
/**
* #var EngineInterface
*/
private $templating;
/**
* #var BaseCaptchaGenerator
*/
private $generator;
/**
* #var Session
*/
private $session;
/**
* #var string
*/
private $sessionKey;
/**
* #var array
*/
private $options;
public function __construct(EngineInterface $templating, BaseCaptchaGenerator $generator, SessionInterface $session, $sessionKey, array $options)
{
$this->templating = $templating;
$this->generator = $generator;
$this->session = $session;
$this->sessionKey = $sessionKey;
$this->options = $options;
}
/**
* #return string
*/
public function generate()
{
$options = $this->options;
$code = $this->generator->getCaptchaCode($options);
$this->session->set($this->sessionKey, $options['phrase']);
return $this->templating->render('AppBundle:My/Security:captcha.html.twig', array(
'code' => $code,
'width' => $options['width'],
'height' => $options['height'],
));
}
}
namespace AppBundle\Security\Captcha;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class CaptchaValidator
{
/**
* #var Session
*/
private $session;
/**
* #var string
*/
private $sessionKey;
public function __construct(SessionInterface $session, $sessionKey)
{
$this->session = $session;
$this->sessionKey = $sessionKey;
}
/**
* #param string $value
* #return bool
*/
public function validate($value)
{
return $this->session->get($this->sessionKey, null) === $value;
}
}
{# AppBundle:My/Security:captcha.html.twig #}
<img class="captcha_image" src="{{ code }}" alt="" title="captcha" width="{{ width }}" height="{{ height }}" />
# services.yaml
parameters:
app.security.captcha.security_key: captcha
services:
AppBundle\Security\Captcha\CaptchaGenerator:
lazy: true
arguments:
$sessionKey: '%app.security.captcha.security_key%'
$options: '%gregwar_captcha.config%'
AppBundle\Security\Captcha\CaptchaValidator:
arguments:
$sessionKey: '%app.security.captcha.security_key%'
then validate value in AppBundle\Security\FormAuthenticator
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$this->captchaValidator->validate($token->getCaptchaValue())) {
throw new CustomUserMessageAuthenticationException('security.captcha');
}
// ...
}

Twig error on WebProfiler with Doctrine filter enable

I have a strange error with Twig and the WebProfiler when I enable a Doctrine filter.
request.CRITICAL: Uncaught PHP Exception Twig_Error_Runtime: "An exception has been thrown
during the rendering of a template ("Error when rendering "http://community.localhost:8000/
_profiler/e94abf?community_subdomain=community&panel=request" (Status code is 404).")." at
/../vendor/symfony/symfony/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/
layout.html.twig line 103
This {{ render(path('_profiler_search_bar', request.query.all)) }} causes the error.
My doctrine filter allows to add filter constraint on some classes (multi tenant app with dynamic subdomains)
<?php
namespace AppBundle\Group\Community;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
/**
* Class CommunityAwareFilter
*/
class CommunityAwareFilter extends SQLFilter
{
/**
* Gets the SQL query part to add to a query.
*
* #param ClassMetadata $targetEntity
* #param string $targetTableAlias
*
* #return string The constraint SQL if there is available, empty string otherwise.
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (!$targetEntity->reflClass->implementsInterface(CommunityAwareInterface::class)) {
return '';
}
return sprintf('%s.community_id = %s', $targetTableAlias, $this->getParameter('communityId')); // <-- error
// return ''; <-- no error
}
}
I have also extended Symfony Router to add subdomain placeholder automatically in routing.
Do you have any idea what can cause this ?
UPDATE
<?php
namespace AppBundle\Routing;
use AppBundle\Group\Community\CommunityResolver;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
class Router implements RouterInterface
{
/**
* #var BaseRouter
*/
private $router;
/**
* #var RequestStack
*/
private $request;
/**
* #var CommunityResolver
*/
private $communityResolver;
/**
* Router constructor.
*
* #param BaseRouter $router
* #param RequestStack $request
* #param CommunityResolver $communityResolver
*/
public function __construct(BaseRouter $router, RequestStack $request, CommunityResolver $communityResolver)
{
$this->router = $router;
$this->request = $request;
$this->communityResolver = $communityResolver;
}
/**
* Sets the request context.
*
* #param RequestContext $context The context
*/
public function setContext(RequestContext $context)
{
$this->router->setContext($context);
}
/**
* Gets the request context.
*
* #return RequestContext The context
*/
public function getContext()
{
return $this->router->getContext();
}
/**
* Gets the RouteCollection instance associated with this Router.
*
* #return RouteCollection A RouteCollection instance
*/
public function getRouteCollection()
{
return $this->router->getRouteCollection();
}
/**
* Tries to match a URL path with a set of routes.
*
* If the matcher can not find information, it must throw one of the exceptions documented
* below.
*
* #param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded)
*
* #return array An array of parameters
*
* #throws ResourceNotFoundException If the resource could not be found
* #throws MethodNotAllowedException If the resource was found but the request method is not allowed
*/
public function match($pathinfo)
{
return $this->router->match($pathinfo);
}
public function generate($name, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
if (null !== ($community = $this->communityResolver->getCommunity())) {
$parameters['community_subdomain'] = $community->getSubDomain();
}
return $this->router->generate($name, $parameters, $referenceType);
}
}
I found the solution, in fact I passed my "tenant" (here my "community") object in the Session like this (in a subscriber onKernelRequest)
if (null === ($session = $request->getSession())) {
$session = new Session();
$session->start();
$request->setSession($session);
}
$session->set('community', $community);
I changed to store this object in a service and it works. Maybe using the Session to store data is a bad practice.
I think your Symmfony Router override may cause the problem. Can you paste us the code ?

How to override bundled Doctrine repository in Symfony

I have an independent Symfony bundle (installed with Composer) with entities and repositories to share between my applications that connect same database.
Entities are attached to every applications using configuration (yml shown):
doctrine:
orm:
mappings:
acme:
type: annotation
dir: %kernel.root_dir%/../vendor/acme/entities/src/Entities
prefix: Acme\Entities
alias: Acme
Well, it was the easiest way to include external entities in application, but looks a bit ugly.
Whenever I get repository from entity manager:
$entityManager->getRepository('Acme:User');
I get either preconfigured repository (in entity configuration) or default Doctrine\ORM\EntityRepository.
Now I want to override bundled (or default) repository class for a single entity. Is there any chance to do it with some configuration/extension/etc?
I think, the best looking way is something like:
doctrine:
orm:
....:
Acme\Entities\User:
repositoryClass: My\Super\Repository
Or with tags:
my.super.repository:
class: My\Super\Repository
tags:
- { name: doctrine.custom.repository, entity: Acme\Entities\User }
You can use LoadClassMetadata event:
class LoadClassMetadataSubscriber implements EventSubscriber
{
/**
* #inheritdoc
*/
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata
];
}
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/**
* #var \Doctrine\ORM\Mapping\ClassMetadata $classMetadata
*/
$classMetadata = $eventArgs->getClassMetadata();
if ($classMetadata->getName() !== 'Acme\Entities\User') {
return;
}
$classMetadata->customRepositoryClassName = 'My\Super\Repository';
}
}
Doctrine Events
Entities are attached to every applications using configuration (yml shown):
Well, it was the easiest way to include external entities in application, but looks a bit ugly.
You can enable auto_mapping
Works for Doctrine versions <2.5
In addition to Artur Vesker answer I've found another way: override global repository_factory.
config.yml:
doctrine:
orm:
repository_factory: new.doctrine.repository_factory
services.yml:
new.doctrine.repository_factory:
class: My\Super\RepositoryFactory
Repository Factory:
namespace My\Super;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
class RepositoryFactory extends DefaultRepositoryFactory
{
/**
* #inheritdoc
*/
protected function createRepository(EntityManagerInterface $entityManager, $entityName)
{
if ($entityName === Acme\Entities\User::class) {
$metadata = $entityManager->getClassMetadata($entityName);
return new ApplicationRepository($entityManager, $metadata);
}
return parent::createRepository($entityManager, $entityName);
}
}
No doubt implementing LoadClassMetadataSubscriber is a better way.
With current symfony 5.3 and doctrine 2.9.5
In your configuration define the service and doctrine.orm.repository_factory:
doctrine:
orm:
#Replace repository factory
repository_factory: 'MyBundle\Factory\RepositoryFactory'
services:
MyBundle\Factory\RepositoryFactory:
arguments: [ '#router', '#translator', '%kernel.secret%' ]
Add you MyBundle/Factory/RepositoryFactory.php file:
<?php declare(strict_types=1);
namespace MyBundle\Factory;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* This factory is used to create default repository objects for entities at runtime.
*/
final class RepositoryFactory implements RepositoryFactoryInterface {
/**
* The list of EntityRepository instances
*
* #var ObjectRepository[]
*/
private $repositoryList = [];
/**
* The kernel secret
*
* #var string
*/
private $secret;
/**
* The RouterInterface instance
*
* #var RouterInterface
*/
private $router;
/**
* The TranslatorInterface instance
*
* #var TranslatorInterface
*/
private $translator;
/**
* Initializes a new RepositoryFactory instance
*
* #param RouterInterface $router The router instance
* #param TranslatorInterface $translator The TranslatorInterface instance
* #param string $secret The kernel secret
*/
public function __construct(RouterInterface $router, TranslatorInterface $translator, string $secret) {
//Set router
$this->router = $router;
//Set secret
$this->secret = $secret;
//Set translator
$this->translator = $translator;
}
/**
* {#inheritdoc}
*/
public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository {
//Set repository hash
$repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_hash($entityManager);
//With entity repository instance
if (isset($this->repositoryList[$repositoryHash])) {
//Return existing entity repository instance
return $this->repositoryList[$repositoryHash];
}
//Store and return created entity repository instance
return $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName);
}
/**
* Create a new repository instance for an entity class
*
* #param EntityManagerInterface $entityManager The EntityManager instance.
* #param string $entityName The name of the entity.
*/
private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository {
//Get class metadata
$metadata = $entityManager->getClassMetadata($entityName);
//Get repository class
$repositoryClass = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();
//Return repository class instance
//XXX: router, translator and secret arguments will be ignored by default
return new $repositoryClass($entityManager, $metadata, $this->router, $this->translator, $this->secret);
}
}
Then define your MyBundle/Repository/EntityRepository.php:
<?php declare(strict_types=1);
namespace MyBundle\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository as BaseEntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* EntityRepository
*
* {#inheritdoc}
*/
class EntityRepository extends BaseEntityRepository {
/**
* The RouterInterface instance
*
* #var RouterInterface
*/
protected RouterInterface $router;
/**
* The table keys array
*
* #var array
*/
protected array $tableKeys;
/**
* The table values array
*
* #var array
*/
protected array $tableValues;
/**
* The TranslatorInterface instance
*
* #var TranslatorInterface
*/
protected TranslatorInterface $translator;
/**
* The kernel secret
*
* #var string
*/
protected string $secret;
/**
* Initializes a new LocationRepository instance
*
* #param EntityManagerInterface $manager The EntityManagerInterface instance
* #param ClassMetadata $class The ClassMetadata instance
* #param RouterInterface $router The router instance
* #param TranslatorInterface $translator The TranslatorInterface instance
* #param string $secret The kernel secret
*/
public function __construct(EntityManagerInterface $manager, ClassMetadata $class, RouterInterface $router, TranslatorInterface $translator, string $secret) {
//Call parent constructor
parent::__construct($manager, $class);
//Set secret
$this->secret = $secret;
//Set router
$this->router = $router;
//Set slugger
$this->slugger = $slugger;
//Set translator
$this->translator = $translator;
//Get quote strategy
$qs = $manager->getConfiguration()->getQuoteStrategy();
$dp = $manager->getConnection()->getDatabasePlatform();
//Set quoted table names
//XXX: remember to place longer prefix before shorter to avoid strange replacings
$tables = [
'MyBundle:UserGroup' => $qs->getJoinTableName($manager->getClassMetadata('MyBundle:User')->getAssociationMapping('groups'), $manager->getClassMetadata('MyBundle:User'), $dp),
'MyBundle:Group' => $qs->getTableName($manager->getClassMetadata('MyBundle:Group'), $dp),
'MyBundle:User' => $qs->getTableName($manager->getClassMetadata('MyBundle:User'), $dp),
//XXX: Set limit used to workaround mariadb subselect optimization
':limit' => PHP_INT_MAX,
"\t" => '',
"\n" => ' '
];
//Set quoted table name keys
$this->tableKeys = array_keys($tables);
//Set quoted table name values
$this->tableValues = array_values($tables);
}
}
Then simply extend it in MyBundle/Repository/UserRepository.php:
<?php declare(strict_types=1);
namespace MyBundle\Repository;
/**
* UserRepository
*/
class UserRepository extends EntityRepository {
}

What design pattern to use for a time-measurement profiler service?

I have a symfony2 application. It abstracts a bunch of external APIs, all of them implementing an ExternalApiInterface.
Each ExternalApiInterface has a lot of methods, e.g. fetchFoo and fetchBar.
Now, I want to write a service that measures the time of each method call of an instance of an ExternalApiInterface.
My current thinking is to implement a StopWatchExternalApiDecorator, that wraps each method call. Yet this approach leads, in my understanding, to code duplication.
I think I am going to use the StopWatch component for the time measurement, yet this feels odd:
class StopWatchExternalApiDecorator implements ExternalApiInterface {
public function __construct(ExternalApiInterface $api, Stopwatch $stopWatch)
{
$this->api = $api;
$this->stopWatch = $stopWatch;
}
public function fetchBar() {
$this->stopWatch->start('fetchBar');
$this->api->fetchBar()
$this->stopWatch->stop('fetchBar');
}
public function fetchFoo() {
$this->stopWatch->start('fetchFoo');
$this->api->fetchFoo()
$this->stopWatch->stop('fetchFoo');
}
}
It seems like I am hurting the DNRY (do not repeat yourself) approach. Am I using the right pattern for this kind of problem, or is there something else more fit? More fit in the sense of: One place to do all the measurement, and no code duplication.
I also dislike of having to touch the decorator in case there will be a new method in the interface. In my mind, that should be independent.
i am thinking of some apis i worked on that use one generic function for calls and a method parameter
heres some very basic pseudocode
public function call($method = 'fetchBar',$params=array()){
$this->stopWatch->start($method);
$this->{"$method"}($params);
$this->stopWatch->stop($method);
}
private function fetchBar(){
echo "yo";
}
maybe that helps
I went with the decorator approach, just on a different level.
In my architecture, api service was using an HttpClientInterface, and each request was handled in the end with a call to doRequest. So there, the decorator made most sense without code duplication:
<?php
namespace Kopernikus\BookingService\Component\Http\Client;
use Kopernikus\BookingService\Component\Performance\PerformanceEntry;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* ProfileClientDecorator
**/
class ProfileClientDecorator implements HttpClientInterface
{
/**
* #var Stopwatch
*/
private $stopwatch;
/**
* #var HttpClientInterface
*/
private $client;
/**
* #var LoggerInterface
*/
private $logger;
/**
* ProfileClientDecorator constructor.
* #param HttpClientInterface $client
* #param Stopwatch $stopwatch
* #param LoggerInterface $logger
*/
public function __construct(HttpClientInterface $client, Stopwatch $stopwatch, LoggerInterface $logger)
{
$this->client = $client;
$this->stopwatch = $stopwatch;
$this->logger = $logger;
}
/**
* #param RequestInterface $request
*
* #return ResponseInterface
*/
public function doRequest(RequestInterface $request)
{
$method = $request->getMethod();
$response = $this->doMeasuredRequest($request, $method);
$performance = $this->getPerformance($method);
$this->logPerformance($performance);
return $response;
}
/**
* #param RequestInterface $request
* #param string $method
*
* #return ResponseInterface
*/
protected function doMeasuredRequest(RequestInterface $request, $method)
{
$this->stopwatch->start($method);
$response = $this->client->doRequest($request);
$this->stopwatch->stop($method);
return $response;
}
/**
* #param $method
* #return PerformanceEntry
*/
protected function getPerformance($method)
{
$event = $this->stopwatch->getEvent($method);
$duration = $event->getDuration();
return new PerformanceEntry($duration, $method);
}
/**
* #param PerformanceEntry $performance
*/
protected function logPerformance(PerformanceEntry $performance)
{
$context = [
'performance' => [
'duration_in_ms' => $performance->getDurationInMs(),
'request_name' => $performance->getRequestName(),
],
];
$this->logger->info(
"The request {$performance->getRequestName()} took {$performance->getDurationInMs()} ms",
$context
);
}
}
And in my services.yml:
performance_client_decorator:
class: Kopernikus\Component\Http\Client\ProfileClientDecorator
decorates: http.guzzle_client
arguments:
- #performance_client_decorator.inner
- #stopwatch
- #logger

Categories