├── .gitignore ├── Model ├── GenericEventInterface.php ├── GenericCreateEntityEventInterface.php ├── GenericRemoveEntityEventInterface.php ├── GenericUpdateEntityEventInterface.php ├── PayloadCanNotBeCreatedException.php ├── OuterEventDispatcherInterface.php ├── CreateRemoveProductEventTypePayload.php ├── CreateEntityEventAdapter.php ├── UpdateEntityEventAdapter.php ├── RemoveEntityEventAdapter.php ├── EventTypeConfigurationList.php ├── CreateRemoveEventTypePayload.php ├── EntityEventTypes.php ├── CreateEventTypePayload.php ├── ResolveEventType.php ├── EventsHandler.php ├── AkeneoBatchOuterEventDispatcher.php └── PimEventTypeConfigurationFactories.php ├── EventType ├── EventIsNotSupportedException.php ├── EventTypeConfigurationInterface.php ├── EventType.php └── EventTypeConfiguration.php ├── HttpClient ├── Exception.php ├── HttpClientFactoryInterface.php └── RequestFactoryInterface.php ├── Resources ├── fixtures │ └── minimal │ │ └── jobs.yml └── config │ ├── jobs.yml │ ├── services.yml │ └── events.yml ├── Transport ├── TransportFactoryInterface.php ├── Transport.php ├── HttpTransportFactory.php ├── IFTTTWebHooksTransportFactory.php ├── HttpTransport.php └── IFTTTWebHooksTransport.php ├── TrilixEventsApiBundle.php ├── Guzzle ├── GuzzleRequestFactory.php ├── GuzzleHttpClientFactory.php └── GuzzleHttpClientAdapter.php ├── OuterEvent ├── OuterEventBuilder.php └── OuterEvent.php ├── Tests └── Unit │ ├── Guzzle │ ├── GuzzleRequestFactoryTest.php │ ├── GuzzleHttpClientFactoryTest.php │ └── GuzzleHttpClientAdapterTest.php │ ├── Job │ └── DeliverOuterEventTaskletTest.php │ ├── OuterEvent │ ├── OuterEventBuilderTest.php │ └── OuterEventTest.php │ ├── Transport │ ├── HttpTransportTest.php │ ├── IFTTTWebHooksTransportTest.php │ ├── HttpTransportFactoryTest.php │ └── IFTTTWebHooksTransportFactoryTest.php │ ├── EventType │ └── EventTypeConfigurationTest.php │ ├── Model │ ├── CreateEventTypePayloadTest.php │ ├── CreateRemoveProductEventTypePayloadTest.php │ ├── CreateRemoveEventTypePayloadTest.php │ ├── AkeneoBatchOuterEventDispatcherTest.php │ ├── ResolveEventTypeTest.php │ └── EventsHandlerTest.php │ └── EventSubscriber │ └── AkeneoStorageUtilsSubscriberTest.php ├── composer.json ├── DependencyInjection ├── Compiler │ └── RegisterEventTypeConfigurationsPass.php ├── Configuration.php └── TrilixEventsApiExtension.php ├── phpunit.xml.dist ├── Job ├── JobParameters │ └── DeliverOuterEventConstraintCollectionProvider.php └── DeliverOuterEventTasklet.php ├── LICENSE ├── EventSubscriber └── AkeneoStorageUtilsSubscriber.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | test-reports/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /Model/GenericEventInterface.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterEventTypeConfigurationsPass()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Guzzle/GuzzleRequestFactory.php: -------------------------------------------------------------------------------- 1 | getSubject(); 20 | 21 | assert::that($product)->propertyExists('identifier'); 22 | return ['identifier' => $product->getIdentifier()]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /OuterEvent/OuterEventBuilder.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 15 | return $this; 16 | } 17 | 18 | /** 19 | * @param string $eventName 20 | * @return OuterEvent 21 | */ 22 | public function build(string $eventName): OuterEvent 23 | { 24 | $outerEvent = new OuterEvent($eventName, $this->payload, time()); 25 | 26 | $this->payload = []; 27 | 28 | return $outerEvent; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Guzzle/GuzzleHttpClientFactory.php: -------------------------------------------------------------------------------- 1 | notEmpty() 21 | ->url(); 22 | 23 | return new GuzzleHttpClientAdapter(new GuzzleHttpClient(['base_uri' => $baseUri, 'timeout' => $timeout])); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Model/CreateEntityEventAdapter.php: -------------------------------------------------------------------------------- 1 | getSubject())->isObject(); 24 | return new self($genericEvent->getSubject()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Model/UpdateEntityEventAdapter.php: -------------------------------------------------------------------------------- 1 | getSubject())->isObject(); 24 | return new self($genericEvent->getSubject()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Model/RemoveEntityEventAdapter.php: -------------------------------------------------------------------------------- 1 | getSubject())->isObject(); 25 | return new self($removeEvent->getSubject()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Model/EventTypeConfigurationList.php: -------------------------------------------------------------------------------- 1 | eventTypeConfigurations[] = $eventTypeConfiguration; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getIterator() 28 | { 29 | return new ArrayIterator($this->eventTypeConfigurations); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Model/CreateRemoveEventTypePayload.php: -------------------------------------------------------------------------------- 1 | getSubject(); 23 | 24 | assert::that($entity)->propertyExists('code'); 25 | return ['code' => $entity->getCode()]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/Unit/Guzzle/GuzzleRequestFactoryTest.php: -------------------------------------------------------------------------------- 1 | create('POST', '', ['bar' => 'foo'], 'foo_bar'); 21 | 22 | self::assertInstanceOf(RequestInterface::class, $request); 23 | self::assertSame('POST', $request->getMethod()); 24 | self::assertContains('foo', $request->getHeader('bar')); 25 | self::assertSame('', $request->getUri()->getPath()); 26 | self::assertSame('foo_bar', $request->getBody()->getContents()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trilix/akeneo-events-api-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Akeneo PIM Events API Bundle", 5 | "keywords": ["Akeneo", "PIM", "Events", "API"], 6 | "homepage": "https://www.trilix-gmbh.de", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Trilix GmbH", 11 | "homepage": "https://www.trilix-gmbh.de" 12 | } 13 | ], 14 | "require": { 15 | "akeneo/pim-community-dev": "^5.0", 16 | "beberlei/assert": "^3.3", 17 | "psr/http-client": "^1", 18 | "guzzlehttp/guzzle": "~6.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^8.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { "Trilix\\EventsApiBundle\\": "./" } 25 | }, 26 | "minimum-stability": "stable", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "0.7.x-dev" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/RegisterEventTypeConfigurationsPass.php: -------------------------------------------------------------------------------- 1 | getDefinition('pim_events_api.event_type_configuration_list'); 17 | 18 | $eventTypeConfigurations = $container->findTaggedServiceIds('pim_events_api.event_type_configuration'); 19 | foreach ($eventTypeConfigurations as $serviceId => $tags) { 20 | $eventTypeConfigurationList->addMethodCall('addEventTypeConfiguration', [new Reference($serviceId)]); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EventType/EventType.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | $this->payload = $payload; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getName(): string 30 | { 31 | return $this->name; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getPayload(): array 38 | { 39 | return $this->payload; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function __toString() 46 | { 47 | return $this->name; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Tests/Unit 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | * 17 | 18 | Tests 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Model/EntityEventTypes.php: -------------------------------------------------------------------------------- 1 | [self::JOB_PARAMETER_KEY_OUTER_EVENT => new Optional()]]); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function supports(JobInterface $job): bool 28 | { 29 | return 'deliver_outer_event_to_consumer' === $job->getName(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Trilix GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('trilix_events_api'); 17 | 18 | $rootNode 19 | ->children() 20 | ->arrayNode('transport') 21 | ->children() 22 | ->scalarNode('factory') 23 | ->isRequired() 24 | ->defaultValue('pim_events_api.transport_factory.http') 25 | ->end() 26 | ->arrayNode('options') 27 | ->prototype('scalar') 28 | ->end() 29 | ->end() 30 | ->end() 31 | ->end() 32 | ->end() 33 | ->end(); 34 | 35 | return $treeBuilder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Guzzle/GuzzleHttpClientAdapter.php: -------------------------------------------------------------------------------- 1 | guzzleHttpClient = $guzzleHttpClient; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | * @throws HttpClientException 30 | */ 31 | public function sendRequest(RequestInterface $request): ResponseInterface 32 | { 33 | try { 34 | return $this->guzzleHttpClient->send($request); 35 | } catch (GuzzleException $e) { 36 | throw new HttpClientException($e->getMessage(), $e->getCode(), $e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/Unit/Guzzle/GuzzleHttpClientFactoryTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 22 | 23 | $factory = new GuzzleHttpClientFactory(); 24 | $factory->create($baseUri); 25 | } 26 | 27 | /** 28 | * @test 29 | */ 30 | public function createsGuzzleHttpClientAdapter(): void 31 | { 32 | $factory = new GuzzleHttpClientFactory(); 33 | $client = $factory->create('http://127.0.0.1'); 34 | 35 | self::assertInstanceOf(GuzzleHttpClientAdapter::class, $client); 36 | } 37 | 38 | public function baseUriDataProvider(): array 39 | { 40 | return [ 41 | [''], 42 | ['foo_host'] 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Transport/HttpTransportFactory.php: -------------------------------------------------------------------------------- 1 | httpClientFactory = $httpClientFactory; 27 | $this->requestFactory = $requestFactory; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function create(array $options): Transport 34 | { 35 | Assert::that($options)->keyExists('request_url'); 36 | Assert::that($options['request_url'])->notEmpty()->url(); 37 | 38 | return new HttpTransport($options['request_url'], $this->httpClientFactory, $this->requestFactory); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Job/DeliverOuterEventTasklet.php: -------------------------------------------------------------------------------- 1 | transport = $transport; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function setStepExecution(StepExecution $stepExecution): void 34 | { 35 | $this->stepExecution = $stepExecution; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function execute(): void 42 | { 43 | $outerEventJson = $this->stepExecution->getJobParameters() 44 | ->get(DeliverOuterEventConstraintCollectionProvider::JOB_PARAMETER_KEY_OUTER_EVENT); 45 | 46 | $this->transport->deliver(OuterEvent::fromArray($outerEventJson)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DependencyInjection/TrilixEventsApiExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yml'); 22 | $loader->load('events.yml'); 23 | $loader->load('jobs.yml'); 24 | 25 | $config = $this->processConfiguration(new Configuration(), $configs); 26 | 27 | $transportFactory = $config['transport']['factory']; 28 | $transportOptions = $config['transport']['options'] ?? []; 29 | 30 | $transportDefinition = new Definition(Transport::class); 31 | $transportDefinition->setFactory([$container->findDefinition($transportFactory), 'create']); 32 | $transportDefinition->setArguments([$transportOptions]); 33 | 34 | $container->setDefinition('pim_events_api.transport.default', $transportDefinition); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /EventType/EventTypeConfiguration.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | $this->resolver = $resolver; 30 | $this->payloadFactory = $payloadFactory; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function resolve(GenericEventInterface $event): EventType 37 | { 38 | if (!($this->resolver)($event)) { 39 | throw new EventIsNotSupportedException( 40 | sprintf( 41 | 'Event (with subject %s) is not supported by this event type configuration (%s)', 42 | get_class($event->getSubject()), 43 | get_class($this) 44 | ) 45 | ); 46 | } 47 | 48 | return new EventType($this->name, ($this->payloadFactory)($event)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Transport/IFTTTWebHooksTransportFactory.php: -------------------------------------------------------------------------------- 1 | httpClientFactory = $httpClientFactory; 27 | $this->requestFactory = $requestFactory; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function create(array $options): Transport 34 | { 35 | Assert::that($options)->keyExists('request_url'); 36 | Assert::that($options['request_url'])->notEmpty(); 37 | 38 | $requestUrl = str_replace('{event}', 'te_st', $options['request_url']); 39 | Assert::that($requestUrl)->url(); 40 | 41 | return new IFTTTWebHooksTransport($options['request_url'], $this->httpClientFactory, $this->requestFactory); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Transport/HttpTransport.php: -------------------------------------------------------------------------------- 1 | requestUrl = $requestUrl; 34 | $this->httpClientFactory = $httpClientFactory; 35 | $this->requestFactory = $requestFactory; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function deliver(OuterEvent $event): void 42 | { 43 | $client = $this->httpClientFactory->create($this->requestUrl); 44 | $request = $this->requestFactory->create('POST', '', ['Content-Type' => 'application/json'], json_encode($event)); 45 | $client->sendRequest($request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Model/CreateEventTypePayload.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 23 | } 24 | 25 | /** 26 | * @param GenericEventInterface $event 27 | * @return array 28 | * @throws PayloadCanNotBeCreatedException if $entity can not be serialized 29 | */ 30 | public function __invoke(GenericEventInterface $event): array 31 | { 32 | $entity = $event->getSubject(); 33 | 34 | try { 35 | $payload = $this->serializer->normalize($entity, 'external_api'); 36 | } catch (SerializerExceptionInterface $e) { 37 | throw new PayloadCanNotBeCreatedException( 38 | sprintf( 39 | 'Payload can not be created for the given event (event=%s; subject=%s).', 40 | get_class($event), 41 | get_class($entity) 42 | ), 43 | 0, 44 | $e 45 | ); 46 | } 47 | 48 | return $payload; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/Unit/Job/DeliverOuterEventTaskletTest.php: -------------------------------------------------------------------------------- 1 | 'payload'], time()); 24 | 25 | /** @var Transport|MockObject $transport */ 26 | $transport = $this->createMock(Transport::class); 27 | $transport->expects(self::once())->method('deliver') 28 | ->with($event); 29 | 30 | $tasklet = new DeliverOuterEventTasklet($transport); 31 | 32 | $tasklet->setStepExecution( 33 | $this->createStepExecution(new JobParameters(['outer_event' => $event->toArray()])) 34 | ); 35 | 36 | $tasklet->execute(); 37 | } 38 | 39 | /** 40 | * @param JobParameters $jobParameters 41 | * @return StepExecution 42 | */ 43 | private function createStepExecution(JobParameters $jobParameters): StepExecution 44 | { 45 | return new StepExecution( 46 | 'foo', 47 | (new JobExecution())->setJobParameters($jobParameters) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Model/ResolveEventType.php: -------------------------------------------------------------------------------- 1 | eventTypeConfigurationList = $eventTypeConfigurationList; 24 | } 25 | 26 | /** 27 | * @param GenericEventInterface $event 28 | * @return EventType|null 29 | * @throws PayloadCanNotBeCreatedException 30 | */ 31 | public function __invoke(GenericEventInterface $event): ?EventType 32 | { 33 | $entity = $event->getSubject(); 34 | 35 | Assert::that($entity)->isObject(); 36 | 37 | $eventType = null; 38 | $iterator = $this->eventTypeConfigurationList->getIterator(); 39 | $iterator->rewind(); 40 | while (is_null($eventType) && $iterator->valid() && $configuration = $iterator->current()) { 41 | try { 42 | /** @var EventTypeConfigurationInterface $configuration */ 43 | $eventType = $configuration->resolve($event); 44 | } catch (EventIsNotSupportedException $e) { 45 | } finally { 46 | $iterator->next(); 47 | } 48 | } 49 | 50 | return $eventType; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Unit/OuterEvent/OuterEventBuilderTest.php: -------------------------------------------------------------------------------- 1 | build('foo_event'); 18 | self::assertSame('foo_event', $outerEvent->eventType()); 19 | } 20 | 21 | /** 22 | * @test 23 | */ 24 | public function buildsOuterEventWithEmptyPayload(): void 25 | { 26 | $outerEvent = (new OuterEventBuilder())->build('foo_event'); 27 | self::assertEmpty($outerEvent->payload()); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function buildsOuterEventWithNotEmptyPayload(): void 34 | { 35 | $payload = ['foo' => 'bar']; 36 | $outerEvent = (new OuterEventBuilder()) 37 | ->withPayload($payload) 38 | ->build('foo_event'); 39 | 40 | self::assertSame($payload, $outerEvent->payload()); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function builderReleasesConfigurationAfterBuild(): void 47 | { 48 | $builder = new OuterEventBuilder(); 49 | 50 | $payload = ['foo' => 'bar']; 51 | 52 | $outerEventWithPayload = $builder->withPayload($payload)->build('with_payload'); 53 | $outerEventWithOutPayload = $builder->build('with_out_payload'); 54 | 55 | self::assertSame($payload, $outerEventWithPayload->payload()); 56 | self::assertEmpty($outerEventWithOutPayload->payload()); 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Transport/IFTTTWebHooksTransport.php: -------------------------------------------------------------------------------- 1 | requestUrl = $requestUrl; 34 | $this->httpClientFactory = $httpClientFactory; 35 | $this->requestFactory = $requestFactory; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function deliver(OuterEvent $event): void 42 | { 43 | $requestUrl = str_replace('{event}', $event->eventType(), $this->requestUrl); 44 | $client = $this->httpClientFactory->create($requestUrl); 45 | $request = $this->requestFactory->create( 46 | 'POST', 47 | '', 48 | ['Content-Type' => 'application/json'], 49 | json_encode( 50 | [ 51 | 'value1' => $event->eventType(), 52 | 'value2' => $event->payload() 53 | ] 54 | ) 55 | ); 56 | $client->sendRequest($request); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Unit/Transport/HttpTransportTest.php: -------------------------------------------------------------------------------- 1 | 'payload'], time()); 24 | 25 | /** @var ClientInterface|MockObject $httpClient */ 26 | $httpClient = $this->createMock(ClientInterface::class); 27 | $request = $this->createMock(RequestInterface::class); 28 | /** @var HttpClientFactoryInterface|MockObject $httpClientFactory */ 29 | $httpClientFactory = $this->createMock(HttpClientFactoryInterface::class); 30 | /** @var RequestFactoryInterface|MockObject $requestFactory */ 31 | $requestFactory = $this->createMock(RequestFactoryInterface::class); 32 | 33 | $httpClientFactory->expects(self::once())->method('create') 34 | ->with('http://localhost:1234')->willReturn($httpClient); 35 | $requestFactory->expects(self::once())->method('create') 36 | ->with('POST', '', ['Content-Type' => 'application/json'], json_encode($outerEvent)) 37 | ->willReturn($request); 38 | $httpClient->expects(self::once())->method('sendRequest')->with($request); 39 | 40 | $httpTransport = new HttpTransport('http://localhost:1234', $httpClientFactory, $requestFactory); 41 | 42 | $httpTransport->deliver($outerEvent); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Unit/Transport/IFTTTWebHooksTransportTest.php: -------------------------------------------------------------------------------- 1 | 'payload'], time()); 24 | $requestUrl = 'https://iftt.com/a/b/{event}/x/y'; 25 | 26 | /** @var ClientInterface|MockObject $httpClient */ 27 | $httpClient = $this->createMock(ClientInterface::class); 28 | $request = $this->createMock(RequestInterface::class); 29 | /** @var HttpClientFactoryInterface|MockObject $httpClientFactory */ 30 | $httpClientFactory = $this->createMock(HttpClientFactoryInterface::class); 31 | /** @var RequestFactoryInterface|MockObject $requestFactory */ 32 | $requestFactory = $this->createMock(RequestFactoryInterface::class); 33 | 34 | $httpClientFactory->expects(self::once())->method('create') 35 | ->with('https://iftt.com/a/b/foo_name/x/y')->willReturn($httpClient); 36 | $requestFactory->expects(self::once())->method('create') 37 | ->with('POST', '', ['Content-Type' => 'application/json'], json_encode(['value1' => 'foo_name', 'value2' => ['foo' => 'payload']])) 38 | ->willReturn($request); 39 | $httpClient->expects(self::once())->method('sendRequest')->with($request); 40 | 41 | $transport = new IFTTTWebHooksTransport($requestUrl, $httpClientFactory, $requestFactory); 42 | 43 | $transport->deliver($outerEvent); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Model/EventsHandler.php: -------------------------------------------------------------------------------- 1 | resolveEventType = $resolveEventType; 39 | $this->outerEventBuilder = $outerEventBuilder; 40 | $this->outerEventDispatcher = $eventDispatcher; 41 | $this->logger = $logger; 42 | } 43 | 44 | /** 45 | * @param GenericEventInterface $event 46 | */ 47 | public function handle(GenericEventInterface $event): void 48 | { 49 | $entity = $event->getSubject(); 50 | Assert::that($entity)->isObject(); 51 | 52 | try { 53 | $eventType = $this->resolveEventType->__invoke($event); 54 | if (!$eventType) { 55 | return; 56 | } 57 | $outerEvent = $this->outerEventBuilder 58 | ->withPayload($eventType->getPayload()) 59 | ->build($eventType->getName()); 60 | $this->outerEventDispatcher->dispatch($outerEvent); 61 | } catch (PayloadCanNotBeCreatedException $e) { 62 | $this->logger->notice($e->getMessage()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Model/AkeneoBatchOuterEventDispatcher.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 36 | $this->jobInstanceRepository = $jobInstanceRepository; 37 | $this->jobLauncher = $jobLauncher; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function dispatch(OuterEvent $event): void 44 | { 45 | $token = $this->tokenStorage->getToken(); 46 | $jobInstance = $this->jobInstanceRepository->findOneByIdentifier('deliver_outer_event_to_consumer'); 47 | if (!$token || !$jobInstance) { 48 | return; 49 | } 50 | 51 | $this->jobLauncher 52 | ->launch( 53 | $jobInstance, 54 | $token->getUser(), 55 | [DeliverOuterEventConstraintCollectionProvider::JOB_PARAMETER_KEY_OUTER_EVENT => $event->toArray()] 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Unit/Guzzle/GuzzleHttpClientAdapterTest.php: -------------------------------------------------------------------------------- 1 | expectException(HttpClientException::class); 25 | 26 | /** @var RequestInterface|MockObject $request */ 27 | $request = $this->createMock(RequestInterface::class); 28 | 29 | /** @var GuzzleHttpClient|MockObject $guzzleClient */ 30 | $guzzleClient = $this->createMock(GuzzleHttpClient::class); 31 | $guzzleClient->expects(self::once())->method('send') 32 | ->with($request)->willThrowException(new class extends RuntimeException implements GuzzleException {}); 33 | 34 | $adapter = new GuzzleHttpClientAdapter($guzzleClient); 35 | $adapter->sendRequest($request); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function guzzleHttpClientSendsGivenRequest(): void 42 | { 43 | /** @var RequestInterface|MockObject $request */ 44 | $request = $this->createMock(RequestInterface::class); 45 | /** @var ResponseInterface|MockObject $response */ 46 | $response = $this->createMock(ResponseInterface::class); 47 | 48 | /** @var GuzzleHttpClient|MockObject $guzzleClient */ 49 | $guzzleClient = $this->createMock(GuzzleHttpClient::class); 50 | $guzzleClient->expects(self::once())->method('send') 51 | ->with($request)->willReturn($response); 52 | 53 | $adapter = new GuzzleHttpClientAdapter($guzzleClient); 54 | self::assertSame($response, $adapter->sendRequest($request)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/Unit/Transport/HttpTransportFactoryTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 25 | 26 | /** @var HttpClientFactoryInterface|MockObject $httpClientFactory */ 27 | $httpClientFactory = $this->createMock(HttpClientFactoryInterface::class); 28 | /** @var RequestFactoryInterface|MockObject $requestFactory */ 29 | $requestFactory = $this->createMock(RequestFactoryInterface::class); 30 | $factory = new HttpTransportFactory($httpClientFactory, $requestFactory); 31 | 32 | $factory->create($options); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function createsHttpTransport(): void 39 | { 40 | /** @var HttpClientFactoryInterface|MockObject $httpClientFactory */ 41 | $httpClientFactory = $this->createMock(HttpClientFactoryInterface::class); 42 | /** @var RequestFactoryInterface|MockObject $requestFactory */ 43 | $requestFactory = $this->createMock(RequestFactoryInterface::class); 44 | $factory = new HttpTransportFactory($httpClientFactory, $requestFactory); 45 | 46 | $transport = $factory->create(['request_url' => 'http://foo.bar']); 47 | 48 | self::assertInstanceOf(HttpTransport::class, $transport); 49 | } 50 | 51 | public function invalidOptionsDataProvider(): array 52 | { 53 | return [ 54 | [ [] ], 55 | [ ['foo_param'] ], 56 | [ ['foo_param' => 'value'] ], 57 | [ ['request_url' => ''] ], 58 | [ ['request_url' => 'url_value'] ], 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /OuterEvent/OuterEvent.php: -------------------------------------------------------------------------------- 1 | notEmpty(); 30 | Assert::that($eventTime)->lessOrEqualThan(time()); 31 | 32 | $this->eventType = $eventType; 33 | $this->payload = $payload; 34 | $this->eventTime = $eventTime; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function eventType(): string 41 | { 42 | return $this->eventType; 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function payload(): array 49 | { 50 | return $this->payload; 51 | } 52 | 53 | /** 54 | * @return int 55 | */ 56 | public function eventTime(): int 57 | { 58 | return $this->eventTime; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function toArray(): array 65 | { 66 | return [ 67 | 'event_type' => $this->eventType, 68 | 'payload' => $this->payload, 69 | 'event_time' => $this->eventTime 70 | ]; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function jsonSerialize() 77 | { 78 | return $this->toArray(); 79 | } 80 | 81 | /** 82 | * @param array $array 83 | * @return OuterEvent 84 | */ 85 | public static function fromArray(array $array): self 86 | { 87 | Assert::that($array) 88 | ->keyExists('event_type') 89 | ->keyExists('payload') 90 | ->keyExists('event_time'); 91 | 92 | Assert::that($array['event_type'])->string(); 93 | Assert::that($array['payload'])->isArray(); 94 | Assert::that($array['event_time'])->integer(); 95 | 96 | return new self($array['event_type'], $array['payload'], $array['event_time']); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/Unit/EventType/EventTypeConfigurationTest.php: -------------------------------------------------------------------------------- 1 | getSubject() instanceof isSupportedEntity; }, 22 | function (GenericEventInterface $event): array { return ['foo' => get_class($event->getSubject())]; } 23 | ); 24 | 25 | $event = new class implements GenericEventInterface 26 | { 27 | public function getSubject() 28 | { 29 | return new isNotSupportedEntity(); 30 | } 31 | }; 32 | 33 | $this->expectException(EventIsNotSupportedException::class); 34 | 35 | $configuration->resolve($event); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function resolvesEventType(): void 42 | { 43 | $eventTypeConfiguration = new EventTypeConfiguration( 44 | 'foo', 45 | function (GenericEventInterface $event): bool { return $event->getSubject() instanceof isSupportedEntity; }, 46 | function (GenericEventInterface $event): array { return ['foo' => get_class($event->getSubject())]; } 47 | ); 48 | 49 | $event = new class implements GenericEventInterface 50 | { 51 | public function getSubject() 52 | { 53 | return new isSupportedEntity(); 54 | } 55 | }; 56 | 57 | $eventType = $eventTypeConfiguration->resolve($event); 58 | 59 | self::assertNotNull($eventType); 60 | self::assertSame($eventType->getName(), 'foo'); 61 | self::assertNotEmpty($eventType->getPayload()); 62 | self::assertArrayHasKey('foo', $eventType->getPayload()); 63 | self::assertSame(get_class($event->getSubject()), $eventType->getPayload()['foo']); 64 | } 65 | } 66 | 67 | class isNotSupportedEntity { 68 | } 69 | 70 | class isSupportedEntity { 71 | } 72 | -------------------------------------------------------------------------------- /Tests/Unit/OuterEvent/OuterEventTest.php: -------------------------------------------------------------------------------- 1 | 'bar'], $eventTime); 19 | 20 | self::assertSame('foo_bar_event', $event->eventType()); 21 | self::assertSame(['foo' => 'bar'], $event->payload()); 22 | self::assertSame($eventTime, $event->eventTime()); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function convertsToArray(): void 29 | { 30 | $eventTime = time(); 31 | $event = new OuterEvent('foo_event', ['foo' => 'payload'], $eventTime); 32 | 33 | self::assertSame( 34 | [ 35 | 'event_type' => 'foo_event', 36 | 'payload' => ['foo' => 'payload'], 37 | 'event_time' => $eventTime 38 | ], 39 | $event->toArray() 40 | ); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function beingSerializedIntoJson(): void 47 | { 48 | $eventTime = time(); 49 | $event = new OuterEvent('foo_bar_event', ['foo' => 'bar'], $eventTime); 50 | 51 | $actualJson = json_encode($event); 52 | 53 | self::assertJson($actualJson); 54 | self::assertJsonStringEqualsJsonString( 55 | json_encode( 56 | [ 57 | 'event_type' => 'foo_bar_event', 58 | 'payload' => ['foo' => 'bar'], 59 | 'event_time' => $eventTime 60 | ] 61 | ), 62 | $actualJson 63 | ); 64 | } 65 | 66 | /** 67 | * @test 68 | */ 69 | public function beingCreatedFromArray(): void 70 | { 71 | $eventTime = time(); 72 | $event = OuterEvent::fromArray( 73 | [ 74 | 'event_type' => 'foo', 75 | 'payload' => ['foo' => 'payload'], 76 | 'event_time' => $eventTime 77 | ] 78 | ); 79 | 80 | self::assertSame('foo', $event->eventType()); 81 | self::assertSame(['foo' => 'payload'], $event->payload()); 82 | self::assertSame($eventTime, $event->eventTime()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/Unit/Transport/IFTTTWebHooksTransportFactoryTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 27 | 28 | /** @var HttpClientFactoryInterface|MockObject $httpClientFactory */ 29 | $httpClientFactory = $this->createMock(HttpClientFactoryInterface::class); 30 | /** @var RequestFactoryInterface|MockObject $requestFactory */ 31 | $requestFactory = $this->createMock(RequestFactoryInterface::class); 32 | $factory = new IFTTTWebHooksTransportFactory($httpClientFactory, $requestFactory); 33 | 34 | $factory->create($options); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function createsIFTTTWebHooksTransport(): void 41 | { 42 | /** @var HttpClientFactoryInterface|MockObject $httpClientFactory */ 43 | $httpClientFactory = $this->createMock(HttpClientFactoryInterface::class); 44 | /** @var RequestFactoryInterface|MockObject $requestFactory */ 45 | $requestFactory = $this->createMock(RequestFactoryInterface::class); 46 | 47 | $factory = new IFTTTWebHooksTransportFactory($httpClientFactory, $requestFactory); 48 | 49 | $transport = $factory->create(['request_url' => self::IFTTT_WEBHOOKS_REQUEST_URL]); 50 | 51 | self::assertInstanceOf(IFTTTWebHooksTransport::class, $transport); 52 | } 53 | 54 | public function invalidOptionsDataProvider(): array 55 | { 56 | return [ 57 | [[]], 58 | [['foo']], 59 | [['foo' => 'bar']], 60 | [['request_url' => '']], 61 | [['request_url' => 'url_value']] 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/Unit/Model/CreateEventTypePayloadTest.php: -------------------------------------------------------------------------------- 1 | serializer = $this->createMock(NormalizerInterface::class); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function throwsPayloadCanNotBeCreatedExceptionIfEntityCanNotBeSerialized(): void 37 | { 38 | $this->expectException(PayloadCanNotBeCreatedException::class); 39 | 40 | $event = new class implements GenericEventInterface { 41 | public function getSubject() 42 | { 43 | return new class {}; 44 | } 45 | 46 | }; 47 | $this->serializer->expects(self::once())->method('normalize') 48 | ->with($this->isType(IsType::TYPE_OBJECT)) 49 | ->willThrowException(new class extends RuntimeException implements SerializerExceptionInterface {}); 50 | 51 | (new CreateEventTypePayload($this->serializer))->__invoke($event); 52 | } 53 | 54 | /** 55 | * @test 56 | */ 57 | public function createsPayload(): void 58 | { 59 | $event = new class implements GenericEventInterface { 60 | public function getSubject() 61 | { 62 | return new class extends AbstractProduct {}; 63 | } 64 | 65 | }; 66 | $expectedPayload = ['foo' => 'bar']; 67 | $this->serializer->expects(self::once())->method('normalize') 68 | ->with($this->isType(IsType::TYPE_OBJECT)) 69 | ->willReturn($expectedPayload); 70 | $actualPayload = (new CreateEventTypePayload($this->serializer))->__invoke($event); 71 | 72 | self::assertSame($expectedPayload, $actualPayload); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Resources/config/jobs.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | pim_events_api.job.job_parameters.constraint_collection_provider.deliver_outer_event.class: Trilix\EventsApiBundle\Job\JobParameters\DeliverOuterEventConstraintCollectionProvider 3 | pim_events_api.tasklet.deliver_outer_event.class: Trilix\EventsApiBundle\Job\DeliverOuterEventTasklet 4 | pim_events_api.deliver_outer_event_to_consumer.job_type: 'deliver_outer_event_to_consumer' 5 | pim_events_api.deliver_outer_event_to_consumer.job_name: 'deliver_outer_event_to_consumer' 6 | pim_events_api.deliver_outer_event_to_consumer.tasklet_name: 'deliver_outer_event_to_consumer' 7 | pim_events_api.deliver_outer_event_to_consumer.connector_name: 'deliver_outer_event_to_consumer' 8 | 9 | services: 10 | pim_events_api.job.job_parameters.constraint_collection_provider.deliver_outer_event: 11 | class: '%pim_events_api.job.job_parameters.constraint_collection_provider.deliver_outer_event.class%' 12 | tags: 13 | - { name: akeneo_batch.job.job_parameters.constraint_collection_provider } 14 | 15 | pim_events_api.job.job_parameters.default_values_provider.deliver_outer_event_to_consumer: 16 | class: '%akeneo_batch.job.job_parameters.empty_values_provider.class%' 17 | arguments: 18 | - 19 | - '%pim_events_api.deliver_outer_event_to_consumer.job_name%' 20 | tags: 21 | - { name: akeneo_batch.job.job_parameters.default_values_provider } 22 | 23 | pim_events_api.tasklet.deliver_outer_event: 24 | class: '%pim_events_api.tasklet.deliver_outer_event.class%' 25 | arguments: 26 | - '@pim_events_api.transport.default' 27 | public: false 28 | 29 | pim_events_api.step.deliver_outer_event: 30 | class: '%pim_connector.step.tasklet.class%' 31 | arguments: 32 | - '%pim_events_api.deliver_outer_event_to_consumer.tasklet_name%' 33 | - '@event_dispatcher' 34 | - '@akeneo_batch.job_repository' 35 | - '@pim_events_api.tasklet.deliver_outer_event' 36 | public: false 37 | 38 | pim_events_api.job.deliver_outer_event_to_consumer: 39 | class: '%pim_connector.job.simple_job.class%' 40 | arguments: 41 | - '%pim_events_api.deliver_outer_event_to_consumer.job_name%' 42 | - '@event_dispatcher' 43 | - '@akeneo_batch.job_repository' 44 | - 45 | - '@pim_events_api.step.deliver_outer_event' 46 | public: false 47 | tags: 48 | - { name: akeneo_batch.job, connector: '%pim_events_api.deliver_outer_event_to_consumer.connector_name%', type: '%pim_events_api.deliver_outer_event_to_consumer.job_type%' } 49 | - name: akeneo_batch.job.not_visible 50 | -------------------------------------------------------------------------------- /Tests/Unit/Model/CreateRemoveProductEventTypePayloadTest.php: -------------------------------------------------------------------------------- 1 | setIdentifier('test-123'); 29 | } 30 | }; 31 | 32 | $expectedPayload = ['identifier' => 'test-123']; 33 | 34 | $actualPayload = (new CreateRemoveProductEventTypePayload())->__invoke($removeProductEvent); 35 | self::assertSame($expectedPayload, $actualPayload); 36 | } 37 | 38 | /** 39 | * @test 40 | * @dataProvider notSupportedEventsDataProvider 41 | * 42 | * @param $event 43 | */ 44 | public function throwsInvalidArgumentExceptionIfSubjectDoesNotContainsIdentifierProperty($event): void 45 | { 46 | $this->expectException(InvalidArgumentException::class); 47 | 48 | (new CreateRemoveProductEventTypePayload())->__invoke($event); 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function notSupportedEventsDataProvider(): array 55 | { 56 | $removeCategoryEvent = new class implements GenericRemoveEntityEventInterface { 57 | public function getSubject(): Category 58 | { 59 | return new Category(); 60 | } 61 | }; 62 | 63 | $removeAttributeEvent = new class implements GenericRemoveEntityEventInterface { 64 | public function getSubject(): Attribute 65 | { 66 | return new Attribute(); 67 | } 68 | }; 69 | 70 | $removeFamilyEvent = new class implements GenericRemoveEntityEventInterface { 71 | public function getSubject(): Family 72 | { 73 | return new Family(); 74 | } 75 | }; 76 | 77 | $removeProductModelEvent = new class implements GenericRemoveEntityEventInterface { 78 | public function getSubject(): ProductModel 79 | { 80 | return new ProductModel(); 81 | } 82 | }; 83 | 84 | return [ 85 | [$removeCategoryEvent], 86 | [$removeAttributeEvent], 87 | [$removeFamilyEvent], 88 | [$removeProductModelEvent] 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Unit/Model/CreateRemoveEventTypePayloadTest.php: -------------------------------------------------------------------------------- 1 | $entityCode]; 29 | 30 | $actualPayload = (new CreateRemoveEventTypePayload())->__invoke($event); 31 | self::assertSame($expectedPayload, $actualPayload); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function throwsInvalidArgumentExceptionIfSubjectDoesNotContainsCodeProperty(): void 38 | { 39 | $removeProductEvent = new class implements GenericRemoveEntityEventInterface { 40 | public function getSubject(): Product 41 | { 42 | return (new Product()); 43 | } 44 | }; 45 | 46 | $this->expectException(InvalidArgumentException::class); 47 | 48 | (new CreateRemoveEventTypePayload())->__invoke($removeProductEvent); 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function supportedEventsDataProvider(): array 55 | { 56 | $removeCategoryEvent = new class implements GenericRemoveEntityEventInterface { 57 | public function getSubject(): Category 58 | { 59 | return (new Category())->setCode('categoryCode'); 60 | } 61 | }; 62 | 63 | $removeAttributeEvent = new class implements GenericRemoveEntityEventInterface { 64 | public function getSubject(): Attribute 65 | { 66 | return (new Attribute())->setCode('attributeCode'); 67 | } 68 | }; 69 | 70 | $removeFamilyEvent = new class implements GenericRemoveEntityEventInterface { 71 | public function getSubject(): Family 72 | { 73 | return (new Family())->setCode('familyCode'); 74 | } 75 | }; 76 | 77 | $removeProductModelEvent = new class implements GenericRemoveEntityEventInterface { 78 | public function getSubject(): ProductModel 79 | { 80 | $productModel = new ProductModel(); 81 | $productModel->setCode('productModelCode'); 82 | return $productModel; 83 | } 84 | }; 85 | 86 | return [ 87 | [$removeCategoryEvent, 'categoryCode'], 88 | [$removeAttributeEvent, 'attributeCode'], 89 | [$removeFamilyEvent, 'familyCode'], 90 | [$removeProductModelEvent, 'productModelCode'] 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /EventSubscriber/AkeneoStorageUtilsSubscriber.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 39 | $this->logger = $logger; 40 | $this->entitiesToBeCreated = []; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public static function getSubscribedEvents() 47 | { 48 | return [ 49 | StorageEvents::PRE_SAVE => 'preSave', 50 | StorageEvents::POST_SAVE => 'postSave', 51 | StorageEvents::POST_REMOVE => 'postRemove' 52 | ]; 53 | } 54 | 55 | /** 56 | * @param GenericEvent $genericEvent 57 | */ 58 | public function preSave(GenericEvent $genericEvent): void 59 | { 60 | $entity = $genericEvent->getSubject(); 61 | $entityHash = spl_object_hash($entity); 62 | 63 | if (!isset($this->entitiesToBeCreated[$entityHash])) { 64 | if ($entity instanceof VersionableInterface && is_null($entity->getId())) { 65 | $this->entitiesToBeCreated[$entityHash] = true; 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * @param GenericEvent $genericEvent 72 | */ 73 | public function postSave(GenericEvent $genericEvent): void 74 | { 75 | $entity = $genericEvent->getSubject(); 76 | $entityHash = spl_object_hash($entity); 77 | 78 | if (isset($this->entitiesToBeCreated[$entityHash])) { 79 | $event = CreateEntityEventAdapter::createFromGenericEvent($genericEvent); 80 | } else { 81 | $event = UpdateEntityEventAdapter::createFromGenericEvent($genericEvent); 82 | } 83 | 84 | $this->handleEvent($event); 85 | } 86 | 87 | /** 88 | * @param RemoveEvent $removeEvent 89 | */ 90 | public function postRemove(RemoveEvent $removeEvent): void 91 | { 92 | $this->handleEvent(RemoveEntityEventAdapter::createFromRemoveEvent($removeEvent)); 93 | } 94 | 95 | /** 96 | * @param GenericEventInterface $event 97 | */ 98 | private function handleEvent(GenericEventInterface $event): void 99 | { 100 | try { 101 | $this->handler->handle($event); 102 | } catch (Throwable $e) { 103 | $this->logger->error($e->getMessage()); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | pim_events_api.event_subscriber.akeneo_storage_utils_subscriber.class: Trilix\EventsApiBundle\EventSubscriber\AkeneoStorageUtilsSubscriber 3 | pim_events_api.events_handler.class: Trilix\EventsApiBundle\Model\EventsHandler 4 | pim_events_api.outer_event_builder.class: Trilix\EventsApiBundle\OuterEvent\OuterEventBuilder 5 | pim_events_api.create_event_type_payload.class: Trilix\EventsApiBundle\Model\CreateEventTypePayload 6 | pim_events_api.create_remove_product_event_type_payload.class: Trilix\EventsApiBundle\Model\CreateRemoveProductEventTypePayload 7 | pim_events_api.create_remove_event_type_payload.class: Trilix\EventsApiBundle\Model\CreateRemoveEventTypePayload 8 | pim_events_api.resolve_event_type.class: Trilix\EventsApiBundle\Model\ResolveEventType 9 | pim_events_api.event_type_configuration_list.class: Trilix\EventsApiBundle\Model\EventTypeConfigurationList 10 | pim_events_api.akeneo_batch_outer_event_dispatcher.class: Trilix\EventsApiBundle\Model\AkeneoBatchOuterEventDispatcher 11 | pim_events_api.guzzle.http_client_factory.class: Trilix\EventsApiBundle\Guzzle\GuzzleHttpClientFactory 12 | pim_events_api.guzzle.request_factory.class: Trilix\EventsApiBundle\Guzzle\GuzzleRequestFactory 13 | pim_events_api.transport_factory.http.class: Trilix\EventsApiBundle\Transport\HttpTransportFactory 14 | pim_events_api.transport_factory.iftt_webhooks.class: Trilix\EventsApiBundle\Transport\IFTTTWebHooksTransportFactory 15 | 16 | services: 17 | pim_events_api.akeneo_batch_outer_event_dispatcher: 18 | class: '%pim_events_api.akeneo_batch_outer_event_dispatcher.class%' 19 | arguments: 20 | - '@security.token_storage' 21 | - '@akeneo_batch.job.job_instance_repository' 22 | - '@akeneo_batch_queue.launcher.queue_job_launcher' 23 | 24 | pim_events_api.event_subscriber.akeneo_storage_utils_subscriber: 25 | class: '%pim_events_api.event_subscriber.akeneo_storage_utils_subscriber.class%' 26 | arguments: 27 | - '@pim_events_api.events_handler' 28 | - '@logger' 29 | tags: 30 | - { name: kernel.event_subscriber, priority: -255 } 31 | 32 | pim_events_api.guzzle.http_client_factory: 33 | class: '%pim_events_api.guzzle.http_client_factory.class%' 34 | 35 | pim_events_api.guzzle.request_factory: 36 | class: '%pim_events_api.guzzle.request_factory.class%' 37 | 38 | pim_events_api.events_handler: 39 | class: '%pim_events_api.events_handler.class%' 40 | arguments: 41 | - '@pim_events_api.resolve_event_type' 42 | - '@pim_events_api.outer_event_builder' 43 | - '@pim_events_api.akeneo_batch_outer_event_dispatcher' 44 | - '@logger' 45 | 46 | pim_events_api.resolve_event_type: 47 | class: '%pim_events_api.resolve_event_type.class%' 48 | arguments: 49 | - '@pim_events_api.event_type_configuration_list' 50 | 51 | pim_events_api.outer_event_builder: 52 | class: '%pim_events_api.outer_event_builder.class%' 53 | 54 | pim_events_api.create_event_type_payload: 55 | class: '%pim_events_api.create_event_type_payload.class%' 56 | arguments: 57 | - '@pim_external_api_serializer' 58 | 59 | pim_events_api.create_remove_product_event_type_payload: 60 | class: '%pim_events_api.create_remove_product_event_type_payload.class%' 61 | 62 | pim_events_api.create_remove_event_type_payload: 63 | class: '%pim_events_api.create_remove_event_type_payload.class%' 64 | 65 | pim_events_api.event_type_configuration_list: 66 | class: '%pim_events_api.event_type_configuration_list.class%' 67 | 68 | pim_events_api.transport_factory.http: 69 | class: '%pim_events_api.transport_factory.http.class%' 70 | arguments: 71 | - '@pim_events_api.guzzle.http_client_factory' 72 | - '@pim_events_api.guzzle.request_factory' 73 | 74 | pim_events_api.transport_factory.ifttt_webhooks: 75 | class: '%pim_events_api.transport_factory.iftt_webhooks.class%' 76 | arguments: 77 | - '@pim_events_api.guzzle.http_client_factory' 78 | - '@pim_events_api.guzzle.request_factory' 79 | -------------------------------------------------------------------------------- /Tests/Unit/Model/AkeneoBatchOuterEventDispatcherTest.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $this->createMock(TokenStorageInterface::class); 39 | $this->jobInstanceRepository = $this->createMock(IdentifiableObjectRepositoryInterface::class); 40 | $this->jobLauncher = $this->createMock(JobLauncherInterface::class); 41 | 42 | $this->dispatcher = new AkeneoBatchOuterEventDispatcher( 43 | $this->tokenStorage, 44 | $this->jobInstanceRepository, 45 | $this->jobLauncher 46 | ); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function notLaunchesJobIfTokenIsMissing(): void 53 | { 54 | $this->tokenStorage->expects(self::once())->method('getToken')->willReturn(null); 55 | $this->jobInstanceRepository->expects(self::once())->method('findOneByIdentifier') 56 | ->with('deliver_outer_event_to_consumer')->willReturn(new JobInstance()); 57 | $this->jobLauncher->expects(self::never())->method('launch'); 58 | 59 | $outerEvent = new OuterEvent('foo_bar_event', ['foo' => 'bar'], time()); 60 | 61 | $this->dispatcher->dispatch($outerEvent); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function notLaunchesJobIfJobInstanceIsNotSetup(): void 68 | { 69 | $token = $this->createMock(TokenInterface::class); 70 | 71 | $this->tokenStorage->expects(self::once())->method('getToken')->willReturn($token); 72 | $this->jobInstanceRepository->expects(self::once())->method('findOneByIdentifier') 73 | ->with('deliver_outer_event_to_consumer')->willReturn(null); 74 | $this->jobLauncher->expects(self::never())->method('launch'); 75 | 76 | $outerEvent = new OuterEvent('foo_bar_event', ['foo' => 'bar'], time()); 77 | 78 | $this->dispatcher->dispatch($outerEvent); 79 | } 80 | 81 | /** 82 | * @test 83 | * @throws \ReflectionException 84 | */ 85 | public function launchesJob(): void 86 | { 87 | $user = $this->createMock(UserInterface::class); 88 | $token = $this->createMock(TokenInterface::class); 89 | $token->expects(self::once())->method('getUser')->willReturn($user); 90 | $jobInstance = new JobInstance(); 91 | 92 | $this->tokenStorage->expects(self::once())->method('getToken')->willReturn($token); 93 | $this->jobInstanceRepository->expects(self::once())->method('findOneByIdentifier') 94 | ->with('deliver_outer_event_to_consumer')->willReturn($jobInstance); 95 | 96 | $eventTime = time(); 97 | $outerEvent = new OuterEvent('foo', ['foo' => 'bar'], $eventTime); 98 | 99 | $this->jobLauncher->expects(self::once())->method('launch') 100 | ->with( 101 | $jobInstance, 102 | $user, 103 | ['outer_event' => ['event_type' => 'foo', 'payload' => ['foo' => 'bar'], 'event_time' => $eventTime]] 104 | ); 105 | 106 | $this->dispatcher->dispatch($outerEvent); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Tests/Unit/Model/ResolveEventTypeTest.php: -------------------------------------------------------------------------------- 1 | addEventTypeConfiguration( 25 | new class implements EventTypeConfigurationInterface { 26 | public function resolve(GenericEventInterface $event): EventType 27 | { 28 | throw new EventIsNotSupportedException('Event is not supported'); 29 | } 30 | } 31 | ); 32 | 33 | $eventType = (new ResolveEventType($eventTypeConfigurationList)) 34 | ->__invoke( 35 | new class implements GenericEventInterface { 36 | public function getSubject() 37 | { 38 | return new isNotSupportedEntity(); 39 | } 40 | } 41 | ); 42 | 43 | self::assertNull($eventType); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function ensureFirstlyResolvedEventTypeBeingReturned(): void 50 | { 51 | $eventTypeConfigurationList = new EventTypeConfigurationList(); 52 | $eventTypeConfigurationList->addEventTypeConfiguration( 53 | new class implements EventTypeConfigurationInterface { 54 | public function resolve(GenericEventInterface $event): EventType 55 | { 56 | return new EventType('bar', ['bar' => 'is_supported']); 57 | } 58 | } 59 | ); 60 | $eventTypeConfigurationList->addEventTypeConfiguration( 61 | new class implements EventTypeConfigurationInterface { 62 | public function resolve(GenericEventInterface $event): EventType 63 | { 64 | return new EventType('foo', ['foo' => 'is_supported']); 65 | } 66 | } 67 | ); 68 | 69 | $eventType = (new ResolveEventType($eventTypeConfigurationList)) 70 | ->__invoke( 71 | new class implements GenericEventInterface { 72 | public function getSubject() 73 | { 74 | return new isSupportedEntity(); 75 | } 76 | } 77 | ); 78 | 79 | self::assertSame('bar', $eventType->getName()); 80 | self::assertNotEmpty($eventType->getPayload()); 81 | self::assertArrayHasKey('bar', $eventType->getPayload()); 82 | self::assertSame('is_supported', $eventType->getPayload()['bar']); 83 | } 84 | 85 | 86 | /** 87 | * @test 88 | */ 89 | public function resolvesEventType(): void 90 | { 91 | $eventTypeConfigurationList = new EventTypeConfigurationList(); 92 | $eventTypeConfigurationList->addEventTypeConfiguration( 93 | new class implements EventTypeConfigurationInterface { 94 | public function resolve(GenericEventInterface $event): EventType 95 | { 96 | throw new EventIsNotSupportedException('Event is not supported'); 97 | } 98 | } 99 | ); 100 | $eventTypeConfigurationList->addEventTypeConfiguration( 101 | new class implements EventTypeConfigurationInterface { 102 | public function resolve(GenericEventInterface $event): EventType 103 | { 104 | return new EventType('foo', ['foo' => 'is_supported']); 105 | } 106 | } 107 | ); 108 | 109 | $eventType = (new ResolveEventType($eventTypeConfigurationList)) 110 | ->__invoke( 111 | new class implements GenericEventInterface { 112 | public function getSubject() 113 | { 114 | return new isSupportedEntity(); 115 | } 116 | } 117 | ); 118 | 119 | self::assertSame('foo', $eventType->getName()); 120 | self::assertNotEmpty($eventType->getPayload()); 121 | self::assertArrayHasKey('foo', $eventType->getPayload()); 122 | self::assertSame('is_supported', $eventType->getPayload()['foo']); 123 | } 124 | } 125 | 126 | class isNotSupportedEntity { 127 | } 128 | 129 | class isSupportedEntity extends AbstractProduct { 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akeneo PIM Events API 2 | 3 | The Events API is a smooth and easy way to build integrations that respond to activities in Akeneo PIM. 4 | All you need is PIM Events API Bundle and an endpoint where to send Akeneo PIM events. 5 | 6 | ## Table of contents 7 | * [Getting Started](#Getting-Started) 8 | * [Functionality](#Functionality) 9 | * [License](#License) 10 | 11 | ## Getting Started 12 | 13 | ### Requirements 14 | 15 | * Akeneo PIM >= 5.0 (CE & EE) 16 | 17 | ### Installation 18 | 19 | Install via composer: 20 | 21 | ```bash 22 | php composer.phar require trilix/akeneo-events-api-bundle:^0.7.0 23 | ``` 24 | 25 | To enable the bundle add to the *config/bundles.php* file: 26 | 27 | ```php 28 | return [ 29 | // ... 30 | Trilix\EventsApiBundle\TrilixEventsApiBundle::class => ['all' => true] 31 | ] 32 | ``` 33 | 34 | Add the following line at the end of env file: 35 | 36 | ```yaml 37 | EVENTS_API_REQUEST_URL=your_request_url 38 | ``` 39 | 40 | where `your_request_url` is a target location where all the events (see [event types](#Event-types-delivered-over-Events-API)) will be delivered. 41 | 42 | Create file *config/packages/trilix_events_api.yml* with the following: 43 | 44 | ```yaml 45 | trilix_events_api: 46 | transport: 47 | factory: "pim_events_api.transport_factory.http" 48 | options: 49 | request_url: "%env(EVENTS_API_REQUEST_URL)%" 50 | ``` 51 | 52 | Clear cache: 53 | 54 | ```bash 55 | php bin/console cache:clear --env=prod 56 | ``` 57 | 58 | Run the following command to create a job to deliver events to consumer: 59 | 60 | ```bash 61 | php bin/console akeneo:batch:create-job 'Deliver outer event to consumer' deliver_outer_event_to_consumer internal deliver_outer_event_to_consumer 62 | ``` 63 | 64 | Make sure Akeneo job queue daemon is running. For more information read [Setting up the job queue daemon](https://docs.akeneo.com/latest/install_pim/manual/daemon_queue.html#setting-up-the-job-queue-daemon). 65 | 66 | ## Functionality 67 | 68 | ### How it works 69 | 70 | Some event(s) happens in Akeneo PIM. This triggers a mechanism to send those event(s) as HTTP POST request to your Request URL. 71 | Each request contains event, with correspondent [event type](#Event-types-delivered-over-Events-API) presented in JSON format (see [example](#Example-of-*category_updated*-event)). 72 | 73 | Events API sends one request per one event, and sending of requests happens in real-time. 74 | 75 | ### Event types delivered over Events API 76 | 77 | | **Event** | **Description** | 78 | | --------------------- |:----------------------------------:| 79 | | category_created | New category was created | 80 | | category_updated | Existing category was updated | 81 | | category_removed | Existing category was deleted | 82 | | attribute_created | New attribute was created | 83 | | attribute_updated | Existing attribute was updated | 84 | | attribute_removed | Existing attribute was deleted | 85 | | family_created | New family was created | 86 | | family_updated | Existing family was updated | 87 | | family_removed | Existing family was deleted | 88 | | product_created | New product was created | 89 | | product_updated | Existing product was updated | 90 | | product_removed | Existing product was deleted | 91 | | product_model_created | New product model was created | 92 | | product_model_updated | Existing product model was updated | 93 | | product_model_removed | Existing product model was deleted | 94 | 95 | ### Example of *category_updated* event 96 | 97 | ```json 98 | { 99 | "event_type": "category_updated", 100 | "payload": { 101 | "code": "cameras", 102 | "labels": { 103 | "de_DE": "Cameras", 104 | "en_US": "Cameras new name", 105 | "fr_FR": "Caméras" 106 | }, 107 | "parent": "master" 108 | }, 109 | "event_time": 1565021907 110 | } 111 | ``` 112 | ### Example of *product_model_removed* event 113 | ```json 114 | { 115 | "event_type": "product_model_removed", 116 | "payload": { 117 | "code": "derby" 118 | }, 119 | "event_time": 1579792377 120 | } 121 | ``` 122 | 123 | ### Event Type Structure 124 | 125 | | Field | Type | Description | 126 | | ------------ |:-------:|:----------------------------------------------------------------------------------------:| 127 | | *event_type* | String | Type of event which happened (see [event types](#Event-types-delivered-over-Events-API)) | 128 | | *payload* | Object | Contains information which represents the event. For events related to deletion of entity it contains entity only identifier (identifier value for Products and code for all others) | 129 | | *event_time* | Integer | Timestamp in seconds when the event was created | 130 | 131 | ### Attention :heavy_exclamation_mark: 132 | 133 | If Akeneo family contains variants, then during family update (or it's variants as well), 134 | Akeneo will re-save related products. It will trigger sending *product_updated* events. 135 | 136 | ## License 137 | 138 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 139 | -------------------------------------------------------------------------------- /Tests/Unit/Model/EventsHandlerTest.php: -------------------------------------------------------------------------------- 1 | resolver = $this->createMock(ResolveEventType::class); 45 | $this->builder = $this->createMock(OuterEventBuilder::class); 46 | $this->dispatcher = $this->createMock(OuterEventDispatcherInterface::class); 47 | $this->logger = $this->createMock(LoggerInterface::class); 48 | 49 | $this->handler = new EventsHandler($this->resolver, $this->builder, $this->dispatcher, $this->logger); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function throwsInvalidArgumentExceptionIfGenericEventSubjectIsNotObject(): void 56 | { 57 | $this->expectException(AssertionInvalidArgumentException::class); 58 | 59 | $this->handler->handle( 60 | new class implements GenericEventInterface { 61 | public function getSubject() 62 | { 63 | return 'string'; 64 | } 65 | } 66 | ); 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function stopsHandlingIfEventTypeWasNotResolved(): void 73 | { 74 | $event = new class implements GenericEventInterface { 75 | public function getSubject() 76 | { 77 | return new Subject(); 78 | } 79 | }; 80 | 81 | $this->resolver->expects(self::once())->method('__invoke') 82 | ->with($event)->willReturn(null); 83 | 84 | $this->builder->expects(self::never())->method('withPayload'); 85 | $this->builder->expects(self::never())->method('build'); 86 | 87 | $this->dispatcher->expects(self::never())->method('dispatch'); 88 | 89 | $this->logger->expects(self::never())->method('notice'); 90 | 91 | $this->handler->handle($event); 92 | } 93 | 94 | /** 95 | * @test 96 | */ 97 | public function catchesPayloadCanNotBeCreatedException(): void 98 | { 99 | $event = new class implements GenericEventInterface { 100 | public function getSubject() 101 | { 102 | return new Subject(); 103 | } 104 | }; 105 | 106 | $this->resolver->expects(self::once())->method('__invoke') 107 | ->with($event)->willThrowException(new PayloadCanNotBeCreatedException('testMessage')); 108 | 109 | $this->logger->expects(self::once()) 110 | ->method('notice') 111 | ->with('testMessage'); 112 | 113 | $this->handler->handle($event); 114 | } 115 | 116 | /** 117 | * @test 118 | */ 119 | public function passesThrownExceptionNext(): void 120 | { 121 | $event = new class implements GenericEventInterface { 122 | public function getSubject() 123 | { 124 | return new Subject(); 125 | } 126 | }; 127 | 128 | $this->resolver->expects(self::once())->method('__invoke') 129 | ->with($event)->willThrowException(new RuntimeException('testMessage')); 130 | 131 | $this->expectException(RuntimeException::class); 132 | $this->expectExceptionMessage('testMessage'); 133 | 134 | $this->logger->expects(self::never()) 135 | ->method('notice'); 136 | 137 | $this->handler->handle($event); 138 | } 139 | 140 | /** 141 | * @test 142 | */ 143 | public function handlesEvent(): void 144 | { 145 | $event = new class implements GenericEventInterface { 146 | public function getSubject() 147 | { 148 | return new Subject(); 149 | } 150 | }; 151 | 152 | $payload = ['foo' => 'bar']; 153 | $eventType = new EventType('test_outer_event', $payload); 154 | $outerEvent = new OuterEvent('test_outer_event', ['foo' => 'bar'], time()); 155 | 156 | $this->resolver->expects(self::once())->method('__invoke')->with($event)->willReturn($eventType); 157 | 158 | $this->builder->expects(self::once())->method('withPayload') 159 | ->with($payload)->willReturnSelf(); 160 | $this->builder->expects(self::once())->method('build') 161 | ->with('test_outer_event')->willReturn($outerEvent); 162 | 163 | $this->dispatcher->expects(self::once())->method('dispatch')->with($outerEvent); 164 | 165 | $this->logger->expects(self::never()) 166 | ->method('notice'); 167 | 168 | $this->handler->handle($event); 169 | } 170 | } 171 | 172 | class Subject {} 173 | -------------------------------------------------------------------------------- /Tests/Unit/EventSubscriber/AkeneoStorageUtilsSubscriberTest.php: -------------------------------------------------------------------------------- 1 | eventsHandler = $this->createMock(EventsHandler::class); 37 | 38 | $this->logger = $this->createMock(LoggerInterface::class); 39 | 40 | $this->subscriber = new AkeneoStorageUtilsSubscriber($this->eventsHandler, $this->logger); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function newEntityIdentifiedAndMarked(): void 47 | { 48 | $subject = new VersionableObject(null); 49 | 50 | $preSaveEvent = new GenericEvent($subject, ['foo' => 'bar']); 51 | $postSaveEvent = new GenericEvent($subject, ['bar' => 'foo']); 52 | 53 | $this->eventsHandler->expects(self::once())->method('handle') 54 | ->with( 55 | $this->logicalAnd( 56 | $this->isInstanceOf(GenericCreateEntityEventInterface::class), 57 | $this->callback( 58 | function (GenericCreateEntityEventInterface $event) use ($subject): bool { 59 | return $event->getSubject() === $subject; 60 | } 61 | ) 62 | ) 63 | ); 64 | 65 | $this->subscriber->preSave($preSaveEvent); 66 | 67 | $subject->setId(8); 68 | 69 | $this->subscriber->postSave($postSaveEvent); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function existingEntityDoesNotMarked(): void 76 | { 77 | $subject = new VersionableObject(4); 78 | 79 | $preSaveEvent = new GenericEvent($subject, ['foo' => 'bar']); 80 | $postSaveEvent = new GenericEvent($subject, ['bar' => 'foo']); 81 | 82 | $this->eventsHandler->expects(self::once())->method('handle') 83 | ->with( 84 | $this->logicalAnd( 85 | $this->isInstanceOf(GenericUpdateEntityEventInterface::class), 86 | $this->callback( 87 | function (GenericUpdateEntityEventInterface $event) use ($subject): bool { 88 | return $event->getSubject() === $subject; 89 | } 90 | ) 91 | ) 92 | ); 93 | 94 | $this->subscriber->preSave($preSaveEvent); 95 | $this->subscriber->postSave($postSaveEvent); 96 | } 97 | 98 | /** 99 | * @test 100 | */ 101 | public function handlesStorageRemoveEvent(): void 102 | { 103 | $subject = new VersionableObject(7); 104 | 105 | $this->eventsHandler->expects(self::once())->method('handle') 106 | ->with( 107 | $this->logicalAnd( 108 | $this->isInstanceOf(GenericRemoveEntityEventInterface::class), 109 | $this->callback( 110 | function (GenericRemoveEntityEventInterface $event) use ($subject): bool { 111 | return $event->getSubject() === $subject; 112 | } 113 | ) 114 | ) 115 | ); 116 | 117 | $this->subscriber->postRemove(new RemoveEvent($subject, 7, ['foo' => 'bar'])); 118 | } 119 | 120 | /** 121 | * @test 122 | */ 123 | public function expectedExceptionIsCatchedDuringPostSave(): void 124 | { 125 | $subject = new VersionableObject(4); 126 | 127 | $postSaveEvent = new GenericEvent($subject, ['bar' => 'foo']); 128 | 129 | $this->eventsHandler->expects(self::once())->method('handle') 130 | ->willThrowException(new \Exception('testMessage')); 131 | 132 | $this->logger->expects(self::once()) 133 | ->method('error') 134 | ->with('testMessage'); 135 | 136 | $this->subscriber->postSave($postSaveEvent); 137 | } 138 | 139 | /** 140 | * @test 141 | */ 142 | public function expectedExceptionIsCatchedDuringPostRemove(): void 143 | { 144 | $subject = new VersionableObject(7); 145 | 146 | $this->eventsHandler->expects(self::once())->method('handle') 147 | ->willThrowException(new \Exception('testMessage')); 148 | 149 | $this->logger->expects(self::once()) 150 | ->method('error') 151 | ->with('testMessage'); 152 | 153 | $this->subscriber->postRemove(new RemoveEvent($subject, 7, ['foo' => 'bar'])); 154 | } 155 | } 156 | 157 | class VersionableObject implements VersionableInterface 158 | { 159 | /** @var int|null */ 160 | private $id; 161 | 162 | /** 163 | * VersionableObject constructor. 164 | * @param int|null $id 165 | */ 166 | public function __construct(?int $id) 167 | { 168 | $this->id = $id; 169 | } 170 | 171 | /** 172 | * @return int|string|null 173 | */ 174 | public function getId() 175 | { 176 | return $this->id; 177 | } 178 | 179 | public function setId(int $id): void 180 | { 181 | $this->id = $id; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Resources/config/events.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pim_events_api.event_type_configuration.category_created: 3 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 4 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'categoryCreatedEventTypeConfiguration'] 5 | arguments: ['@pim_events_api.create_event_type_payload'] 6 | tags: 7 | - { name: 'pim_events_api.event_type_configuration' } 8 | 9 | pim_events_api.event_type_configuration.category_updated: 10 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 11 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'categoryUpdatedEventTypeConfiguration'] 12 | arguments: ['@pim_events_api.create_event_type_payload'] 13 | tags: 14 | - { name: 'pim_events_api.event_type_configuration' } 15 | 16 | pim_events_api.event_type_configuration.category_removed: 17 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 18 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'categoryRemovedEventTypeConfiguration'] 19 | arguments: ['@pim_events_api.create_remove_event_type_payload'] 20 | tags: 21 | - { name: 'pim_events_api.event_type_configuration' } 22 | 23 | pim_events_api.event_type_configuration.attribute_created: 24 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 25 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'attributeCreatedEventTypeConfiguration'] 26 | arguments: ['@pim_events_api.create_event_type_payload'] 27 | tags: 28 | - { name: 'pim_events_api.event_type_configuration' } 29 | 30 | pim_events_api.event_type_configuration.attribute_updated: 31 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 32 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'attributeUpdatedEventTypeConfiguration'] 33 | arguments: ['@pim_events_api.create_event_type_payload'] 34 | tags: 35 | - { name: 'pim_events_api.event_type_configuration' } 36 | 37 | pim_events_api.event_type_configuration.attribute_removed: 38 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 39 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'attributeRemovedEventTypeConfiguration'] 40 | arguments: ['@pim_events_api.create_remove_event_type_payload'] 41 | tags: 42 | - { name: 'pim_events_api.event_type_configuration' } 43 | 44 | pim_events_api.event_type_configuration.family_created: 45 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 46 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'familyCreatedEventTypeConfiguration'] 47 | arguments: ['@pim_events_api.create_event_type_payload'] 48 | tags: 49 | - { name: 'pim_events_api.event_type_configuration' } 50 | 51 | pim_events_api.event_type_configuration.family_updated: 52 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 53 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'familyUpdatedEventTypeConfiguration'] 54 | arguments: ['@pim_events_api.create_event_type_payload'] 55 | tags: 56 | - { name: 'pim_events_api.event_type_configuration' } 57 | 58 | pim_events_api.event_type_configuration.family_removed: 59 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 60 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'familyRemovedEventTypeConfiguration'] 61 | arguments: ['@pim_events_api.create_remove_event_type_payload'] 62 | tags: 63 | - { name: 'pim_events_api.event_type_configuration' } 64 | 65 | pim_events_api.event_type_configuration.product_created: 66 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 67 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'productCreatedEventTypeConfiguration'] 68 | arguments: ['@pim_events_api.create_event_type_payload'] 69 | tags: 70 | - { name: 'pim_events_api.event_type_configuration' } 71 | 72 | pim_events_api.event_type_configuration.product_updated: 73 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 74 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'productUpdatedEventTypeConfiguration'] 75 | arguments: ['@pim_events_api.create_event_type_payload'] 76 | tags: 77 | - { name: 'pim_events_api.event_type_configuration' } 78 | 79 | pim_events_api.event_type_configuration.product_removed: 80 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 81 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'productRemovedEventTypeConfiguration'] 82 | arguments: ['@pim_events_api.create_remove_product_event_type_payload'] 83 | tags: 84 | - { name: 'pim_events_api.event_type_configuration' } 85 | 86 | pim_events_api.event_type_configuration.product_model_created: 87 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 88 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'productModelCreatedEventTypeConfiguration'] 89 | arguments: ['@pim_events_api.create_event_type_payload'] 90 | tags: 91 | - { name: 'pim_events_api.event_type_configuration' } 92 | 93 | pim_events_api.event_type_configuration.product_model_updated: 94 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 95 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'productModelUpdatedEventTypeConfiguration'] 96 | arguments: ['@pim_events_api.create_event_type_payload'] 97 | tags: 98 | - { name: 'pim_events_api.event_type_configuration' } 99 | 100 | pim_events_api.event_type_configuration.product_model_removed: 101 | class: Trilix\EventsApiBundle\EventType\EventTypeConfiguration 102 | factory: ['Trilix\EventsApiBundle\Model\PimEventTypeConfigurationFactories', 'productModelRemovedEventTypeConfiguration'] 103 | arguments: ['@pim_events_api.create_remove_event_type_payload'] 104 | tags: 105 | - { name: 'pim_events_api.event_type_configuration' } 106 | -------------------------------------------------------------------------------- /Model/PimEventTypeConfigurationFactories.php: -------------------------------------------------------------------------------- 1 | getSubject() instanceof CategoryInterface && $event instanceof GenericCreateEntityEventInterface; 27 | }, 28 | $createEventTypePayload 29 | ); 30 | } 31 | 32 | /** 33 | * @param CreateEventTypePayload $createEventTypePayload 34 | * @return EventTypeConfigurationInterface 35 | */ 36 | public static function categoryUpdatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 37 | { 38 | return new EventTypeConfiguration( 39 | EntityEventTypes::CATEGORY_UPDATED, 40 | static function (GenericEventInterface $event) { 41 | return $event->getSubject() instanceof CategoryInterface && $event instanceof GenericUpdateEntityEventInterface; 42 | }, 43 | $createEventTypePayload 44 | ); 45 | } 46 | 47 | /** 48 | * @param CreateRemoveEventTypePayload $createEventTypePayload 49 | * @return EventTypeConfigurationInterface 50 | */ 51 | public static function categoryRemovedEventTypeConfiguration(CreateRemoveEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 52 | { 53 | return new EventTypeConfiguration( 54 | EntityEventTypes::CATEGORY_REMOVED, 55 | static function (GenericEventInterface $event) { 56 | return $event->getSubject() instanceof CategoryInterface && $event instanceof GenericRemoveEntityEventInterface; 57 | }, 58 | $createEventTypePayload 59 | ); 60 | } 61 | 62 | /** 63 | * @param CreateEventTypePayload $createEventTypePayload 64 | * @return EventTypeConfigurationInterface 65 | */ 66 | public static function attributeCreatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 67 | { 68 | return new EventTypeConfiguration( 69 | EntityEventTypes::ATTRIBUTE_CREATED, 70 | static function (GenericEventInterface $event) { 71 | return $event->getSubject() instanceof AttributeInterface && $event instanceof GenericCreateEntityEventInterface; 72 | }, 73 | $createEventTypePayload 74 | ); 75 | } 76 | 77 | /** 78 | * @param CreateEventTypePayload $createEventTypePayload 79 | * @return EventTypeConfigurationInterface 80 | */ 81 | public static function attributeUpdatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 82 | { 83 | return new EventTypeConfiguration( 84 | EntityEventTypes::ATTRIBUTE_UPDATED, 85 | static function (GenericEventInterface $event) { 86 | return $event->getSubject() instanceof AttributeInterface && $event instanceof GenericUpdateEntityEventInterface; 87 | }, 88 | $createEventTypePayload 89 | ); 90 | } 91 | 92 | /** 93 | * @param CreateRemoveEventTypePayload $createEventTypePayload 94 | * @return EventTypeConfigurationInterface 95 | */ 96 | public static function attributeRemovedEventTypeConfiguration(CreateRemoveEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 97 | { 98 | return new EventTypeConfiguration( 99 | EntityEventTypes::ATTRIBUTE_REMOVED, 100 | static function (GenericEventInterface $event) { 101 | return $event->getSubject() instanceof AttributeInterface && $event instanceof GenericRemoveEntityEventInterface; 102 | }, 103 | $createEventTypePayload 104 | ); 105 | } 106 | 107 | /** 108 | * @param CreateEventTypePayload $createEventTypePayload 109 | * @return EventTypeConfigurationInterface 110 | */ 111 | public static function familyCreatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 112 | { 113 | return new EventTypeConfiguration( 114 | EntityEventTypes::FAMILY_CREATED, 115 | static function (GenericEventInterface $event) { 116 | return $event->getSubject() instanceof FamilyInterface && $event instanceof GenericCreateEntityEventInterface; 117 | }, 118 | $createEventTypePayload 119 | ); 120 | } 121 | 122 | /** 123 | * @param CreateEventTypePayload $createEventTypePayload 124 | * @return EventTypeConfigurationInterface 125 | */ 126 | public static function familyUpdatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 127 | { 128 | return new EventTypeConfiguration( 129 | EntityEventTypes::FAMILY_UPDATED, 130 | static function (GenericEventInterface $event) { 131 | return $event->getSubject() instanceof FamilyInterface && $event instanceof GenericUpdateEntityEventInterface; 132 | }, 133 | $createEventTypePayload 134 | ); 135 | } 136 | 137 | /** 138 | * @param CreateRemoveEventTypePayload $createEventTypePayload 139 | * @return EventTypeConfigurationInterface 140 | */ 141 | public static function familyRemovedEventTypeConfiguration(CreateRemoveEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 142 | { 143 | return new EventTypeConfiguration( 144 | EntityEventTypes::FAMILY_REMOVED, 145 | static function (GenericEventInterface $event) { 146 | return $event->getSubject() instanceof FamilyInterface && $event instanceof GenericRemoveEntityEventInterface; 147 | }, 148 | $createEventTypePayload 149 | ); 150 | } 151 | 152 | /** 153 | * @param CreateEventTypePayload $createEventTypePayload 154 | * @return EventTypeConfigurationInterface 155 | */ 156 | public static function productCreatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 157 | { 158 | return new EventTypeConfiguration( 159 | EntityEventTypes::PRODUCT_CREATED, 160 | static function (GenericEventInterface $event) { 161 | return $event->getSubject() instanceof ProductInterface && $event instanceof GenericCreateEntityEventInterface; 162 | }, 163 | $createEventTypePayload 164 | ); 165 | } 166 | 167 | /** 168 | * @param CreateEventTypePayload $createEventTypePayload 169 | * @return EventTypeConfigurationInterface 170 | */ 171 | public static function productUpdatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 172 | { 173 | return new EventTypeConfiguration( 174 | EntityEventTypes::PRODUCT_UPDATED, 175 | static function (GenericEventInterface $event) { 176 | return $event->getSubject() instanceof ProductInterface && $event instanceof GenericUpdateEntityEventInterface; 177 | }, 178 | $createEventTypePayload 179 | ); 180 | } 181 | 182 | /** 183 | * @param CreateRemoveProductEventTypePayload $createEventTypePayload 184 | * @return EventTypeConfigurationInterface 185 | */ 186 | public static function productRemovedEventTypeConfiguration(CreateRemoveProductEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 187 | { 188 | return new EventTypeConfiguration( 189 | EntityEventTypes::PRODUCT_REMOVED, 190 | static function (GenericEventInterface $event) { 191 | return $event->getSubject() instanceof ProductInterface && $event instanceof GenericRemoveEntityEventInterface; 192 | }, 193 | $createEventTypePayload 194 | ); 195 | } 196 | 197 | /** 198 | * @param CreateEventTypePayload $createEventTypePayload 199 | * @return EventTypeConfigurationInterface 200 | */ 201 | public static function productModelCreatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 202 | { 203 | return new EventTypeConfiguration( 204 | EntityEventTypes::PRODUCT_MODEL_CREATED, 205 | static function (GenericEventInterface $event) { 206 | return $event->getSubject() instanceof ProductModelInterface && $event instanceof GenericCreateEntityEventInterface; 207 | }, 208 | $createEventTypePayload 209 | ); 210 | } 211 | 212 | /** 213 | * @param CreateEventTypePayload $createEventTypePayload 214 | * @return EventTypeConfigurationInterface 215 | */ 216 | public static function productModelUpdatedEventTypeConfiguration(CreateEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 217 | { 218 | return new EventTypeConfiguration( 219 | EntityEventTypes::PRODUCT_MODEL_UPDATED, 220 | static function (GenericEventInterface $event) { 221 | return $event->getSubject() instanceof ProductModelInterface && $event instanceof GenericUpdateEntityEventInterface; 222 | }, 223 | $createEventTypePayload 224 | ); 225 | } 226 | 227 | /** 228 | * @param CreateRemoveEventTypePayload $createEventTypePayload 229 | * @return EventTypeConfigurationInterface 230 | */ 231 | public static function productModelRemovedEventTypeConfiguration(CreateRemoveEventTypePayload $createEventTypePayload): EventTypeConfigurationInterface 232 | { 233 | return new EventTypeConfiguration( 234 | EntityEventTypes::PRODUCT_MODEL_REMOVED, 235 | static function (GenericEventInterface $event) { 236 | return $event->getSubject() instanceof ProductModelInterface && $event instanceof GenericRemoveEntityEventInterface; 237 | }, 238 | $createEventTypePayload 239 | ); 240 | } 241 | } 242 | --------------------------------------------------------------------------------