Add delivery results to application settings page

This commit is contained in:
Thibaud Fabre
2016-10-13 14:05:53 +02:00
parent 981e55f026
commit e77178d757
9 changed files with 341 additions and 19 deletions

View File

@@ -19,6 +19,7 @@ use Alchemy\Phrasea\Model\Manipulator\ApiOauthTokenManipulator;
use Alchemy\Phrasea\Model\Repositories\ApiAccountRepository; use Alchemy\Phrasea\Model\Repositories\ApiAccountRepository;
use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository; use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository;
use Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository; use Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -244,8 +245,12 @@ class DeveloperController extends Controller
throw new AccessDeniedHttpException(); throw new AccessDeniedHttpException();
} }
$deliveries = $this->getWebhookDeliveryRepository()
->findLastDeliveries($account->getApplication(), 10);
return $this->render('developers/application.html.twig', [ return $this->render('developers/application.html.twig', [
"application" => $application, "application" => $application,
"deliveries" => $deliveries,
"user" => $user, "user" => $user,
"token" => $token, "token" => $token,
]); ]);
@@ -298,4 +303,12 @@ class DeveloperController extends Controller
{ {
return $this->app['repo.api-applications']; return $this->app['repo.api-applications'];
} }
/**
* @return WebhookEventDeliveryRepository
*/
private function getWebhookDeliveryRepository()
{
return $this->app['webhook.delivery_repository'];
}
} }

View File

@@ -21,6 +21,10 @@ class WebhookServiceProvider implements ServiceProviderInterface
$this->createAlias($app, 'webhook.delivery_repository', 'repo.webhook-delivery'); $this->createAlias($app, 'webhook.delivery_repository', 'repo.webhook-delivery');
$this->createAlias($app, 'webhook.delivery_manipulator', 'manipulator.webhook-delivery'); $this->createAlias($app, 'webhook.delivery_manipulator', 'manipulator.webhook-delivery');
$app['webhook.delivery_payload_repository'] = $app->share(function ($app) {
return $app['orm.em']->getRepository('Phraseanet:WebhookEventPayload');
});
$app['webhook.processor_factory'] = $app->share(function ($app) { $app['webhook.processor_factory'] = $app->share(function ($app) {
return new EventProcessorFactory($app); return new EventProcessorFactory($app);
}); });
@@ -32,7 +36,8 @@ class WebhookServiceProvider implements ServiceProviderInterface
$app['webhook.event_repository'], $app['webhook.event_repository'],
$app['webhook.event_manipulator'], $app['webhook.event_manipulator'],
$app['webhook.delivery_repository'], $app['webhook.delivery_repository'],
$app['webhook.delivery_manipulator'] $app['webhook.delivery_manipulator'],
$app['webhook.delivery_payload_repository']
); );
}); });

View File

@@ -56,6 +56,11 @@ class WebhookEventDelivery
*/ */
private $created; private $created;
/**
* @ORM\OneToOne(targetEntity="WebhookEventPayload", mappedBy="delivery")
*/
private $payload;
/** /**
* @param \DateTime $created * @param \DateTime $created
* *
@@ -163,4 +168,12 @@ class WebhookEventDelivery
{ {
return $this->event; return $this->event;
} }
/**
* @return WebhookEventPayload
*/
public function getPayload()
{
return $this->payload;
}
} }

View File

