Skip to content

Commit

Permalink
Log incoming webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
loevgaard committed Aug 13, 2024
1 parent f59ccc7 commit 3ce43c4
Show file tree
Hide file tree
Showing 16 changed files with 369 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,56 @@

namespace Setono\SyliusPeakPlugin\Controller;

use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerAwareInterface;
use Setono\Doctrine\ORMTrait;
use Setono\PeakWMS\Parser\WebhookParser;
use Setono\PeakWMS\Parser\WebhookParserInterface;
use Setono\SyliusPeakPlugin\Factory\WebhookFactoryInterface;
use Setono\SyliusPeakPlugin\Logger\WebhookLogger;
use Setono\SyliusPeakPlugin\WebhookHandler\WebhookHandlerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

final class HandleWebhookControllerAction
final class HandleWebhookAction
{
use ORMTrait;

public function __construct(
private readonly WebhookParserInterface $webhookParser,
private readonly WebhookHandlerInterface $webhookHandler,
private readonly WebhookFactoryInterface $webhookFactory,
ManagerRegistry $managerRegistry,
) {
$this->managerRegistry = $managerRegistry;
}

public function __invoke(Request $request): JsonResponse
{
$webhook = null;

try {
$webhook = $this->webhookFactory->createFromRequest($request);
$logger = new WebhookLogger($webhook);

if ($this->webhookHandler instanceof LoggerAwareInterface) {
$this->webhookHandler->setLogger($logger);
}

$dataClass = WebhookParser::convertNameToDataClass($request->query->getInt('name'));

$data = $this->webhookParser->parse($request->getContent(), $dataClass);

$this->webhookHandler->handle($data);
} catch (\InvalidArgumentException $e) {
throw new BadRequestHttpException($e->getMessage());
} finally {
if (null !== $webhook) {
$this->getManager($webhook)->persist($webhook);
$this->getManager($webhook)->flush();
}
}

return new JsonResponse(status: Response::HTTP_NO_CONTENT);
Expand Down
15 changes: 15 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Setono\SyliusPeakPlugin\Model\InventoryUpdate;
use Setono\SyliusPeakPlugin\Model\UploadOrderRequest;
use Setono\SyliusPeakPlugin\Model\UploadProductVariantRequest;
use Setono\SyliusPeakPlugin\Model\Webhook;
use Setono\SyliusPeakPlugin\Model\WebhookRegistration;
use Sylius\Component\Resource\Factory\Factory;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
Expand Down Expand Up @@ -64,6 +65,20 @@ private function addResourcesSection(ArrayNodeDefinition $node): void
->end()
->end()
->end()
->arrayNode('webhook')
->addDefaultsIfNotSet()
->children()
->variableNode('options')->end()
->arrayNode('classes')
->addDefaultsIfNotSet()
->children()
->scalarNode('model')->defaultValue(Webhook::class)->cannotBeEmpty()->end()
->scalarNode('repository')->cannotBeEmpty()->end()
->scalarNode('factory')->defaultValue(Factory::class)->end()
->end()
->end()
->end()
->end()
->arrayNode('webhook_registration')
->addDefaultsIfNotSet()
->children()
Expand Down
37 changes: 37 additions & 0 deletions src/Factory/WebhookFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusPeakPlugin\Factory;

use Setono\SyliusPeakPlugin\Model\WebhookInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Webmozart\Assert\Assert;

final class WebhookFactory implements WebhookFactoryInterface
{
public function __construct(private readonly FactoryInterface $decorated)
{
}

public function createNew(): WebhookInterface
{
$obj = $this->decorated->createNew();
Assert::isInstanceOf($obj, WebhookInterface::class);

return $obj;
}

public function createFromRequest(Request $request): WebhookInterface
{
$obj = $this->createNew();
$obj->setMethod($request->getMethod());
$obj->setUrl($request->getUri());
$obj->setHeaders($request->headers->all());

Check failure on line 31 in src/Factory/WebhookFactory.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)

InvalidArgument

src/Factory/WebhookFactory.php:31:26: InvalidArgument: Argument 1 of Setono\SyliusPeakPlugin\Model\WebhookInterface::setHeaders expects array<string, list<null|string>>|null, but array<int|string, array<int, null|string>|null|string> provided (see https://psalm.dev/004)

Check failure on line 31 in src/Factory/WebhookFactory.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: highest | SF~5.4.0)

InvalidArgument

src/Factory/WebhookFactory.php:31:26: InvalidArgument: Argument 1 of Setono\SyliusPeakPlugin\Model\WebhookInterface::setHeaders expects array<string, list<null|string>>|null, but array<int|string, array<int, null|string>|null|string> provided (see https://psalm.dev/004)

Check failure on line 31 in src/Factory/WebhookFactory.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~5.4.0)

InvalidArgument

src/Factory/WebhookFactory.php:31:26: InvalidArgument: Argument 1 of Setono\SyliusPeakPlugin\Model\WebhookInterface::setHeaders expects array<string, list<null|string>>|null, but array<int|string, array<int, null|string>|null|string> provided (see https://psalm.dev/004)

Check failure on line 31 in src/Factory/WebhookFactory.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: highest | SF~5.4.0)

InvalidArgument

src/Factory/WebhookFactory.php:31:26: InvalidArgument: Argument 1 of Setono\SyliusPeakPlugin\Model\WebhookInterface::setHeaders expects array<string, list<null|string>>|null, but array<int|string, array<int, null|string>|null|string> provided (see https://psalm.dev/004)
$obj->setBody($request->getContent());
$obj->setRemoteIp($request->getClientIp());

return $obj;
}
}
19 changes: 19 additions & 0 deletions src/Factory/WebhookFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusPeakPlugin\Factory;

use Setono\SyliusPeakPlugin\Model\WebhookInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\HttpFoundation\Request;

/**
* @extends FactoryInterface<WebhookInterface>
*/
interface WebhookFactoryInterface extends FactoryInterface
{
public function createNew(): WebhookInterface;

public function createFromRequest(Request $request): WebhookInterface;
}
20 changes: 20 additions & 0 deletions src/Logger/WebhookLogger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusPeakPlugin\Logger;

use Psr\Log\AbstractLogger;
use Setono\SyliusPeakPlugin\Model\WebhookInterface;

final class WebhookLogger extends AbstractLogger
{
public function __construct(private readonly WebhookInterface $webhook)
{
}

public function log($level, \Stringable|string $message, array $context = []): void

Check failure on line 16 in src/Logger/WebhookLogger.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)

MethodSignatureMismatch

src/Logger/WebhookLogger.php:16:52: MethodSignatureMismatch: Argument 2 of Setono\SyliusPeakPlugin\Logger\WebhookLogger::log has wrong type 'Stringable|string', expecting '' as defined by Psr\Log\LoggerInterface::log (see https://psalm.dev/042)

Check failure on line 16 in src/Logger/WebhookLogger.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~5.4.0)

MethodSignatureMismatch

src/Logger/WebhookLogger.php:16:52: MethodSignatureMismatch: Argument 2 of Setono\SyliusPeakPlugin\Logger\WebhookLogger::log has wrong type 'Stringable|string', expecting '' as defined by Psr\Log\LoggerInterface::log (see https://psalm.dev/042)

Check failure on line 16 in src/Logger/WebhookLogger.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~6.4.0)

MethodSignatureMismatch

src/Logger/WebhookLogger.php:16:52: MethodSignatureMismatch: Argument 2 of Setono\SyliusPeakPlugin\Logger\WebhookLogger::log has wrong type 'Stringable|string', expecting '' as defined by Psr\Log\LoggerInterface::log (see https://psalm.dev/042)
{
$this->webhook->addLog(sprintf('[%s] %s', (new \DateTimeImmutable())->format(\DATE_ATOM), (string) $message));

Check failure on line 18 in src/Logger/WebhookLogger.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)

RedundantCastGivenDocblockType

src/Logger/WebhookLogger.php:18:99: RedundantCastGivenDocblockType: Redundant cast to string given docblock-provided type (see https://psalm.dev/263)

Check failure on line 18 in src/Logger/WebhookLogger.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~5.4.0)

RedundantCastGivenDocblockType

src/Logger/WebhookLogger.php:18:99: RedundantCastGivenDocblockType: Redundant cast to string given docblock-provided type (see https://psalm.dev/263)

Check failure on line 18 in src/Logger/WebhookLogger.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: lowest | SF~6.4.0)

RedundantCastGivenDocblockType

src/Logger/WebhookLogger.php:18:99: RedundantCastGivenDocblockType: Redundant cast to string given docblock-provided type (see https://psalm.dev/263)
}
}
109 changes: 109 additions & 0 deletions src/Model/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusPeakPlugin\Model;

class Webhook implements WebhookInterface
{
protected ?int $id = null;

protected ?string $method = null;

protected ?string $url = null;

/** @var array<string, list<string|null>>|null */
protected ?array $headers = null;

protected ?string $body = null;

protected ?string $remoteIp = null;

protected ?string $log = null;

protected \DateTimeInterface $createdAt;

public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}

public function getId(): ?int
{
return $this->id;
}

public function getMethod(): ?string
{
return $this->method;
}

public function setMethod(?string $method): void
{
$this->method = $method;
}

public function getUrl(): ?string
{
return $this->url;
}

public function setUrl(?string $url): void
{
$this->url = $url;
}

public function getHeaders(): array
{
return $this->headers ?? [];
}

public function setHeaders(?array $headers): void
{
$this->headers = $headers;
}

public function getBody(): ?string
{
return $this->body;
}

public function setBody(?string $body): void
{
$this->body = $body;
}

public function getRemoteIp(): ?string
{
return $this->remoteIp;
}

public function setRemoteIp(?string $remoteIp): void
{
$this->remoteIp = $remoteIp;
}

public function getLog(): ?string
{
return $this->log;
}

public function setLog(?string $log): void
{
$this->log = $log;
}

public function addLog(string $log): void
{
if (null === $this->log) {
$this->log = '';
}

$this->log = $log . \PHP_EOL . $this->log;
}

public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}
}
46 changes: 46 additions & 0 deletions src/Model/WebhookInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusPeakPlugin\Model;

