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\ApiApplicationRepository;
use Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository;
use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -244,8 +245,12 @@ class DeveloperController extends Controller
throw new AccessDeniedHttpException();
}
$deliveries = $this->getWebhookDeliveryRepository()
->findLastDeliveries($account->getApplication(), 10);
return $this->render('developers/application.html.twig', [
"application" => $application,
"deliveries" => $deliveries,
"user" => $user,
"token" => $token,
]);
@@ -298,4 +303,12 @@ class DeveloperController extends Controller
{
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_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) {
return new EventProcessorFactory($app);
});
@@ -32,7 +36,8 @@ class WebhookServiceProvider implements ServiceProviderInterface
$app['webhook.event_repository'],
$app['webhook.event_manipulator'],
$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;
/**
* @ORM\OneToOne(targetEntity="WebhookEventPayload", mappedBy="delivery")
*/
private $payload;
/**
* @param \DateTime $created
*
@@ -163,4 +168,12 @@ class WebhookEventDelivery
{
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;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Doctrine\ORM\EntityRepository;
@@ -22,6 +23,10 @@ use Doctrine\ORM\EntityRepository;
*/
class WebhookEventDeliveryRepository extends EntityRepository
{
/**
* @return WebhookEventDelivery[]
*/
public function findUndeliveredEvents()
{
$qb = $this->createQueryBuilder('e');
@@ -34,4 +39,22 @@ class WebhookEventDeliveryRepository extends EntityRepository
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\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\Batch\BatchBuilder;
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;
@@ -62,26 +65,35 @@ class WebhookInvoker implements LoggerAwareInterface
* @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 WebhookEventDeliveryManipulator $eventDeliveryManipulator
* @param WebhookEventDeliveryRepository $eventDeliveryRepository
* @param WebhookEventDeliveryManipulator $eventDeliveryManipulator
* @param WebhookEventPayloadRepository $eventPayloadRepository
* @param Client $client
*
* @todo Extract classes to reduce number of required dependencies
@@ -93,6 +105,7 @@ class WebhookInvoker implements LoggerAwareInterface
WebhookEventManipulator $eventManipulator,
WebhookEventDeliveryRepository $eventDeliveryRepository,
WebhookEventDeliveryManipulator $eventDeliveryManipulator,
WebhookEventPayloadRepository $eventPayloadRepository,
Client $client = null
) {
$this->applicationRepository = $applicationRepository;
@@ -101,6 +114,8 @@ class WebhookInvoker implements LoggerAwareInterface
$this->eventManipulator = $eventManipulator;
$this->eventDeliveryManipulator = $eventDeliveryManipulator;
$this->eventDeliveryRepository = $eventDeliveryRepository;
$this->eventPayloadRepository = $eventPayloadRepository;
$this->client = $client ?: new Client();
$this->logger = new NullLogger();
@@ -216,24 +231,41 @@ class WebhookInvoker implements LoggerAwareInterface
$eventProcessor = $this->processorFactory->getProcessor($event);
$data = $eventProcessor->process($event);
// Batch requests
$batch = BatchBuilder::factory()
->transferRequests(10)
->build();
foreach ($targets as $thirdPartyApplication) {
$delivery = $this->eventDeliveryManipulator->create($thirdPartyApplication, $event);
// append delivery id as url anchor
// Append delivery id as url anchor
$uniqueUrl = $this->buildUrl($thirdPartyApplication, $delivery);
// create http request with data as request body
$batch->add($this->client->createRequest('POST', $uniqueUrl, [
// Create http request with data as request body
$request = $this->client->createRequest('POST', $uniqueUrl, [
'Content-Type' => 'application/vnd.phraseanet.event+json'
], json_encode($data)));
}
], json_encode($data));
$batch->flush();
$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);
}
}
/**
@@ -245,4 +277,16 @@ class WebhookInvoker implements LoggerAwareInterface
{
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 %}
<div class="row-fluid">
<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"/>
<div>
<div><strong><a class="link" href="{{ path("developers_application", {"application" : application.getId()}) }}">{{ application.getName() }}</a></strong></div>
<div>{{ application.getDescription() }}</div>
</div>
<br />
<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">
<tbody>
@@ -121,9 +121,39 @@
</tr>
</tbody>
</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">
<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>
{% endblock %}