Add third party application webhook event

This commit is contained in:
Nicolas Le Goff
2014-06-23 17:48:42 +02:00
parent 1645d914af
commit 0d040519f3
26 changed files with 1106 additions and 195 deletions

View File

@@ -37,6 +37,8 @@ use Alchemy\Phrasea\Model\Entities\UsrList;
use Alchemy\Phrasea\Model\Entities\UsrListEntry; use Alchemy\Phrasea\Model\Entities\UsrListEntry;
use Alchemy\Phrasea\Model\Entities\StoryWZ; use Alchemy\Phrasea\Model\Entities\StoryWZ;
use Alchemy\Phrasea\Core\Provider\ORMServiceProvider; use Alchemy\Phrasea\Core\Provider\ORMServiceProvider;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\SchemaTool;
@@ -109,6 +111,8 @@ class RegenerateSqliteDb extends Command
$this->insertTwoTokens($this->container['EM'], $DI); $this->insertTwoTokens($this->container['EM'], $DI);
$this->insertOneInvalidToken($this->container['EM'], $DI); $this->insertOneInvalidToken($this->container['EM'], $DI);
$this->insertOneValidationToken($this->container['EM'], $DI); $this->insertOneValidationToken($this->container['EM'], $DI);
$this->insertWebhookEvent($this->container['EM'], $DI);
$this->insertWebhookEventDelivery($this->container['EM'], $DI);
$this->container['EM']->flush(); $this->container['EM']->flush();
@@ -121,6 +125,7 @@ class RegenerateSqliteDb extends Command
$fixtures['token']['token_2'] = $DI['token_2']->getValue(); $fixtures['token']['token_2'] = $DI['token_2']->getValue();
$fixtures['token']['token_invalid'] = $DI['token_invalid']->getValue(); $fixtures['token']['token_invalid'] = $DI['token_invalid']->getValue();
$fixtures['token']['token_validation'] = $DI['token_validation']->getValue(); $fixtures['token']['token_validation'] = $DI['token_validation']->getValue();
$fixtures['user']['test_phpunit'] = $DI['user']->getId(); $fixtures['user']['test_phpunit'] = $DI['user']->getId();
$fixtures['user']['test_phpunit_not_admin'] = $DI['user_notAdmin']->getId(); $fixtures['user']['test_phpunit_not_admin'] = $DI['user_notAdmin']->getId();
$fixtures['user']['test_phpunit_alt1'] = $DI['user_alt1']->getId(); $fixtures['user']['test_phpunit_alt1'] = $DI['user_alt1']->getId();
@@ -133,6 +138,7 @@ class RegenerateSqliteDb extends Command
$fixtures['oauth']['acc-user-not-admin'] = $DI['api-app-acc-user-not-admin']->getId(); $fixtures['oauth']['acc-user-not-admin'] = $DI['api-app-acc-user-not-admin']->getId();
$fixtures['databox']['records'] = $DI['databox']->get_sbas_id(); $fixtures['databox']['records'] = $DI['databox']->get_sbas_id();
$fixtures['collection']['coll'] = $DI['coll']->get_base_id(); $fixtures['collection']['coll'] = $DI['coll']->get_base_id();
$fixtures['collection']['coll_no_access'] = $DI['coll_no_access']->get_base_id(); $fixtures['collection']['coll_no_access'] = $DI['coll_no_access']->get_base_id();
$fixtures['collection']['coll_no_status'] = $DI['coll_no_status']->get_base_id(); $fixtures['collection']['coll_no_status'] = $DI['coll_no_status']->get_base_id();
@@ -170,6 +176,8 @@ class RegenerateSqliteDb extends Command
$fixtures['feed']['private']['feed'] = $DI['feed_private']->getId(); $fixtures['feed']['private']['feed'] = $DI['feed_private']->getId();
$fixtures['feed']['private']['entry'] = $DI['feed_private_entry']->getId(); $fixtures['feed']['private']['entry'] = $DI['feed_private_entry']->getId();
$fixtures['feed']['private']['token'] = $DI['feed_private_token']->getId(); $fixtures['feed']['private']['token'] = $DI['feed_private_token']->getId();
$fixtures['webhook']['event'] = $DI['event_webhook_1']->getId();
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln("<error>".$e->getMessage()."</error>"); $output->writeln("<error>".$e->getMessage()."</error>");
if ($renamed) { if ($renamed) {
@@ -329,6 +337,45 @@ class RegenerateSqliteDb extends Command
return $this->container['manipulator.user']->createUser($login, uniqid('pass'), $email, $admin); return $this->container['manipulator.user']->createUser($login, uniqid('pass'), $email, $admin);
} }
protected function insertWebhookEvent(EntityManager $em, \Pimple $DI)
{
$event = new WebhookEvent();
$event->setName(WebhookEvent::NEW_FEED_ENTRY);
$event->setType(WebhookEvent::FEED_ENTRY_TYPE);
$event->setData(array(
'feed_id' => $DI['feed_public_entry']->getFeed()->getId(),
'entry_id' => $DI['feed_public_entry']->getId()
));
$em->persist($event);
$DI['event_webhook_1'] = $event;
$event2 = new WebhookEvent();
$event2->setName(WebhookEvent::NEW_FEED_ENTRY);
$event2->setType(WebhookEvent::FEED_ENTRY_TYPE);
$event2->setData(array(
'feed_id' => $DI['feed_public_entry']->getFeed()->getId(),
'entry_id' => $DI['feed_public_entry']->getId()
));
$event2->setProcessed(true);
$em->persist($event2);
}
protected function insertWebhookEventDelivery(EntityManager $em, \Pimple $DI)
{
$delivery = new WebhookEventDelivery();
$delivery->setThirdPartyApplication($DI['api-app-user']);
$delivery->setWebhookEvent($DI['event_webhook_1']);
$delivery->setDelivered(true);
$em->persist($delivery);
$delivery2 = new WebhookEventDelivery();
$delivery2->setThirdPartyApplication($DI['api-app-user-not-admin']);
$delivery2->setWebhookEvent($DI['event_webhook_1']);
$delivery2->setDeliverTries(1);
$em->persist($delivery2);
}
private function generateCollection(\Pimple $DI) private function generateCollection(\Pimple $DI)
{ {
$coll = $collection_no_acces = $collection_no_acces_by_status = $db = null; $coll = $collection_no_acces = $collection_no_acces_by_status = $db = null;

View File

@@ -23,6 +23,8 @@ use Alchemy\Phrasea\Model\Manipulator\RegistrationManipulator;
use Alchemy\Phrasea\Model\Manipulator\TaskManipulator; use Alchemy\Phrasea\Model\Manipulator\TaskManipulator;
use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
use Alchemy\Phrasea\Model\Manipulator\UserManipulator; use Alchemy\Phrasea\Model\Manipulator\UserManipulator;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator;
use Alchemy\Phrasea\Model\Manager\UserManager; use Alchemy\Phrasea\Model\Manager\UserManager;
use Silex\Application as SilexApplication; use Silex\Application as SilexApplication;
use Silex\ServiceProviderInterface; use Silex\ServiceProviderInterface;
@@ -82,6 +84,14 @@ class ManipulatorServiceProvider implements ServiceProviderInterface
$app['manipulator.api-log'] = $app->share(function ($app) { $app['manipulator.api-log'] = $app->share(function ($app) {
return new ApiLogManipulator($app['EM'], $app['repo.api-logs']); return new ApiLogManipulator($app['EM'], $app['repo.api-logs']);
}); });
$app['manipulator.webhook-event'] = $app->share(function ($app) {
return new WebhookEventManipulator($app['EM'], $app['repo.webhook-event']);
});
$app['manipulator.webhook-delivery'] = $app->share(function ($app) {
return new WebhookEventDeliveryManipulator($app['EM'], $app['repo.webhook-delivery']);
});
} }
public function boot(SilexApplication $app) public function boot(SilexApplication $app)