@@ -0,0 +1,127 @@
<?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\Model\Entities;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
/**
* @ORM\Table(name="WebhookEventPayloads")
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WebhookEventPayloadRepository")
*/
class WebhookEventPayload
{
/**
* @ORM\Column(type="guid")
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*
* @var string
*/
private $id;
/**
* @ORM\OneToOne(targetEntity="WebhookEventDelivery")
* @ORM\JoinColumn(name="delivery_id", referencedColumnName="id")
*/
private $delivery;
/**
* @ORM\Column(type="text", name="request")
* @var string
*/
private $requestPayload;
/**
* @ORM\Column(type="text", name="response")
* @var string
*/
private $responsePayload;
/**
* @ORM\Column(type="integer", name="status")
* @var int
*/
private $statusCode;
/**
* @ORM\Column(type="text")
* @var string
*/
private $headers;
/**
* @param WebhookEventDelivery $eventDelivery
* @param string $requestPayload
* @param string $responsePayload
* @param int $statusCode
* @param string $headers
*/
public function __construct(WebhookEventDelivery $eventDelivery, $requestPayload, $responsePayload, $statusCode, $headers)
{
$this->id = Uuid::uuid4()->toString();
$this->delivery = $eventDelivery;
$this->requestPayload = $requestPayload;
$this->responsePayload = $responsePayload;
$this->statusCode = $statusCode;
$this->headers = $headers;
}
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @return WebhookEventDelivery
*/
public function getDelivery()
{
return $this->delivery;
}
/**
* @return string
*/
public function getRequestPayload()
{
return $this->requestPayload;
}
/**
* @return string
*/
public function getResponsePayload()
{
return $this->responsePayload;
}
/**
* @return int
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* @return string
*/
public function getResponseHeaders()
{
return $this->headers;
}
}

View File

