Skip to content

Commit

Permalink
Merge pull request #39 from Setono/per-pixel-token
Browse files Browse the repository at this point in the history
Per pixel token
  • Loading branch information
igormukhingmailcom authored Nov 11, 2021
2 parents 532ccfd + 8850bfe commit 1a1658c
Show file tree
Hide file tree
Showing 24 changed files with 388 additions and 8 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
"symfony/http-kernel": "^4.4 || ^5.1.5",
"symfony/options-resolver": "^4.4 || ^5.0",
"symfony/routing": "^4.4 || ^5.0",
"symfony/security": "^4.4 || ^5.0",
"symfony/security-bundle": "^4.4 || ^5.0",
"symfony/uid": "^4.4 || ^5.0",
"symfony/workflow": "^4.4 || ^5.0",
"twig/twig": "^2.0 || ^3.0",
"webmozart/assert": "^1.10"
},
"require-dev": {
Expand Down
36 changes: 33 additions & 3 deletions src/Client/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Setono\SyliusFacebookPlugin\Model\PixelEventInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Webmozart\Assert\Assert;

final class Client implements ClientInterface
Expand Down Expand Up @@ -38,13 +39,15 @@ public function sendPixelEvent(PixelEventInterface $pixelEvent): int
$pixelId = $pixel->getPixelId();
Assert::notNull($pixelId);

$accessToken = $pixel->getCustomAccessToken() ?? $this->accessToken;

$options = [
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json',
],
'body' => [
'access_token' => $this->accessToken,
'access_token' => $accessToken,
'data' => json_encode([
$pixelEvent->getData(),
]),
Expand All @@ -61,11 +64,38 @@ public function sendPixelEvent(PixelEventInterface $pixelEvent): int
$options
);

Assert::same($response->getStatusCode(), 200);
$content = $response->getContent();
Assert::same($response->getStatusCode(), 200, $this->getErrorMessage($response));
$content = $response->getContent(false);
$json = json_decode($content, true);
Assert::isArray($json);

return (int) $json['events_received'];
}

private function getErrorMessage(ResponseInterface $response): string
{
$content = $response->getContent(false);
$json = json_decode($content, true);
Assert::isArray($json);

$error = sprintf(
'Wrong status code. Expected %s. Got: %s.',
200,
$response->getStatusCode()
);

if (array_key_exists('error', $json)) {
/** @psalm-var array{message: string, error_subcode: int, error_user_msg: string} $errorPayload */
$errorPayload = $json['error'];

$error .= sprintf(
' Reason: %s [%s] %s',
$errorPayload['error_subcode'],
$errorPayload['message'],
$errorPayload['error_user_msg']
);
}

return $error;
}
}
91 changes: 91 additions & 0 deletions src/Controller/Action/Pixel/ResetFailedEventsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusFacebookPlugin\Controller\Action\Pixel;

use Setono\SyliusFacebookPlugin\Model\PixelInterface;
use Setono\SyliusFacebookPlugin\Repository\PixelEventRepositoryInterface;
use Setono\SyliusFacebookPlugin\Repository\PixelRepositoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Webmozart\Assert\Assert;

final class ResetFailedEventsAction
{
private PixelRepositoryInterface $pixelRepository;

private CsrfTokenManagerInterface $csrfTokenManager;

private PixelEventRepositoryInterface $pixelEventRepository;

private UrlGeneratorInterface $urlGeneratorInterface;

public function __construct(
PixelRepositoryInterface $pixelRepository,
CsrfTokenManagerInterface $csrfTokenManager,
PixelEventRepositoryInterface $pixelEventRepository,
UrlGeneratorInterface $urlGeneratorInterface
) {
$this->pixelRepository = $pixelRepository;
$this->csrfTokenManager = $csrfTokenManager;
$this->pixelEventRepository = $pixelEventRepository;
$this->urlGeneratorInterface = $urlGeneratorInterface;
}

public function __invoke(Request $request, int $id): Response
{
/** @var FlashBagInterface $flashBag */
$flashBag = $request->getSession()->getBag('flashes');

/** @var PixelInterface|null $pixel */
$pixel = $this->pixelRepository->find($id);
Assert::notNull($pixel);

$csrfToken = $request->request->get('_csrf_token');
Assert::string($csrfToken);
if (!$this->isCsrfTokenValid((string) $pixel->getId(), $csrfToken)) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
}

$this->pixelEventRepository->resetFailedByPixel($pixel);

$flashBag->add('success', 'setono_sylius_facebook.pixel.failed_events_reset');

return new RedirectResponse(
$this->getRedirectUrl($request, 'setono_sylius_facebook_admin_pixel_index')
);
}

