From 3bc77c75e82d76fe66a5315170337651e107526a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 7 Sep 2026 13:29:34 +0200 Subject: [PATCH] Document the event pipeline and expose priority constants The order of the ten listeners on ConversionsApiEventRaised lived as magic numbers in ten files and was documented nowhere, so integrators could not tell where to hook in or that a late listener may never run. Name the four bands as constants on the event, derive every subscriber priority from them, and document the pipeline with an enrichment example. No priority changes value. Fixes #22 --- README.md | 69 ++++++++++ src/Event/ConversionsApiEventRaised.php | 33 +++++ .../AddEventToTagBagSubscriber.php | 2 +- .../DispatchOnCommandBusSubscriber.php | 2 +- src/EventSubscriber/FilterBotsSubscriber.php | 2 +- .../FilterConfiguredUserAgentsSubscriber.php | 2 +- .../FilterEmptyUserAgentSubscriber.php | 2 +- .../PopulateFbpAndFbcPropertiesSubscriber.php | 2 +- .../PopulatePixelsSubscriber.php | 2 +- .../PopulateRequestPropertiesSubscriber.php | 2 +- ...opulateTestEventCodePropertySubscriber.php | 2 +- ...gationIfNoPixelsHasBeenAddedSubscriber.php | 2 +- .../Event/ConversionsApiEventRaisedTest.php | 122 ++++++++++++++++++ 13 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 tests/Unit/Event/ConversionsApiEventRaisedTest.php diff --git a/README.md b/README.md index ba37294..8718f50 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,75 @@ final class YourService } ``` +## How it works + +Dispatching a `ConversionsApiEventRaised` runs the event through a pipeline of listeners. The bundle populates the +event first, then leaves a gap for your own listeners, then filters and sends: + +| Priority | Listener | What it does | +|---------------------------------------|---------------------------------------------------|---------------------------------------------------------| +| `PRIORITY_POPULATE` (1000) | `PopulateRequestPropertiesSubscriber` | Source url, client ip and user agent from the request | +| 900 | `PopulateFbpAndFbcPropertiesSubscriber` | `fbp` and `fbc` | +| 800 | `PopulateTestEventCodePropertySubscriber` | Test event code | +| 700 | `PopulatePixelsSubscriber` | Pixels from the pixel provider | +| **`PRIORITY_ENRICH` (0)** | **your listeners** | **Email, phone, external id, custom data** | +| -850 | `FilterEmptyUserAgentSubscriber` | Stops events without a user agent | +| -875 | `FilterConfiguredUserAgentsSubscriber` | Stops events matching `filters.user_agent` | +| `PRIORITY_FILTER` (-900) | `FilterBotsSubscriber` | Stops events from bots | +| -950 | `StopPropagationIfNoPixelsHasBeenAddedSubscriber` | Stops events without pixels | +| `PRIORITY_SEND` (-1000) | `AddEventToTagBagSubscriber` | Renders the `fbq()` calls (client side) | +| `PRIORITY_SEND` (-1000) | `DispatchOnCommandBusSubscriber` | Dispatches `SendEvent` (server side) | + +Two things follow from this: + +- **Enrich at `PRIORITY_ENRICH`**, which is the default priority of any listener. Everything the bundle knows about + the request is populated by then, and nothing has been filtered or sent yet. +- **A listener below `PRIORITY_FILTER` may never run**, because the filters stop propagation. + +The constants live on `ConversionsApiEventRaised`, so you can position your listener without hard coding a number. + +### Enriching an event + +Everything the Conversions API can do beyond the browser pixel comes from the user data you attach server side. Meta +normalises and hashes it for you, so set the raw values: + +```php +security->getUser(); + if (!$user instanceof User) { + return; + } + + $userData = $event->event->userData; + $userData->email[] = $user->getEmail(); + $userData->firstName[] = $user->getFirstName(); + $userData->lastName[] = $user->getLastName(); + $userData->externalId[] = (string) $user->getId(); + } +} +``` + +You can also replace a step instead of adding to it: alias `PixelProviderInterface`, `FbpContextInterface` or +`FbcContextInterface` to your own service, or register a listener above the corresponding populate priority. + ## Graph API version Events are posted to the Graph API version of the installed `facebook/php-business-sdk` package (the SDK reads diff --git a/src/Event/ConversionsApiEventRaised.php b/src/Event/ConversionsApiEventRaised.php index 3489281..b5a8f0a 100644 --- a/src/Event/ConversionsApiEventRaised.php +++ b/src/Event/ConversionsApiEventRaised.php @@ -9,9 +9,42 @@ /** * Dispatch this event onto the EventDispatcher and everything will be handled for you + * + * The bundle's own listeners run in four bands. Use the constants below to position your own listener relative to + * them instead of hard coding a number: + * + * | Priority | What happens | + * |--------------------------------|-----------------------------------------------------------------------| + * | PRIORITY_POPULATE (and below) | The bundle fills in request properties, fbp/fbc, test event code, pixels | + * | PRIORITY_ENRICH | Your listeners add user data and custom data | + * | PRIORITY_FILTER | The bundle drops events it should not track (bots, filtered user agents) | + * | PRIORITY_SEND | The bundle renders the client side tags and dispatches the command | + * + * A listener below PRIORITY_FILTER may never run, because the filters stop propagation */ final class ConversionsApiEventRaised extends StoppableEvent { + /** + * The bundle populates the event from the current request at this priority and just below it + */ + public const PRIORITY_POPULATE = 1000; + + /** + * The priority your own listeners should use. Everything the bundle knows about the request is populated by + * now, and nothing has been filtered or sent yet. This is the default priority of an event listener + */ + public const PRIORITY_ENRICH = 0; + + /** + * The bundle decides here whether the event should be tracked at all + */ + public const PRIORITY_FILTER = -900; + + /** + * The bundle hands the event to the tag bag and the command bus at this priority + */ + public const PRIORITY_SEND = -1000; + /** * @param array $context */ diff --git a/src/EventSubscriber/AddEventToTagBagSubscriber.php b/src/EventSubscriber/AddEventToTagBagSubscriber.php index 14f9589..9394af4 100644 --- a/src/EventSubscriber/AddEventToTagBagSubscriber.php +++ b/src/EventSubscriber/AddEventToTagBagSubscriber.php @@ -25,7 +25,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['add', -1000], + ConversionsApiEventRaised::class => ['add', ConversionsApiEventRaised::PRIORITY_SEND], ]; } diff --git a/src/EventSubscriber/DispatchOnCommandBusSubscriber.php b/src/EventSubscriber/DispatchOnCommandBusSubscriber.php index ff79721..e52fe95 100644 --- a/src/EventSubscriber/DispatchOnCommandBusSubscriber.php +++ b/src/EventSubscriber/DispatchOnCommandBusSubscriber.php @@ -21,7 +21,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['dispatch', -1000], + ConversionsApiEventRaised::class => ['dispatch', ConversionsApiEventRaised::PRIORITY_SEND], ]; } diff --git a/src/EventSubscriber/FilterBotsSubscriber.php b/src/EventSubscriber/FilterBotsSubscriber.php index ff35c26..a407055 100644 --- a/src/EventSubscriber/FilterBotsSubscriber.php +++ b/src/EventSubscriber/FilterBotsSubscriber.php @@ -17,7 +17,7 @@ public function __construct(private readonly BotDetectorInterface $botDetector) public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['filter', -900], + ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_FILTER], ]; } diff --git a/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php b/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php index 6ba48b2..4ce047c 100644 --- a/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php +++ b/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php @@ -44,7 +44,7 @@ public function __construct(array $userAgents) public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['filter', -875], + ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_FILTER + 25], ]; } diff --git a/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php b/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php index 834cfa4..7abec51 100644 --- a/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php +++ b/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php @@ -12,7 +12,7 @@ final class FilterEmptyUserAgentSubscriber implements EventSubscriberInterface public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['filter', -850], + ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_FILTER + 50], ]; } diff --git a/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php b/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php index 11886df..11de20e 100644 --- a/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php +++ b/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php @@ -20,7 +20,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['populate', 900], + ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 100], ]; } diff --git a/src/EventSubscriber/PopulatePixelsSubscriber.php b/src/EventSubscriber/PopulatePixelsSubscriber.php index b71bbba..f55c1b0 100644 --- a/src/EventSubscriber/PopulatePixelsSubscriber.php +++ b/src/EventSubscriber/PopulatePixelsSubscriber.php @@ -17,7 +17,7 @@ public function __construct(private readonly PixelProviderInterface $pixelProvid public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['populate', 700], + ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 300], ]; } diff --git a/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php b/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php index 3f07711..afb22b3 100644 --- a/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php +++ b/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php @@ -17,7 +17,7 @@ public function __construct(private readonly RequestStack $requestStack) public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['populate', 1000], + ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE], ]; } diff --git a/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php b/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php index 5770ece..802dbb5 100644 --- a/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php +++ b/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php @@ -23,7 +23,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['populate', 800], + ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 200], ]; } diff --git a/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php b/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php index 5331399..d111744 100644 --- a/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php +++ b/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php @@ -12,7 +12,7 @@ final class StopPropagationIfNoPixelsHasBeenAddedSubscriber implements EventSubs public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['filter', -950], + ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_SEND + 50], ]; } diff --git a/tests/Unit/Event/ConversionsApiEventRaisedTest.php b/tests/Unit/Event/ConversionsApiEventRaisedTest.php new file mode 100644 index 0000000..a56624d --- /dev/null +++ b/tests/Unit/Event/ConversionsApiEventRaisedTest.php @@ -0,0 +1,122 @@ +> + */ + private const PIPELINE = [ + PopulateRequestPropertiesSubscriber::class, + PopulateFbpAndFbcPropertiesSubscriber::class, + PopulateTestEventCodePropertySubscriber::class, + PopulatePixelsSubscriber::class, + FilterEmptyUserAgentSubscriber::class, + FilterConfiguredUserAgentsSubscriber::class, + FilterBotsSubscriber::class, + StopPropagationIfNoPixelsHasBeenAddedSubscriber::class, + AddEventToTagBagSubscriber::class, + DispatchOnCommandBusSubscriber::class, + ]; + + #[Test] + public function it_has_context(): void + { + $event = new ConversionsApiEventRaised(new Event(Event::EVENT_VIEW_CONTENT), ['order' => 1]); + + self::assertTrue($event->hasContext('order')); + self::assertFalse($event->hasContext('customer')); + } + + /** + * The documented pipeline only holds as long as the bundle's own listeners keep their relative order, so this + * pins it down. It is the contract integrators position their own listeners against + */ + #[Test] + public function the_pipeline_runs_in_the_documented_order(): void + { + $previous = null; + + foreach (self::PIPELINE as $subscriber) { + $priority = self::priority($subscriber); + + if (null !== $previous) { + self::assertLessThanOrEqual($previous, $priority, sprintf('%s runs out of order', $subscriber)); + } + + $previous = $priority; + } + } + + #[Test] + public function everything_is_populated_before_your_listeners_run(): void + { + foreach ([ + PopulateRequestPropertiesSubscriber::class, + PopulateFbpAndFbcPropertiesSubscriber::class, + PopulateTestEventCodePropertySubscriber::class, + PopulatePixelsSubscriber::class, + ] as $subscriber) { + self::assertGreaterThan(ConversionsApiEventRaised::PRIORITY_ENRICH, self::priority($subscriber)); + } + } + + #[Test] + public function filtering_and_sending_happen_after_your_listeners(): void + { + foreach ([ + FilterEmptyUserAgentSubscriber::class, + FilterConfiguredUserAgentsSubscriber::class, + FilterBotsSubscriber::class, + StopPropagationIfNoPixelsHasBeenAddedSubscriber::class, + AddEventToTagBagSubscriber::class, + DispatchOnCommandBusSubscriber::class, + ] as $subscriber) { + self::assertLessThan(ConversionsApiEventRaised::PRIORITY_ENRICH, self::priority($subscriber)); + } + } + + #[Test] + public function the_sinks_run_last(): void + { + self::assertSame(ConversionsApiEventRaised::PRIORITY_SEND, self::priority(AddEventToTagBagSubscriber::class)); + self::assertSame(ConversionsApiEventRaised::PRIORITY_SEND, self::priority(DispatchOnCommandBusSubscriber::class)); + } + + /** + * @param class-string $subscriber + */ + private static function priority(string $subscriber): int + { + $listener = $subscriber::getSubscribedEvents()[ConversionsApiEventRaised::class] ?? null; + + self::assertIsArray($listener); + self::assertArrayHasKey(1, $listener); + self::assertIsInt($listener[1]); + + return $listener[1]; + } +}