View File

@@ -115,6 +115,12 @@ class RepositoriesServiceProvider implements ServiceProviderInterface
$app['repo.api-oauth-refresh-tokens'] = $app->share(function (PhraseaApplication $app) { $app['repo.api-oauth-refresh-tokens'] = $app->share(function (PhraseaApplication $app) {
return $app['EM']->getRepository('Phraseanet:ApiOauthRefreshToken'); return $app['EM']->getRepository('Phraseanet:ApiOauthRefreshToken');
}); });
$app['repo.webhook-event'] = $app->share(function (PhraseaApplication $app) {
return $app['EM']->getRepository('Phraseanet:WebhookEvent');
});
$app['repo.webhook-delivery'] = $app->share(function (PhraseaApplication $app) {
return $app['EM']->getRepository('Phraseanet:WebhookEventDelivery');
});
} }
public function boot(Application $app) public function boot(Application $app)

View File

@@ -18,12 +18,14 @@ use Alchemy\Phrasea\TaskManager\Job\FtpPullJob;
use Alchemy\Phrasea\TaskManager\Job\PhraseanetIndexerJob; use Alchemy\Phrasea\TaskManager\Job\PhraseanetIndexerJob;
use Alchemy\Phrasea\TaskManager\Job\RecordMoverJob; use Alchemy\Phrasea\TaskManager\Job\RecordMoverJob;
use Alchemy\Phrasea\TaskManager\Job\SubdefsJob; use Alchemy\Phrasea\TaskManager\Job\SubdefsJob;
use Alchemy\Phrasea\TaskManager\Job\WebhookJob;
use Alchemy\Phrasea\TaskManager\Job\WriteMetadataJob; use Alchemy\Phrasea\TaskManager\Job\WriteMetadataJob;
use Alchemy\Phrasea\TaskManager\Job\Factory as JobFactory; use Alchemy\Phrasea\TaskManager\Job\Factory as JobFactory;
use Alchemy\Phrasea\TaskManager\LiveInformation; use Alchemy\Phrasea\TaskManager\LiveInformation;
use Alchemy\Phrasea\TaskManager\TaskManagerStatus; use Alchemy\Phrasea\TaskManager\TaskManagerStatus;
use Alchemy\Phrasea\TaskManager\Log\LogFileFactory; use Alchemy\Phrasea\TaskManager\Log\LogFileFactory;
use Alchemy\Phrasea\TaskManager\Notifier; use Alchemy\Phrasea\TaskManager\Notifier;
use Alchemy\Phrasea\Webhook\EventProcessorFactory;
use Silex\Application; use Silex\Application;
use Silex\ServiceProviderInterface; use Silex\ServiceProviderInterface;
@@ -76,6 +78,7 @@ class TasksServiceProvider implements ServiceProviderInterface
new RecordMoverJob($app['dispatcher'], $logger, $app['translator']), new RecordMoverJob($app['dispatcher'], $logger, $app['translator']),
new SubdefsJob($app['dispatcher'], $logger, $app['translator']), new SubdefsJob($app['dispatcher'], $logger, $app['translator']),
new WriteMetadataJob($app['dispatcher'], $logger, $app['translator']), new WriteMetadataJob($app['dispatcher'], $logger, $app['translator']),
new WebhookJob($app['dispatcher'], $logger, $app['translator']),
]; ];
}); });
} }