@@ -11,6 +11,7 @@
namespace Alchemy\Phrasea\Model\Repositories; namespace Alchemy\Phrasea\Model\Repositories;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery; use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
@@ -22,6 +23,10 @@ use Doctrine\ORM\EntityRepository;
*/ */
class WebhookEventDeliveryRepository extends EntityRepository class WebhookEventDeliveryRepository extends EntityRepository
{ {
/**
* @return WebhookEventDelivery[]
*/
public function findUndeliveredEvents() public function findUndeliveredEvents()
{ {
$qb = $this->createQueryBuilder('e'); $qb = $this->createQueryBuilder('e');
@@ -34,4 +39,22 @@ class WebhookEventDeliveryRepository extends EntityRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
/**
* @param ApiApplication $apiApplication
* @param int $count
* @return WebhookEventDelivery[]
*/
public function findLastDeliveries(ApiApplication $apiApplication, $count = 10)
{
$qb = $this->createQueryBuilder('e');
$qb
->where('e.application = :app')
->setMaxResults(max(0, (int) $count))
->orderBy('e.created', 'DESC')
->setParameters([ 'app' => $apiApplication ]);
return $qb->getQuery()->getResult();
}
} }

View File

@@ -0,0 +1,27 @@
<?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\Model\Repositories;
use Alchemy\Phrasea\Model\Entities\WebhookEventPayload;
use Doctrine\ORM\EntityRepository;
class WebhookEventPayloadRepository extends EntityRepository
{
public function save(WebhookEventPayload $payload)
{
$this->_em->persist($payload);
$this->_em->persist($payload->getDelivery());
$this->_em->flush([ $payload, $payload->getDelivery() ]);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Alchemy\Phrasea\Setup\DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20161013115559 extends AbstractMigration
{
public function isAlreadyApplied()
{
return $this->tableExists('Orders');
}
/**
* @param Schema $schema
*/
public function doUpSql(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql("CREATE TABLE WebhookEventPayloads (id CHAR(36) NOT NULL COMMENT '(DC2Type:guid)', delivery_id INT DEFAULT NULL, request LONGTEXT NOT NULL, response LONGTEXT NOT NULL, status INT NOT NULL, headers LONGTEXT NOT NULL, UNIQUE INDEX UNIQ_B949629612136921 (delivery_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;");
$this->addSql("ALTER TABLE WebhookEventPayloads ADD CONSTRAINT FK_B949629612136921 FOREIGN KEY (delivery_id) REFERENCES WebhookEventDeliveries (id);");
}
/**
* @param Schema $schema
*/
public function doDownSql(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE WebhookEventPayloads');
}
}

View File

@@ -14,15 +14,18 @@ namespace Alchemy\Phrasea\Webhook;
use Alchemy\Phrasea\Model\Entities\ApiApplication; use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\Model\Entities\WebhookEvent; use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery; use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Entities\WebhookEventPayload;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator; use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator; use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator;
use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository; use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository; use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventPayloadRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventRepository; use Alchemy\Phrasea\Model\Repositories\WebhookEventRepository;
use Guzzle\Batch\BatchBuilder;
use Guzzle\Common\Event; use Guzzle\Common\Event;
use Guzzle\Http\Client; use Guzzle\Http\Client;
use Guzzle\Http\Message\EntityEnclosingRequestInterface;
use Guzzle\Http\Message\Request; use Guzzle\Http\Message\Request;
use Guzzle\Http\Message\Response;
use Guzzle\Plugin\Backoff\BackoffPlugin; use Guzzle\Plugin\Backoff\BackoffPlugin;
use Guzzle\Plugin\Backoff\CallbackBackoffStrategy; use Guzzle\Plugin\Backoff\CallbackBackoffStrategy;
use Guzzle\Plugin\Backoff\CurlBackoffStrategy; use Guzzle\Plugin\Backoff\CurlBackoffStrategy;
@@ -62,26 +65,35 @@ class WebhookInvoker implements LoggerAwareInterface
* @var LoggerInterface * @var LoggerInterface
*/ */
private $logger; private $logger;
/** /**
* @var EventProcessorFactory * @var EventProcessorFactory
*/ */
private $processorFactory; private $processorFactory;
/** /**
* @var WebhookEventRepository * @var WebhookEventRepository
*/ */
private $eventRepository; private $eventRepository;
/** /**
* @var WebhookEventManipulator * @var WebhookEventManipulator
*/ */
private $eventManipulator; private $eventManipulator;
/**
* @var WebhookEventPayloadRepository
*/
private $eventPayloadRepository;
/** /**
* @param ApiApplicationRepository $applicationRepository * @param ApiApplicationRepository $applicationRepository
* @param EventProcessorFactory $processorFactory * @param EventProcessorFactory $processorFactory
* @param WebhookEventRepository $eventRepository * @param WebhookEventRepository $eventRepository
* @param WebhookEventManipulator $eventManipulator * @param WebhookEventManipulator $eventManipulator
* @param WebhookEventDeliveryManipulator $eventDeliveryManipulator
* @param WebhookEventDeliveryRepository $eventDeliveryRepository * @param WebhookEventDeliveryRepository $eventDeliveryRepository
* @param WebhookEventDeliveryManipulator $eventDeliveryManipulator
* @param WebhookEventPayloadRepository $eventPayloadRepository
* @param Client $client * @param Client $client
* *
* @todo Extract classes to reduce number of required dependencies * @todo Extract classes to reduce number of required dependencies
@@ -93,6 +105,7 @@ class WebhookInvoker implements LoggerAwareInterface
WebhookEventManipulator $eventManipulator, WebhookEventManipulator $eventManipulator,
WebhookEventDeliveryRepository $eventDeliveryRepository, WebhookEventDeliveryRepository $eventDeliveryRepository,
WebhookEventDeliveryManipulator $eventDeliveryManipulator, WebhookEventDeliveryManipulator $eventDeliveryManipulator,
WebhookEventPayloadRepository $eventPayloadRepository,
Client $client = null Client $client = null
) { ) {
$this->applicationRepository = $applicationRepository; $this->applicationRepository = $applicationRepository;
@@ -101,6 +114,8 @@ class WebhookInvoker implements LoggerAwareInterface
$this->eventManipulator = $eventManipulator; $this->eventManipulator = $eventManipulator;
$this->eventDeliveryManipulator = $eventDeliveryManipulator; $this->eventDeliveryManipulator = $eventDeliveryManipulator;
$this->eventDeliveryRepository = $eventDeliveryRepository; $this->eventDeliveryRepository = $eventDeliveryRepository;
$this->eventPayloadRepository = $eventPayloadRepository;
$this->client = $client ?: new Client(); $this->client = $client ?: new Client();
$this->logger = new NullLogger(); $this->logger = new NullLogger();
@@ -216,24 +231,41 @@ class WebhookInvoker implements LoggerAwareInterface
$eventProcessor = $this->processorFactory->getProcessor($event); $eventProcessor = $this->processorFactory->getProcessor($event);
$data = $eventProcessor->process($event); $data = $eventProcessor->process($event);
// Batch requests
$batch = BatchBuilder::factory()
->transferRequests(10)
->build();
foreach ($targets as $thirdPartyApplication) { foreach ($targets as $thirdPartyApplication) {
$delivery = $this->eventDeliveryManipulator->create($thirdPartyApplication, $event); $delivery = $this->eventDeliveryManipulator->create($thirdPartyApplication, $event);
// Append delivery id as url anchor
// append delivery id as url anchor
$uniqueUrl = $this->buildUrl($thirdPartyApplication, $delivery); $uniqueUrl = $this->buildUrl($thirdPartyApplication, $delivery);
// create http request with data as request body // Create http request with data as request body
$batch->add($this->client->createRequest('POST', $uniqueUrl, [ $request = $this->client->createRequest('POST', $uniqueUrl, [
'Content-Type' => 'application/vnd.phraseanet.event+json' 'Content-Type' => 'application/vnd.phraseanet.event+json'
], json_encode($data))); ], 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 = '';
} }
$batch->flush(); $deliveryPayload = new WebhookEventPayload(
$delivery,
$requestBody,
$responseBody,
$statusCode,
$headers
);
$this->eventPayloadRepository->save($deliveryPayload);
}
} }
/** /**
@@ -245,4 +277,16 @@ class WebhookInvoker implements LoggerAwareInterface
{ {
return sprintf('%s#%s', $application->getWebhookUrl(), $delivery->getId()); 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);
}
} }

View File

@@ -19,16 +19,16 @@
{% block content_account %} {% block content_account %}
<div class="row-fluid"> <div class="row-fluid">
<div class="span12"> <div class="span12">
<h1>{{ "Application" | trans }}</h1> <h1>{{ "Application" | trans }} - <strong><a class="link" href="{{ path("developers_application", {"application" : application.getId()}) }}">{{ application.getName() }}</a></strong></h1>
<input type="hidden" value="{{ application.getId() }}" name="app_id"/> <input type="hidden" value="{{ application.getId() }}" name="app_id"/>
<div> <div>
<div><strong><a class="link" href="{{ path("developers_application", {"application" : application.getId()}) }}">{{ application.getName() }}</a></strong></div>
<div>{{ application.getDescription() }}</div> <div>{{ application.getDescription() }}</div>
</div> </div>
<br />
<h1>{{ "settings OAuth" | trans }}</h1> <h1>{{ "settings OAuth" | trans }}</h1>
<p>{{ "Les parametres oauth de votre application." | trans }}</p>
<table id="app-oauth-setting" class="table table-condensed table-bordered"> <table id="app-oauth-setting" class="table table-condensed table-bordered">
<tbody> <tbody>
@@ -121,9 +121,39 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if deliveries | length > 0 %}
<h1>{{ "Derniers envois" | trans }}</h1>
<p> {{ "Résultats des derniers envois effectués pour cette application" | trans }}</p>
<table id="app-access-delivery-log" class="table table-condensed table-bordered">
<thead>
<tr>
<th>&nbsp;</th>
<th>ID</th>
<th>Type</th>
<th>Date</th>
<th>Status code</th>
</tr>
</thead>
<tbody>
{% for delivery in deliveries %}
<tr>
<td>{{ delivery.isDelivered ? 'OK' : 'KO' }}</td>
<td style="font-family: monospace">{{ delivery.payload ? delivery.payload.id : '-' }}</td>
<td>{{ delivery.webhookEvent.type }}</td>
<td>{{ delivery.created | date }}</td>
<td>{{ delivery.payload ? delivery.payload.statusCode : '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="form-actions"> <div class="form-actions">
<a class="btn btn-primary" href="{{ path("developers_applications") }}">{{ "boutton::retour" | trans }}</a> <a class="btn btn-primary" href="{{ path("developers_applications") }}">{{ "boutton::retour" | trans }}</a>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}