use Sylius\Component\Resource\Model\ResourceInterface;

interface WebhookInterface extends ResourceInterface
{
public function getId(): ?int;

public function getMethod(): ?string;

public function setMethod(?string $method): void;

public function getUrl(): ?string;

public function setUrl(?string $url): void;

/**
* @return array<string, list<string|null>>
*/
public function getHeaders(): array;

/**
* @param array<string, list<string|null>>|null $headers
*/
public function setHeaders(?array $headers): void;

public function getBody(): ?string;

public function setBody(?string $body): void;

public function getRemoteIp(): ?string;

public function setRemoteIp(?string $remoteIp): void;

public function getLog(): ?string;

public function setLog(?string $log): void;

public function addLog(string $log): void;

public function getCreatedAt(): \DateTimeInterface;
}
1 change: 1 addition & 0 deletions src/Registrar/WebhookRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ private function getWebhooks(): array
foreach ([Name::StockAdjust, Name::PickOrderPacked] as $name) {
$webhooks[] = new Webhook(
name: $name,
// todo this URL should include some kind of key to make it secure
url: $this->urlGenerator->generate(
name: 'setono_sylius_peak_global_webhook',
parameters: ['name' => $name->value],
Expand Down
21 changes: 21 additions & 0 deletions src/Resources/config/doctrine/model/Webhook.orm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>

<doctrine-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<mapped-superclass name="Setono\SyliusPeakPlugin\Model\Webhook"
table="setono_sylius_peak_wms__webhook">
<id name="id" type="integer">
<generator strategy="AUTO"/>
</id>

<field name="method" type="string"/>
<field name="url" type="string"/>
<field name="headers" type="json"/>
<field name="body" type="text"/>
<field name="remoteIp" type="string"/>
<field name="log" type="text" nullable="true"/>
<field name="createdAt" type="datetime"/>
</mapped-superclass>
</doctrine-mapping>
2 changes: 1 addition & 1 deletion src/Resources/config/routes/global.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ setono_sylius_peak_global_webhook:
path: /peak/webhook
methods: [POST]
defaults:
_controller: Setono\SyliusPeakPlugin\Controller\HandleWebhookControllerAction
_controller: Setono\SyliusPeakPlugin\Controller\HandleWebhookAction
4 changes: 3 additions & 1 deletion src/Resources/config/services/controller.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
<tag name="controller.service_arguments"/>
</service>

<service id="Setono\SyliusPeakPlugin\Controller\HandleWebhookControllerAction" public="true">
<service id="Setono\SyliusPeakPlugin\Controller\HandleWebhookAction" public="true">
<argument type="service" id="Setono\PeakWMS\Parser\WebhookParserInterface"/>
<argument type="service" id="Setono\SyliusPeakPlugin\WebhookHandler\WebhookHandlerInterface"/>
<argument type="service" id="setono_sylius_peak.factory.webhook"/>
<argument type="service" id="doctrine"/>
</service>
</services>
</container>
5 changes: 5 additions & 0 deletions src/Resources/config/services/factory.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
<services>
<!-- todo missing aliases -->

<service id="Setono\SyliusPeakPlugin\Factory\WebhookFactory"
decorates="setono_sylius_peak.factory.webhook" decoration-priority="64">
<argument type="service" id="Setono\SyliusPeakPlugin\Factory\WebhookFactory.inner"/>
</service>

<service id="Setono\SyliusPeakPlugin\Factory\WebhookRegistrationFactory"
decorates="setono_sylius_peak.factory.webhook_registration" decoration-priority="64">
<argument type="service" id="Setono\SyliusPeakPlugin\Factory\WebhookRegistrationFactory.inner"/>
Expand Down
Loading

0 comments on commit 3ce43c4

Please sign in to comment.