View File

@@ -127,7 +127,7 @@ class ApiApplication
/** /**
* @var string * @var string
* *
* @ORM\Column(name="webhook_url", type="string", length=128) * @ORM\Column(name="webhook_url", type="string", length=128, nullable=true)
*/ */
private $webhookUrl; private $webhookUrl;

View File

@@ -0,0 +1,180 @@
<?php
namespace Alchemy\Phrasea\Model\Entities;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ORM\Table(name="WebhookEvents", indexes={@ORM\Index(name="name", columns={"name"})})
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WebhookEventRepository")
*/
class WebhookEvent
{
const NEW_FEED_ENTRY = 'new_feed_entry';
const FEED_ENTRY_TYPE = 'feed_entry';
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(type="string", length=64, nullable=false)
*/
private $name;
/**
* @ORM\Column(type="string", length=64, nullable=false)
*/
private $type;
/**
* @var string
*
* @ORM\Column(type="json_array", nullable=false)
*/
private $data;
/**
* @var boolean
*
* @ORM\Column(type="boolean", nullable=false)
*/
private $processed = false;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private $created;
public static function types()
{
return array(self::FEED_ENTRY_TYPE);
}
public static function events()
{
return array(self::NEW_FEED_ENTRY);
}
/**
* @param \DateTime $created
*
* @return WebhookEvent
*/
public function setCreated(\DateTime $created)
{
$this->created = $created;
return $this;
}
/**
* @return \DateTime
*/
public function getCreated()
{
return $this->created;
}
/**
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* @param array $data
*
* @return WebhookEvent
*/
public function setData(array $data)
{
$this->data = $data;
return $this;
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* @param $name
*
* @return WebhookEvent
* @throws \InvalidArgumentException
*/
public function setName($name)
{
if (!in_array($name, self::events())) {
throw new \InvalidArgumentException("Invalid event name");
}
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param boolean $processed
*
* @return $this
*/
public function setProcessed($processed)
{
$this->processed = (Boolean) $processed;
return $this;
}
/**
* @return boolean
*/
public function isProcessed()
{
return $this->processed;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @param $type
*
* @return $this
* @throws \InvalidArgumentException
*/
public function setType($type)
{
if (!in_array($type, self::types())) {
throw new \InvalidArgumentException("Invalid event name");
}
$this->type = $type;
return $this;
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Alchemy\Phrasea\Model\Entities;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ORM\Table(name="WebhookEventDeliveries",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="unique_app_delivery",columns={"application_id", "event_id"})
* }
* )
* @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository")
*/
class WebhookEventDelivery
{
const MAX_DELIVERY_TRIES = 3;
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="ApiApplication")
* @ORM\JoinColumn(name="application_id", referencedColumnName="id", nullable=false)
*
* @return ApiApplication
**/
private $application;
/**
* @ORM\ManyToOne(targetEntity="WebhookEvent")
* @ORM\JoinColumn(name="event_id", referencedColumnName="id", nullable=false)
*
* @return WebhookEvent
**/
private $event;
/**
* @ORM\Column(type="boolean", nullable=false)
*/
private $delivered = false;
/**
* @ORM\Column(type="integer", nullable=false)
*/
private $deliveryTries = 0;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private $created;
/**
* @param \DateTime $created
*
* @return WebhookEvent
*/
public function setCreated(\DateTime $created)
{
$this->created = $created;
return $this;
}
/**
* @return \DateTime
*/
public function getCreated()
{
return $this->created;
}
/**
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* @param $delivered
*
* @return $this
*/
public function setDelivered($delivered)
{
$this->delivered = (Boolean) $delivered;
return $this;
}
/**
* @return Boolean
*/
public function isDelivered()
{
return $this->delivered;
}
/**
* @return integer
*/
public function getDeliveryTries()
{
return $this->deliveryTries;
}
/**
* @param integer $try
*
* @return $this
*/
public function setDeliverTries($try)
{
$this->deliveryTries = (int) $try;
return $this;
}
/**
* @return ApiApplication
*/
public function getThirdPartyApplication()
{
return $this->application;
}
/**
* @param ApiApplication $application
*
* @return $this
*/
public function setThirdPartyApplication(ApiApplication $application)
{
$this->application = $application;
return $this;
}
/**
* @param WebhookEvent $event
*
* @return $this
*/
public function setWebhookEvent(WebhookEvent $event)
{
$this->event = $event;
return $this;
}
/**
* @return WebhookEvent
*/
public function getWebhookEvent()
{
return $this->event;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Model\Manipulator;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Authentication\ACLProvider;
use Alchemy\Phrasea\Model\Entities\ApiAccount;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\Model\Entities\ApiLog;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class WebhookEventDeliveryManipulator implements ManipulatorInterface
{
private $om;
private $repository;
public function __construct(ObjectManager $om, EntityRepository $repo)
{
$this->om = $om;
$this->repository = $repo;
}
public function create(ApiApplication $application, WebhookEvent $event)
{
$delivery = new WebhookEventDelivery();
$delivery->setThirdPartyApplication($application);
$delivery->setWebhookEvent($event);
$this->update($delivery);
return $delivery;
}
public function delete(WebhookEventDelivery $delivery)
{
$this->om->remove($delivery);
$this->om->flush();
}
public function update(WebhookEventDelivery $delivery)
{
$this->om->persist($delivery);
$this->om->flush();
}
public function deliverySuccess(WebhookEventDelivery $delivery)
{
$delivery->setDelivered(true);
$delivery->setDeliverTries($delivery->getDeliveryTries() + 1);
$this->update($delivery);
}
public function deliveryFailure(WebhookEventDelivery $delivery)
{
$delivery->setDelivered(false);
$delivery->setDeliverTries($delivery->getDeliveryTries() + 1);
$this->update($delivery);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Model\Manipulator;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Authentication\ACLProvider;
use Alchemy\Phrasea\Model\Entities\ApiAccount;
use Alchemy\Phrasea\Model\Entities\ApiLog;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class WebhookEventManipulator implements ManipulatorInterface
{
private $om;
private $repository;
public function __construct(ObjectManager $om, EntityRepository $repo)
{
$this->om = $om;
$this->repository = $repo;
}
public function create($eventName, $type, array $data)
{
$event = new WebhookEvent();
$event->setName($eventName);
$event->setType($type);
$event->setData($data);
$this->update($event);
return $event;
}
public function delete(WebhookEvent $event)
{
$this->om->remove($event);
$this->om->flush();
}
public function update(WebhookEvent $event)
{
$this->om->persist($event);
$this->om->flush();
}
public function processed(WebhookEvent $event)
{
$event->setProcessed(true);
$this->update($event);
}
}

View File

@@ -50,4 +50,12 @@ class ApiApplicationRepository extends EntityRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function findWithDefinedWebhookCallback()
{
$qb = $this->createQueryBuilder('app');
$qb->where($qb->expr()->isNotNull('app.webhookUrl'));
return $qb->getQuery()->getResult();
}
} }

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* 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\WebhookEventDelivery;
use Doctrine\ORM\EntityRepository;
/**
* WebhookEventDeliveryRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class WebhookEventDeliveryRepository extends EntityRepository
{
public function findUndeliveredEvents()
{
$qb = $this->createQueryBuilder('e');
$qb
->where($qb->expr()->eq('e.delivered', $qb->expr()->literal(false)))
->andWhere($qb->expr()->lt('e.deliveryTries', ':nb_tries'));
$qb->setParameter(':nb_tries', WebhookEventDelivery::MAX_DELIVERY_TRIES);
return $qb->getQuery()->getResult();
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* 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\WebhookEvent;
use Doctrine\ORM\EntityRepository;
/**
* WebhookEventRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class WebhookEventRepository extends EntityRepository
{
public function findUnprocessedEvents()
{
$qb = $this->createQueryBuilder('e');
$qb->where($qb->expr()->eq('e.processed', $qb->expr()->literal(false)));
return $qb->getQuery()->getResult();
}
}

View File

@@ -0,0 +1,168 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\TaskManager\Job;
use Alchemy\Phrasea\Core\Version;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\TaskManager\Editor\DefaultEditor;
use Alchemy\Phrasea\Webhook\EventProcessorFactory;
use Guzzle\Http\Client as GuzzleClient;
use Guzzle\Batch\BatchBuilder;
use Silex\Application;
use Guzzle\Common\Event;
use Guzzle\Plugin\Backoff\BackoffPlugin;
use Guzzle\Plugin\Backoff\TruncatedBackoffStrategy;
use Guzzle\Plugin\Backoff\CallbackBackoffStrategy;
use Guzzle\Plugin\Backoff\CurlBackoffStrategy;
use Guzzle\Plugin\Backoff\ExponentialBackoffStrategy;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Guzzle\Http\Exception\MultiTransferException;
class WebhookJob extends AbstractJob
{
private $httpClient;
public function __construct(EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null, TranslatorInterface $translator, GuzzleClient $httpClient = null)
{
parent::__construct($dispatcher, $logger, $translator);
$this->httpClient = $httpClient ?: new GuzzleClient();
$this->httpClient->setUserAgent(sprintf('Phraseanet/%s (%s)', Version::getNumber(), Version::getName()));
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->translator->trans("API Webhook");
}
/**
* {@inheritdoc}
*/
public function getJobId()
{
return 'Webhook';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return $this->translator->trans("Notify third party application when an event occurs in Phraseanet");
}
/**
* {@inheritdoc}
*/
public function getEditor()
{
return new DefaultEditor($this->translator);
}
/**
* {@inheritdoc}
*/
protected function doJob(JobData $data)
{
$app = $data->getApplication();
$thirdPartyApplications = $app['repo.api-applications']->findWithDefinedWebhookCallback();
$that = $this;
$this->httpClient->getEventDispatcher()->addListener('request.error', function (Event $event) {
// override guzzle default behavior of throwing exceptions
// when 4xx & 5xx responses are encountered
$event->stopPropagation();
}, -254);
$this->httpClient->addSubscriber(new BackoffPlugin(
// set max retries
new TruncatedBackoffStrategy(WebhookEventDelivery::MAX_DELIVERY_TRIES,
// set callback which logs success or failure
new CallbackBackoffStrategy(function($retries, $request, $response, $e) use ($app, $that) {
$retry = true;
if ($response && (null !== $deliverId = parse_url($request->getUrl(), PHP_URL_FRAGMENT))) {
$delivery = $app['repo.webhook-delivery']->find($deliverId);
if ($response->isSuccessful()) {
$app['manipulator.webhook-delivery']->deliverySuccess($delivery);
$that->log('info', sprintf('Deliver success event "%d:%s" for app "%s"', $delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(), $delivery->getThirdPartyApplication()->getName()));
$retry = false;
} else {
$app['manipulator.webhook-delivery']->deliveryFailure($delivery);
$that->log('error', sprintf('Deliver failure event "%d:%s" for app "%s"', $delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(), $delivery->getThirdPartyApplication()->getName()));
}
return $retry;
}},
true,
new CurlBackoffStrategy()
)
)
));
foreach ($app['repo.webhook-event']->findUnprocessedEvents() as $event) {
// set event as processed
$app['manipulator.webhook-event']->processed($event);
$this->log('info', sprintf('Processing event "%s" with id %d', $event->getName(), $event->getId()));
// send requests
$this->deliverEvent($app, $thirdPartyApplications, $event);
}
}
private function deliverEvent(Application $app, array $thirdPartyApplications, WebhookEvent $event)
{
if (count($thirdPartyApplications) === 0) {
$this->log('info', sprintf('No applications defined to listen for webhook events'));
return;
}
// format event data
$eventFactory = new EventProcessorFactory($app);
$eventProcessor = $eventFactory->get($event);
$data = $eventProcessor->process();
// batch requests
$batch = BatchBuilder::factory()
->transferRequests(10)
->build();
foreach ($thirdPartyApplications as $thirdPartyApplication) {
$delivery = $app['manipulator.webhook-delivery']->create($thirdPartyApplication, $event);
// append delivery id as url anchor
$uniqueUrl = $this->getUrl($thirdPartyApplication, $delivery);
// create http request with data as request body
$batch->add($this->httpClient->createRequest('POST', $uniqueUrl, array(
'Content-Type' => 'application/vnd.phraseanet.event+json'
), json_encode($data)));
}
$batch->flush();
}
private function getUrl(ApiApplication $application, WebhookEventDelivery $delivery)
{
return sprintf('%s#%s', $application->getWebhookUrl(), $delivery->getId());
}
}

View File

@@ -11,6 +11,7 @@
use Alchemy\Phrasea\Notification\Receiver; use Alchemy\Phrasea\Notification\Receiver;
use Alchemy\Phrasea\Notification\Mail\MailInfoNewPublication; use Alchemy\Phrasea\Notification\Mail\MailInfoNewPublication;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
class eventsmanager_notify_feed extends eventsmanager_notifyAbstract class eventsmanager_notify_feed extends eventsmanager_notifyAbstract
{ {
@@ -59,9 +60,11 @@ class eventsmanager_notify_feed extends eventsmanager_notifyAbstract
$data = $dom_xml->saveXml(); $data = $dom_xml->saveXml();
API_Webhook::create($this->app['phraseanet.appbox'], API_Webhook::NEW_FEED_ENTRY, array_merge( $this->app['manipulator.webhook-event']->create(
array('feed_id' => $entry->getFeed()->getId()), $params WebhookEvent::NEW_FEED_ENTRY,
)); WebhookEvent::FEED_ENTRY_TYPE,
array_merge(array('feed_id' => $entry->getFeed()->getId()), $params)
);
$Query = new \User_Query($this->app); $Query = new \User_Query($this->app);

View File

@@ -1,144 +0,0 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Guzzle\Http\Client as GuzzleClient;
class task_period_apiwebhooks extends task_appboxAbstract
{
public static function getName()
{
return _('Api Webhook');
}
public static function help()
{
return _('Notify Phraseanet Oauth2 client applications using webhooks.');
}
protected function retrieveContent(appbox $appbox)
{
$stmt = $appbox->get_connection()->prepare('SELECT id, `type`, `data` FROM api_webhooks');
$stmt->execute();
$rs = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$stmt->closeCursor();
return $rs;
}
protected function processOneContent(appbox $appbox, array $row)
{
$data = null;
switch ($row['type']) {
case \API_Webhook::NEW_FEED_ENTRY:
$data = $this->processNewFeedEntry($row);
}
if (null === $data) {
return;
}
$urls = $this->getApplicationHookUrls($appbox);
$this->sendData($urls, $data);
}
protected function postProcessOneContent(appbox $appbox, array $row)
{
$w = new API_Webhook($appbox, $row['id']);
$w->delete();
}
protected function getApplicationHookUrls(appbox $appbox)
{
$stmt = $appbox->get_connection()->prepare('
SELECT webhook_url
FROM api_applications
WHERE webhook_url IS NOT NULL
');
$stmt->execute();
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$stmt->closeCursor();
return array_map(function ($row) {
return $row['webhook_url'];
}, $rows);
}
protected function sendData(array $urls, array $data)
{
if (count($urls) === 0) {
return;
}
$client = new GuzzleClient();
$body = json_encode($data);
$requests = array();
foreach ($urls as $url) {
$requests[] = $client->createRequest('POST', $url, array(
'Content-Type' => 'application/vnd.phraseanet.event+json'
), $body);
}
$client->send($requests);
}
protected function processNewFeedEntry(array $row)
{
$data = json_decode($row['data']);
if (!isset($data->{"feed_id"}) || !isset($data->{"entry_id"})) {
return;
}
$feed = new Feed_Adapter($this->dependencyContainer, $data->{"feed_id"});
$entry = new \Feed_Entry_Adapter($this->dependencyContainer, $feed, $data->{"entry_id"});
$query = new \User_Query($this->dependencyContainer);
$query->include_phantoms(true)
->include_invite(false)
->include_templates(false)
->email_not_null(true);
if ($entry->get_feed()->get_collection()) {
$query->on_base_ids(array($entry->get_feed()->get_collection()->get_base_id()));
}
$start = 0;
$perLoop = 100;
$users = array();
do {
$results = $query->limit($start, $perLoop)->execute()->get_results();
foreach ($results as $user) {
$users[] = array(
'email' => $user->get_email(),
'firstname' => $user->get_firstname() ?: null,
'lastname' => $user->get_lastname() ?: null,
);
}
$start += $perLoop;
} while (count($results) > 0);
return array(
'event' => $row['type'],
'users_were_notified' => !!$data->{"notify_email"},
'feed' => array(
'id' => $feed->get_id(),
'title' => $feed->get_title(),
'description' => $feed->get_subtitle() ?: null,
),
'entry' => array(
'id' => $entry->get_id(),
'author' => array(
'name' => $entry->get_author_name(),
'email' => $entry->get_author_email()
),
'title' => $entry->get_title(),
'description' => $entry->get_subtitle() ?: null,
),
'users' => $users
);
}
}

View File

@@ -76,53 +76,6 @@
</indexes> </indexes>
<engine>InnoDB</engine> <engine>InnoDB</engine>
</table> </table>
<table name="api_webhooks">
<fields>
<field>
<name>id</name>
<type>int(11) unsigned</type>
<null></null>
<extra>auto_increment</extra>
<default></default>
<comment></comment>
</field>
<field>
<name>type</name>
<type>varchar(64)</type>
<null></null>
<extra></extra>
<default></default>
<comment></comment>
</field>
<field>
<name>data</name>
<type>longtext</type>
<null></null>
<extra></extra>
<default></default>
<comment></comment>
</field>
<field>
<name>created</name>
<type>datetime</type>
<null></null>
<extra></extra>
<default></default>
<comment></comment>
</field>
</fields>
<indexes>
<index>
<name>PRIMARY</name>
<type>PRIMARY</type>
<fields>
<field>id</field>
</fields>
</index>
</indexes>
<engine>InnoDB</engine>
</table>
<table name="bridge_accounts"> <table name="bridge_accounts">
<fields> <fields>
<field> <field>

View File

@@ -57,6 +57,16 @@ class ManipulatorServiceProviderTest extends ServiceProviderTestCase
'manipulator.api-oauth-refresh-token', 'manipulator.api-oauth-refresh-token',
'Alchemy\Phrasea\Model\Manipulator\ApiOauthRefreshTokenManipulator' 'Alchemy\Phrasea\Model\Manipulator\ApiOauthRefreshTokenManipulator'
], ],
[
'Alchemy\Phrasea\Core\Provider\ManipulatorServiceProvider',
'manipulator.webhook-event',
'Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator'
],
[
'Alchemy\Phrasea\Core\Provider\ManipulatorServiceProvider',
'manipulator.webhook-delivery',
'Alchemy\Phrasea\Model\Manipulator\WebhookEventDelivery'
],
]; ];
} }
} }