private function getRedirectUrl(Request $request, string $defaultRoute): string
{
$syliusParameters = [];

if ($request->attributes->has('_sylius')) {
/** @var array|mixed $syliusParameters */
$syliusParameters = $request->attributes->get('_sylius');
Assert::isArray($syliusParameters);
}

/** @var string|mixed $route */
$route = $syliusParameters['redirect']['route'] ?? $defaultRoute;
Assert::string($route);

/** @var array|mixed $parameters */
$parameters = $syliusParameters['redirect']['parameters'] ?? [];
Assert::isArray($parameters);

return $this->urlGeneratorInterface->generate($route, $parameters);
}

private function isCsrfTokenValid(string $id, ?string $token): bool
{
return $this->csrfTokenManager->isTokenValid(new CsrfToken($id, $token));
}
}
29 changes: 29 additions & 0 deletions src/Doctrine/ORM/PixelEventRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,26 @@
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Setono\SyliusFacebookPlugin\Model\PixelEventInterface;
use Setono\SyliusFacebookPlugin\Model\PixelInterface;
use Setono\SyliusFacebookPlugin\Repository\PixelEventRepositoryInterface;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Webmozart\Assert\Assert;

class PixelEventRepository extends EntityRepository implements PixelEventRepositoryInterface
{
public function getCountByPixelAndState(PixelInterface $pixel, string $state): int
{
return (int) $this->createQueryBuilder('o')
->select('count(o)')
->andWhere('o.pixel = :pixel')
->setParameter('pixel', $pixel)
->andWhere('o.state = :state')
->setParameter('state', $state)
->getQuery()
->getSingleScalarResult()
;
}

public function hasConsentedPending(int $delay = 0): bool
{
$qb = $this->createQueryBuilder('o')
Expand Down Expand Up @@ -79,6 +93,21 @@ protected static function applyDelay(QueryBuilder $qb, int $delay): void
}
}

public function resetFailedByPixel(PixelInterface $pixel): void
{
$this->createQueryBuilder('o')
->update()
->set('o.state', ':initialState')
->setParameter('initialState', PixelEventInterface::STATE_PENDING, Types::STRING)
->andWhere('o.pixel = :pixel')
->setParameter('pixel', $pixel)
->andWhere('o.state = :state')
->setParameter('state', PixelEventInterface::STATE_FAILED, Types::STRING)
->getQuery()
->execute()
;
}

