Files
Phraseanet/lib/Alchemy/Phrasea/Webhook/WebhookInvoker.php
2016-10-13 14:05:53 +02:00

293 lines
9.4 KiB
PHP

<?php
/*
* This file is part of phrasea-4.1.
*
* (c) Alchemy <info@alchemy.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Webhook;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Entities\WebhookEventPayload;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator;
use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventPayloadRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventRepository;
use Guzzle\Common\Event;
use Guzzle\Http\Client;
use Guzzle\Http\Message\EntityEnclosingRequestInterface;
use Guzzle\Http\Message\Request;
use Guzzle\Http\Message\Response;
use Guzzle\Plugin\Backoff\BackoffPlugin;
use Guzzle\Plugin\Backoff\CallbackBackoffStrategy;
use Guzzle\Plugin\Backoff\CurlBackoffStrategy;
use Guzzle\Plugin\Backoff\TruncatedBackoffStrategy;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* Class WebhookInvoker invokes remote endpoints with webhook event data
* @package Alchemy\Phrasea\Webhook
*/
class WebhookInvoker implements LoggerAwareInterface
{
/**
* @var ApiApplicationRepository
*/
private $applicationRepository;
/**
* @var WebhookEventDeliveryManipulator
*/
private $eventDeliveryManipulator;
/**
* @var WebhookEventDeliveryRepository
*/
private $eventDeliveryRepository;
/**
* @var Client
*/
private $client;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var EventProcessorFactory
*/
private $processorFactory;
/**
* @var WebhookEventRepository
*/
private $eventRepository;
/**
* @var WebhookEventManipulator
*/
private $eventManipulator;
/**
* @var WebhookEventPayloadRepository
*/
private $eventPayloadRepository;
/**
* @param ApiApplicationRepository $applicationRepository
* @param EventProcessorFactory $processorFactory
* @param WebhookEventRepository $eventRepository
* @param WebhookEventManipulator $eventManipulator
* @param WebhookEventDeliveryRepository $eventDeliveryRepository
* @param WebhookEventDeliveryManipulator $eventDeliveryManipulator
* @param WebhookEventPayloadRepository $eventPayloadRepository
* @param Client $client
*
* @todo Extract classes to reduce number of required dependencies
*/
public function __construct(
ApiApplicationRepository $applicationRepository,
EventProcessorFactory $processorFactory,
WebhookEventRepository $eventRepository,
WebhookEventManipulator $eventManipulator,
WebhookEventDeliveryRepository $eventDeliveryRepository,
WebhookEventDeliveryManipulator $eventDeliveryManipulator,
WebhookEventPayloadRepository $eventPayloadRepository,
Client $client = null
) {
$this->applicationRepository = $applicationRepository;
$this->processorFactory = $processorFactory;
$this->eventRepository = $eventRepository;
$this->eventManipulator = $eventManipulator;
$this->eventDeliveryManipulator = $eventDeliveryManipulator;
$this->eventDeliveryRepository = $eventDeliveryRepository;
$this->eventPayloadRepository = $eventPayloadRepository;
$this->client = $client ?: new Client();
$this->logger = new NullLogger();
$this->configureClient();
}
/**
* Sets a logger instance on the object.
*
* @param LoggerInterface $logger
*
* @return null
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function invoke(WebhookEvent $event)
{
$this->doInvoke($event, $this->applicationRepository->findWithDefinedWebhookCallback());
}
public function invokeUnprocessedEvents()
{
$targetApplications = $this->applicationRepository->findWithDefinedWebhookCallback();
foreach ($this->eventRepository->getUnprocessedEventIterator() as $row) {
/** @var WebhookEvent $event */
$event = $row[0];
$this->doInvoke($event, $targetApplications);
}
}
/**
* @param WebhookEvent $event
* @param ApiApplication[] $targets
*/
private function doInvoke(WebhookEvent $event, array $targets)
{
$this->eventManipulator->processed($event);
$this->logger->info(sprintf('Processing event "%s" with id %d', $event->getName(), $event->getId()));
// send requests
$this->doHttpDelivery($event, $targets);
}
private function configureClient()
{
$this->client->getEventDispatcher()->addListener('request.error', function (Event $event) {
// Override guzzle default behavior of throwing exceptions
// when 4xx & 5xx responses are encountered
$event->stopPropagation();
}, -254);
// Set callback which logs success or failure
$subscriber = new CallbackBackoffStrategy(function ($retries, Request $request, $response, $e) {
$retry = true;
if ($response && (null !== $deliverId = parse_url($request->getUrl(), PHP_URL_FRAGMENT))) {
$delivery = $this->eventDeliveryRepository->find($deliverId);
$logContext = ['host' => $request->getHost()];
if ($response->isSuccessful()) {
$this->eventDeliveryManipulator->deliverySuccess($delivery);
$logType = 'info';
$logEntry = sprintf('Deliver success event "%d:%s" for app "%s"',
$delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(),
$delivery->getThirdPartyApplication()->getName()
);
$retry = false;
} else {
$this->eventDeliveryManipulator->deliveryFailure($delivery);
$logType = 'error';
$logEntry = sprintf('Deliver failure event "%d:%s" for app "%s"',
$delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(),
$delivery->getThirdPartyApplication()->getName()
);
}
$this->logger->log($logType, $logEntry, $logContext);
return $retry;
}
}, true, new CurlBackoffStrategy());
// Set max retries
$subscriber = new TruncatedBackoffStrategy(WebhookEventDelivery::MAX_DELIVERY_TRIES, $subscriber);
$subscriber = new BackoffPlugin($subscriber);
$this->client->addSubscriber($subscriber);
}
/**
* @param WebhookEvent $event
* @param ApiApplication[] $targets
*/
private function doHttpDelivery(
WebhookEvent $event,
array $targets
) {
if (count($targets) === 0) {
$this->logger->info(sprintf('No applications defined to listen for webhook events'));
return;
}
// Format event data
$eventProcessor = $this->processorFactory->getProcessor($event);
$data = $eventProcessor->process($event);
foreach ($targets as $thirdPartyApplication) {
$delivery = $this->eventDeliveryManipulator->create($thirdPartyApplication, $event);
// Append delivery id as url anchor
$uniqueUrl = $this->buildUrl($thirdPartyApplication, $delivery);
// Create http request with data as request body
$request = $this->client->createRequest('POST', $uniqueUrl, [
'Content-Type' => 'application/vnd.phraseanet.event+json'
], json_encode($data));
$requestBody = $request instanceof EntityEnclosingRequestInterface ? $request->getBody() : '';
try {
$response = $request->send();
$responseBody = $response->getBody(true);
$statusCode = $response->getStatusCode();
$headers = $this->extractResponseHeaders($response);
}
catch (\Exception $exception) {
$responseBody = $exception->getMessage();
$statusCode = -1;
$headers = '';
}
$deliveryPayload = new WebhookEventPayload(
$delivery,
$requestBody,
$responseBody,
$statusCode,
$headers
);
$this->eventPayloadRepository->save($deliveryPayload);
}
}
/**
* @param ApiApplication $application
* @param WebhookEventDelivery $delivery
* @return string
*/
private function buildUrl(ApiApplication $application, WebhookEventDelivery $delivery)
{
return sprintf('%s#%s', $application->getWebhookUrl(), $delivery->getId());
}
private function extractResponseHeaders(Response $response)
{
$headerCollection = $response->getHeaders()->toArray();
$headers = '';
foreach ($headerCollection as $name => $value) {
$headers .= sprintf('%s: %s', $name, $value) . PHP_EOL;
}
return trim($headers);
}
}