View File

@@ -39,6 +39,8 @@ class RepositoriesServiceProviderTest extends ServiceProviderTestCase
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-tokens', 'Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository'], ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-tokens', 'Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository'],
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-codes', 'Alchemy\Phrasea\Model\Repositories\ApiOauthCodeRepository'], ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-codes', 'Alchemy\Phrasea\Model\Repositories\ApiOauthCodeRepository'],
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-refresh-tokens', 'Alchemy\Phrasea\Model\Repositories\ApiOauthRefreshTokenRepository'], ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-refresh-tokens', 'Alchemy\Phrasea\Model\Repositories\ApiOauthRefreshTokenRepository'],
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.webhook-event', 'Alchemy\Phrasea\Model\Repositories\WebhookEventRepository'],
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.webhook-delivery', 'Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository'],
]; ];
} }
} }

View File

@@ -0,0 +1,106 @@
<?php
namespace Alchemy\Tests\Phrasea\Model\Manipulator;
use Alchemy\Phrasea\Controller\Api\V1;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Manipulator\ApiApplicationManipulator;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
class WebhookEventDeliveryManipulatorTest extends \PhraseanetTestCase
{
public function testCreate()
{
$manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
$application = $manipApp->create(
uniqid('app'),
ApiApplication::WEB_TYPE,
'Desktop application description',
'http://web-app-url.net',
self::$DI['user'],
'http://web-app-url.net/callback'
);
$manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
$nbHooks = count(self::$DI['app']['repo.webhook-delivery']->findAll());
$manipulator->create($application, self::$DI['webhook-event']);
$this->assertGreaterThan($nbHooks, count(self::$DI['app']['repo.webhook-delivery']->findAll()));
}
public function testDelete()
{
$manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
$application = $manipApp->create(
uniqid('app'),
ApiApplication::WEB_TYPE,
'Desktop application description',
'http://web-app-url.net',
self::$DI['user'],
'http://web-app-url.net/callback'
);
$manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
$eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
$countBefore = count(self::$DI['app']['repo.webhook-delivery']->findAll());
$manipulator->delete($eventDelivery);
$this->assertGreaterThan(count(self::$DI['app']['repo.webhook-delivery']->findAll()), $countBefore);
}
public function testUpdate()
{
$manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
$application = $manipApp->create(
uniqid('app'),
ApiApplication::WEB_TYPE,
'Desktop application description',
'http://web-app-url.net',
self::$DI['user'],
'http://web-app-url.net/callback'
);
$manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
$eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
$this->assertEquals(0, $eventDelivery->getDeliveryTries());
$eventDelivery->setDeliverTries(1);
$manipulator->update($eventDelivery);
$eventDelivery = self::$DI['app']['repo.webhook-delivery']->find($eventDelivery->getId());
$this->assertEquals(1, $eventDelivery->getDeliveryTries());
}
public function testDeliverySuccess()
{
$manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
$application = $manipApp->create(
uniqid('app'),
ApiApplication::WEB_TYPE,
'Desktop application description',
'http://web-app-url.net',
self::$DI['user'],
'http://web-app-url.net/callback'
);
$manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
$eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
$tries = $eventDelivery->getDeliveryTries();
$manipulator->deliverySuccess($eventDelivery);
$this->assertTrue($eventDelivery->isDelivered());
$this->assertGreaterThan($tries, $eventDelivery->getDeliveryTries());
}
public function testDeliveryFailure()
{
$manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
$application = $manipApp->create(
uniqid('app'),
ApiApplication::WEB_TYPE,
'Desktop application description',
'http://web-app-url.net',
self::$DI['user'],
'http://web-app-url.net/callback'
);
$manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
$eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
$tries = $eventDelivery->getDeliveryTries();
$manipulator->deliveryFailure($eventDelivery);
$this->assertfalse($eventDelivery->isDelivered());
$this->assertGreaterThan($tries, $eventDelivery->getDeliveryTries());
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Alchemy\Tests\Phrasea\Model\Manipulator;
use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator;
use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
class WebhookEventManipulatorTest extends \PhraseanetTestCase
{
public function testCreate()
{
$manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.manipulator.webhook-delivery']);
$nbEvents = count(self::$DI['app']['repo.webhook-event']->findAll());
$event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
));
$this->assertGreaterThan($nbEvents, count(self::$DI['app']['repo.webhook-event']->findAll()));
}
public function testDelete()
{
$manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-event']);
$event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
));
$eventMem = clone $event;
$countBefore = count(self::$DI['app']['repo.webhook-event']->findAll());
self::$DI['app']['manipulator.webhook-delivery']->create($event);
$manipulator->delete($event);
$this->assertGreaterThan(count(self::$DI['app']['repo.webhook-event']->findAll()), $countBefore);
$tokens = self::$DI['app']['repo.api-oauth-tokens']->findOauthTokens($eventMem);
$this->assertEquals(0, count($tokens));
}
public function testUpdate()
{
$manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-event']);
$event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
));
$event->setProcessed(true);
$manipulator->update($event);
$event = self::$DI['app']['repo.webhook-event']->find($event->getId());
$this->assertTrue($event->isProcessed());
}
public function testProcessed()
{
$manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-event']);
$event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
));
$manipulator->processed($event);
$this->assertTrue($event->isProcessed());
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Alchemy\Tests\Phrasea\Model\Repositories;
class WebhookEventDeliveryRepositoryTest extends \PhraseanetTestCase
{
public function testFindUndeliveredEvents()
{
$events = self::$DI['app']['EM']->getRepository('Phraseanet:WebhookEventDelivery')->findUndeliveredEvents();
$this->assertCount(1, $events);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Alchemy\Tests\Phrasea\Model\Repositories;
class WebhookEventRepositoryTest extends \PhraseanetTestCase
{
public function testFindUnprocessedEvents()
{
$events = self::$DI['app']['EM']->getRepository('Phraseanet:WebhookEvent')->findUnprocessedEvents();
$this->assertCount(1, $events);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Alchemy\Tests\Phrasea\TaskManager\Job;
use Alchemy\Phrasea\TaskManager\Job\WebhookJob;
class WebhookJobTestJobTest extends JobTestCase
{
protected function getJob()
{
return new WebhookJob(null, null, $this->createTranslatorMock());
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Alchemy\Tests\Phrasea\Webhook;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Webhook\EventProcessorFactory;
class EventProcessorFactoryTest extends \PhraseanetTestCase
{
/**
* @dataProvider eventProvider
*/
public function testGet($type, $expected)
{
$factory = new EventProcessorFactory(self::$DI['app']);
$event = new WebhookEvent();
$event->setType($type);
$this->assertInstanceOf($expected, $factory->get($event));
}
/**
* @expectedException \RuntimeException
*/
public function testUnknownProcessor()
{
$factory = new EventProcessorFactory(self::$DI['app']);
$event = new WebhookEvent();
$factory->get($event);
}
public function eventProvider()
{
return array(
array(WebhookEvent::FEED_ENTRY_TYPE, 'Alchemy\Phrasea\Webhook\Processor\FeedEntryProcessor'),
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Alchemy\Tests\Phrasea\Webhook;
use Alchemy\Phrasea\Model\Entities\WebhookEvent;
use Alchemy\Phrasea\Webhook\Processor\FeedEntryProcessor;
class FeedEntryProcessorTest extends \PhraseanetTestCase
{
public function testProcessWithNoFeedId()
{
$event = new WebhookEvent();
$event->setData(array(
'feed_id' => 0,
'entry_id' => 0
));
$event->setName(WebhookEvent::NEW_FEED_ENTRY);
$event->setType(WebhookEvent::FEED_ENTRY_TYPE);
$processor = new FeedEntryProcessor($event, self::$DI['app']);
$this->assertEquals($processor->process(), null);
}
public function testProcessWithMissingDataProperty()
{
$event = new WebhookEvent();
$event->setData(array(
'feed_id' => 0,
));
$event->setName(WebhookEvent::NEW_FEED_ENTRY);
$event->setType(WebhookEvent::FEED_ENTRY_TYPE);
$processor = new FeedEntryProcessor($event, self::$DI['app']);
$this->assertEquals($processor->process(), null);
}
public function testProcess()
{
$event = new WebhookEvent();
$event->setData(array(
'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(),
'entry_id' => self::$DI['feed_public_entry']->getId()
));
$event->setName(WebhookEvent::NEW_FEED_ENTRY);
$event->setType(WebhookEvent::FEED_ENTRY_TYPE);
$processor = new FeedEntryProcessor($event, self::$DI['app']);
$this->assertEquals($processor->process(), null);
}
}

View File

@@ -202,6 +202,10 @@ abstract class PhraseanetTestCase extends WebTestCase
return $DI['app']['repo.api-applications']->find(self::$fixtureIds['oauth']['user']); return $DI['app']['repo.api-applications']->find(self::$fixtureIds['oauth']['user']);
}); });
self::$DI['webhook-event'] = self::$DI->share(function ($DI) {
return $DI['app']['repo.webhook-event']->find(self::$fixtureIds['webhook']['event']);
});
self::$DI['oauth2-app-user-not-admin'] = self::$DI->share(function ($DI) { self::$DI['oauth2-app-user-not-admin'] = self::$DI->share(function ($DI) {
return $DI['app']['repo.api-applications']->find(self::$fixtureIds['oauth']['user-not-admin']); return $DI['app']['repo.api-applications']->find(self::$fixtureIds['oauth']['user-not-admin']);
}); });