public function removeSent(int $delay = 0): int
{
$qb = $this->_em->createQueryBuilder()
Expand Down
12 changes: 11 additions & 1 deletion src/Form/Type/PixelType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;

final class PixelType extends AbstractResourceType
Expand All @@ -17,12 +18,21 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$builder
->add('pixelId', IntegerType::class, [
'label' => 'setono_sylius_facebook.form.pixel.pixel_id',
'help' => 'Get it from https://www.facebook.com/events_manager2',
'help' => 'setono_sylius_facebook.form.pixel.pixel_id_help',
'attr' => [
'min' => 1,
'placeholder' => 'setono_sylius_facebook.form.pixel.pixel_id_placeholder',
],
])
->add('customAccessToken', TextareaType::class, [
'label' => 'setono_sylius_facebook.form.pixel.custom_access_token',
'help' => 'setono_sylius_facebook.form.pixel.custom_access_token_help',
'required' => false,
'attr' => [
'rows' => 3,
'placeholder' => 'setono_sylius_facebook.form.pixel.custom_access_token_placeholder',
],
])
->add('enabled', CheckboxType::class, [
'required' => false,
'label' => 'sylius.ui.enabled',
Expand Down
12 changes: 12 additions & 0 deletions src/Model/Pixel.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class Pixel implements PixelInterface

protected ?string $pixelId = null;

protected ?string $customAccessToken = null;

/**
* @var Collection|BaseChannelInterface[]
*
Expand Down Expand Up @@ -49,6 +51,16 @@ public function setPixelId(string $pixelId): void
$this->pixelId = $pixelId;
}

public function getCustomAccessToken(): ?string
{
return $this->customAccessToken;
}

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

public function getChannels(): Collection
{
return $this->channels;
Expand Down
4 changes: 4 additions & 0 deletions src/Model/PixelInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ public function getId(): ?int;
public function getPixelId(): ?string;

public function setPixelId(string $pixelId): void;

public function getCustomAccessToken(): ?string;

public function setCustomAccessToken(?string $customAccessToken): void;
}
5 changes: 5 additions & 0 deletions src/Repository/PixelEventRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
namespace Setono\SyliusFacebookPlugin\Repository;

use Setono\SyliusFacebookPlugin\Model\PixelEventInterface;
use Setono\SyliusFacebookPlugin\Model\PixelInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;

interface PixelEventRepositoryInterface extends RepositoryInterface
{
public function getCountByPixelAndState(PixelInterface $pixel, string $state): int;

/**
* Returns true if there are pending consented hits created before $delay seconds ago
*
Expand All @@ -29,5 +32,7 @@ public function assignBulkIdentifierToPendingConsented(string $bulkIdentifier, i
*/
public function findByBulkIdentifier(string $bulkIdentifier): array;

public function resetFailedByPixel(PixelInterface $pixel): void;

public function removeSent(int $delay = 0): int;
}
2 changes: 2 additions & 0 deletions src/Resources/config/doctrine/model/Pixel.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
</id>

<field name="pixelId" column="pixel_id" unique="true"/>
<field name="customAccessToken" column="custom_access_token" nullable="true"/>

<field name="enabled" column="enabled" type="boolean"/>

<many-to-many field="channels" target-entity="Sylius\Component\Channel\Model\ChannelInterface">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
sylius_grid:
templates:
action:
setono_facebook_reset_failed_events: "@SetonoSyliusFacebookPlugin/Admin/Grid/Action/setono_facebook_reset_failed_events.html.twig"
grids:
setono_sylius_facebook_admin_pixel:
driver:
Expand All @@ -7,8 +10,17 @@ sylius_grid:
class: "%setono_sylius_facebook.model.pixel.class%"
fields:
pixelId:
type: string
type: twig
label: setono_sylius_facebook.ui.pixel_id
path: .
options:
template: "@SetonoSyliusFacebookPlugin/Admin/Grid/Field/pixel.html.twig"
events_statistics:
type: twig
label: setono_sylius_facebook.ui.events_statistics
path: .
options:
template: "@SetonoSyliusFacebookPlugin/Admin/Grid/Field/statistics.html.twig"
channels:
type: twig
label: sylius.ui.channels
Expand All @@ -35,5 +47,7 @@ sylius_grid:
item:
update:
type: update
setono_facebook_reset_failed_events:
type: setono_facebook_reset_failed_events
delete:
type: delete
13 changes: 13 additions & 0 deletions src/Resources/config/routes/admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ setono_sylius_facebook_admin_pixel:
vars:
all:
subheader: setono_sylius_facebook.ui.manage_pixels
templates:
form: "@SetonoSyliusFacebookPlugin/Admin/Pixel/_form.html.twig"
index:
icon: 'facebook'
type: sylius.resource

setono_sylius_facebook_admin_pixel_reset_failed_events:
path: /{id}/events/failed/reset
methods: [PATCH]
defaults:
_controller: setono_sylius_facebook.controller.action.pixel.reset_failed_events
_sylius:
section: admin
permission: true
redirect:
route: setono_sylius_facebook_admin_pixel_index
2 changes: 2 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<import resource="services/client.xml"/>
<import resource="services/command.xml"/>
<import resource="services/context.xml"/>
<import resource="services/controller.xml"/>
<import resource="services/data_mapper.xml"/>
<import resource="services/event_listener.xml"/>
<import resource="services/factory.xml"/>
Expand All @@ -13,5 +14,6 @@
<import resource="services/formatter.xml"/>
<import resource="services/menu.xml"/>
<import resource="services/server_side_factory.xml"/>
<import resource="services/twig.xml"/>
</imports>
</container>
Loading

0 comments on commit 1a1658c

Please sign in to comment.