├── LICENSE ├── composer-dependency-analyser.php ├── composer.json └── src ├── Client ├── Client.php ├── ClientInterface.php └── ErrorResponse.php ├── Event ├── Content.php ├── Custom.php ├── Event.php ├── Parameters.php └── User.php ├── Exception └── ClientException.php ├── Generator ├── FbqGenerator.php └── FbqGeneratorInterface.php ├── Pixel └── Pixel.php └── ValueObject ├── Fb.php ├── Fbc.php └── Fbp.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Setono 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 | -------------------------------------------------------------------------------- /composer-dependency-analyser.php: -------------------------------------------------------------------------------- 1 | addPathToExclude(__DIR__ . '/tests') 10 | ->ignoreErrorsOnPackage('psr/http-client-implementation', [ErrorType::UNUSED_DEPENDENCY]) 11 | ->ignoreErrorsOnPackage('psr/http-factory-implementation', [ErrorType::UNUSED_DEPENDENCY]) 12 | ; 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setono/meta-conversions-api-php-sdk", 3 | "description": "PHP library with basic objects and more for working with Facebook/Metas Conversions API", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Joachim Løvgaard", 9 | "email": "joachim@loevgaard.dk" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=8.1", 14 | "ext-json": "*", 15 | "facebook/php-business-sdk": "^22.0", 16 | "php-http/discovery": "^1.20", 17 | "psr/http-client": "^1.0", 18 | "psr/http-client-implementation": "*", 19 | "psr/http-factory": "^1.0", 20 | "psr/http-factory-implementation": "*", 21 | "psr/log": "^1.1 || ^2.0 || ^3.0", 22 | "webmozart/assert": "^1.11" 23 | }, 24 | "require-dev": { 25 | "infection/infection": "^0.26.21", 26 | "nyholm/psr7": "^1.8", 27 | "phpunit/phpunit": "^9.6", 28 | "psalm/plugin-phpunit": "^0.19", 29 | "setono/code-quality-pack": "^2.9", 30 | "shipmonk/composer-dependency-analyser": "^1.8.2", 31 | "symfony/http-client": "^6.4 || ^7.0" 32 | }, 33 | "prefer-stable": true, 34 | "autoload": { 35 | "psr-4": { 36 | "Setono\\MetaConversionsApi\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Setono\\MetaConversionsApi\\": "tests/" 42 | } 43 | }, 44 | "config": { 45 | "allow-plugins": { 46 | "dealerdirect/phpcodesniffer-composer-installer": false, 47 | "ergebnis/composer-normalize": true, 48 | "infection/extension-installer": true, 49 | "php-http/discovery": false 50 | }, 51 | "sort-packages": true 52 | }, 53 | "scripts": { 54 | "analyse": "psalm", 55 | "check-style": "ecs check", 56 | "fix-style": "ecs check --fix", 57 | "phpunit": "phpunit" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Client/Client.php: -------------------------------------------------------------------------------- 1 | logger = new NullLogger(); 35 | } 36 | 37 | public function sendEvent(Event $event): void 38 | { 39 | if (!$event->hasPixels()) { 40 | $this->logger->error('You are trying to send events to Meta/Facebook, but you haven\'n associated any pixels with your event. This is most likely an error.'); 41 | 42 | return; 43 | } 44 | 45 | $httpClient = $this->getHttpClient(); 46 | $requestFactory = $this->getRequestFactory(); 47 | 48 | $data = json_encode([$event->getPayload()], \JSON_THROW_ON_ERROR); 49 | 50 | foreach ($event->pixels as $pixel) { 51 | $body = [ 52 | 'access_token' => $pixel->accessToken, 53 | 'data' => $data, 54 | ]; 55 | 56 | if (null !== $event->testEventCode) { 57 | $body['test_event_code'] = $event->testEventCode; 58 | } 59 | 60 | $request = $requestFactory->createRequest( 61 | 'POST', 62 | sprintf('https://graph.facebook.com/%s/%s/events', sprintf('v%s', ApiConfig::APIVersion), $pixel->id), 63 | ) 64 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded') 65 | ->withHeader('Accept', 'application/json') 66 | ->withBody($this->getStreamFactory()->createStream(http_build_query($body))); 67 | 68 | $response = $httpClient->sendRequest($request); 69 | 70 | if ($response->getStatusCode() !== 200) { 71 | throw ClientException::fromErrorResponse(ErrorResponse::fromJson((string) $response->getBody())); 72 | } 73 | } 74 | } 75 | 76 | private function getHttpClient(): HttpClientInterface 77 | { 78 | if (null === $this->httpClient) { 79 | $this->httpClient = Psr18ClientDiscovery::find(); 80 | } 81 | 82 | return $this->httpClient; 83 | } 84 | 85 | public function setHttpClient(HttpClientInterface $httpClient): void 86 | { 87 | $this->httpClient = $httpClient; 88 | } 89 | 90 | private function getRequestFactory(): RequestFactoryInterface 91 | { 92 | if (null === $this->requestFactory) { 93 | $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory(); 94 | } 95 | 96 | return $this->requestFactory; 97 | } 98 | 99 | public function setRequestFactory(RequestFactoryInterface $requestFactory): void 100 | { 101 | $this->requestFactory = $requestFactory; 102 | } 103 | 104 | private function getResponseFactory(): ResponseFactoryInterface 105 | { 106 | if (null === $this->responseFactory) { 107 | $this->responseFactory = Psr17FactoryDiscovery::findResponseFactory(); 108 | } 109 | 110 | return $this->responseFactory; 111 | } 112 | 113 | public function setResponseFactory(ResponseFactoryInterface $responseFactory): void 114 | { 115 | $this->responseFactory = $responseFactory; 116 | } 117 | 118 | private function getStreamFactory(): StreamFactoryInterface 119 | { 120 | if (null === $this->streamFactory) { 121 | $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); 122 | } 123 | 124 | return $this->streamFactory; 125 | } 126 | 127 | public function setStreamFactory(StreamFactoryInterface $streamFactory): void 128 | { 129 | $this->streamFactory = $streamFactory; 130 | } 131 | 132 | public function setLogger(LoggerInterface $logger): void 133 | { 134 | $this->logger = $logger; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Client/ClientInterface.php: -------------------------------------------------------------------------------- 1 | json = $json; 35 | $this->message = $message; 36 | $this->type = $type; 37 | $this->code = $code; 38 | $this->traceId = $traceId; 39 | } 40 | 41 | /** 42 | * @throws ClientException if the JSON / response is invalid 43 | */ 44 | public static function fromJson(string $json): self 45 | { 46 | try { 47 | $data = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); 48 | } catch (\JsonException $e) { 49 | throw ClientException::invalidJson($e, $json); 50 | } 51 | 52 | try { 53 | Assert::isArray($data); 54 | 55 | if (!isset($data['error']['message'], $data['error']['type'], $data['error']['code'], $data['error']['fbtrace_id'])) { 56 | throw ClientException::invalidResponseFormat($json); 57 | } 58 | 59 | ['message' => $message, 'type' => $type, 'code' => $code, 'fbtrace_id' => $traceId] = $data['error']; 60 | 61 | Assert::string($message); 62 | Assert::string($type); 63 | Assert::integer($code); 64 | Assert::string($traceId); 65 | } catch (\InvalidArgumentException $e) { 66 | throw ClientException::invalidResponseFormat($json); 67 | } 68 | 69 | return new self($json, $message, $type, $code, $traceId); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Event/Content.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->quantity = $quantity; 25 | $this->itemPrice = $itemPrice; 26 | $this->deliveryCategory = $deliveryCategory; 27 | } 28 | 29 | protected function getMapping(string $context): array 30 | { 31 | return [ 32 | 'id' => $this->id, 33 | 'quantity' => $this->quantity, 34 | 'item_price' => $this->itemPrice, 35 | 'delivery_category' => $this->deliveryCategory, 36 | ]; 37 | } 38 | 39 | /** 40 | * @see \FacebookAds\Object\ServerSide\Content::normalize 41 | */ 42 | protected static function getNormalizedFields(): array 43 | { 44 | return [ 45 | 'delivery_category', 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Event/Custom.php: -------------------------------------------------------------------------------- 1 | */ 12 | public array $contentIds = []; 13 | 14 | public ?string $contentName = null; 15 | 16 | public ?string $contentType = null; 17 | 18 | /** @var list */ 19 | public array $contents = []; 20 | 21 | public ?string $currency = null; 22 | 23 | public ?string $deliveryCategory = null; 24 | 25 | public ?int $numItems = null; 26 | 27 | public ?string $orderId = null; 28 | 29 | public ?float $predictedLtv = null; 30 | 31 | public ?string $searchString = null; 32 | 33 | public ?string $status = null; 34 | 35 | public ?float $value = null; 36 | 37 | /** 38 | * This holds an array of custom properties. See https://developers.facebook.com/docs/meta-pixel/implementation/conversion-tracking#custom-properties 39 | * 40 | * NOTICE that if you define a custom property with the same name as any of the standard properties, your 41 | * custom property will be overridden by the value of the standard property 42 | * 43 | * @var array 44 | */ 45 | public array $customProperties = []; 46 | 47 | protected function getMapping(string $context): array 48 | { 49 | return array_merge($this->customProperties, [ 50 | 'content_category' => $this->contentCategory, 51 | 'content_ids' => $this->contentIds, 52 | 'content_name' => $this->contentName, 53 | 'content_type' => $this->contentType, 54 | 'contents' => $this->contents, 55 | 'currency' => $this->currency, 56 | 'delivery_category' => $this->deliveryCategory, 57 | 'num_items' => $this->numItems, 58 | 'order_id' => $this->orderId, 59 | 'predicted_ltv' => $this->predictedLtv, 60 | 'search_string' => $this->searchString, 61 | 'status' => $this->status, 62 | 'value' => $this->value, 63 | ]); 64 | } 65 | 66 | /** 67 | * @see \FacebookAds\Object\ServerSide\CustomData::normalize 68 | */ 69 | protected static function getNormalizedFields(): array 70 | { 71 | return [ 72 | 'currency', 'delivery_category', 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Event/Event.php: -------------------------------------------------------------------------------- 1 | 69 | */ 70 | public array $metadata = []; 71 | 72 | /** 73 | * Holds the list of pixel this event should 'be sent to' 74 | * 75 | * @var list 76 | */ 77 | public array $pixels = []; 78 | 79 | public string $eventName; 80 | 81 | public int $eventTime; 82 | 83 | public User $userData; 84 | 85 | public Custom $customData; 86 | 87 | public ?string $eventSourceUrl = null; 88 | 89 | public ?bool $optOut = null; 90 | 91 | public string $eventId; 92 | 93 | public ?string $actionSource = null; 94 | 95 | public array $dataProcessingOptions = []; 96 | 97 | public ?int $dataProcessingOptionsCountry = null; 98 | 99 | public ?int $dataProcessingOptionsState = null; 100 | 101 | /** 102 | * Use this to test your implementation 103 | * 104 | * See https://developers.facebook.com/docs/marketing-api/conversions-api/using-the-api#testEvents 105 | */ 106 | public ?string $testEventCode = null; 107 | 108 | public function __construct(string $eventName, string $actionSource = self::ACTION_SOURCE_WEBSITE) 109 | { 110 | // We set the event id by default because of deduplication 111 | // See https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event#event-id 112 | $this->eventId = bin2hex(random_bytes(16)); 113 | $this->eventName = $eventName; 114 | $this->eventTime = time(); 115 | $this->userData = new User(); 116 | $this->customData = new Custom(); 117 | $this->actionSource = $actionSource; 118 | } 119 | 120 | /** 121 | * Returns true if the event is a custom event (i.e. not a standard event) 122 | * 123 | * See also 124 | * - https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event#event-name 125 | * - https://developers.facebook.com/docs/meta-pixel/implementation/conversion-tracking#custom-events 126 | */ 127 | public function isCustom(): bool 128 | { 129 | return !in_array($this->eventName, self::getEvents(), true); 130 | } 131 | 132 | /** 133 | * Returns true if one or more pixels are associated with this event 134 | */ 135 | public function hasPixels(): bool 136 | { 137 | return [] !== $this->pixels; 138 | } 139 | 140 | /** 141 | * @return list 142 | */ 143 | public static function getEvents(): array 144 | { 145 | return [ 146 | self::EVENT_ADD_TO_CART, 147 | self::EVENT_ADD_PAYMENT_INFO, 148 | self::EVENT_ADD_TO_WISHLIST, 149 | self::EVENT_COMPLETE_REGISTRATION, 150 | self::EVENT_CONTACT, 151 | self::EVENT_CUSTOMIZE_PRODUCT, 152 | self::EVENT_DONATE, 153 | self::EVENT_FIND_LOCATION, 154 | self::EVENT_INITIATE_CHECKOUT, 155 | self::EVENT_LEAD, 156 | self::EVENT_PURCHASE, 157 | self::EVENT_SCHEDULE, 158 | self::EVENT_SEARCH, 159 | self::EVENT_START_TRIAL, 160 | self::EVENT_SUBMIT_APPLICATION, 161 | self::EVENT_SUBSCRIBE, 162 | self::EVENT_VIEW_CONTENT, 163 | ]; 164 | } 165 | 166 | protected function getMapping(string $context): array 167 | { 168 | return [ 169 | 'event_name' => $this->eventName, 170 | 'event_time' => $this->eventTime, 171 | 'user_data' => $this->userData, 172 | 'custom_data' => $this->customData, 173 | 'event_source_url' => $this->eventSourceUrl, 174 | 'opt_out' => $this->optOut, 175 | 'event_id' => $this->eventId, 176 | 'action_source' => $this->actionSource, 177 | 'data_processing_options' => $this->dataProcessingOptions, 178 | 'data_processing_options_country' => $this->dataProcessingOptionsCountry, 179 | 'data_processing_options_state' => $this->dataProcessingOptionsState, 180 | ]; 181 | } 182 | 183 | /** 184 | * @see \FacebookAds\Object\ServerSide\Event::normalize 185 | */ 186 | protected static function getNormalizedFields(): array 187 | { 188 | return [ 189 | 'action_source', 190 | ]; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Event/Parameters.php: -------------------------------------------------------------------------------- 1 | getMapping($context)); 24 | Assert::isArray($payload); 25 | 26 | return $payload; 27 | } 28 | 29 | /** 30 | * Returns the Meta/Facebook fields mapped to their respective values 31 | * 32 | * @return array 33 | */ 34 | abstract protected function getMapping(string $context): array; 35 | 36 | /** 37 | * Returns a list of Meta/Facebook field names that must be normalized by \FacebookAds\Object\ServerSide\Normalizer::normalize 38 | * 39 | * @return list 40 | */ 41 | abstract protected static function getNormalizedFields(): array; 42 | 43 | /** 44 | * Returns a list of Meta/Facebook field names that must be hashed 45 | * 46 | * @return list 47 | */ 48 | protected static function getHashedFields(): array 49 | { 50 | return []; 51 | } 52 | 53 | /** 54 | * @param mixed $data 55 | * 56 | * @return array|string|float|int|bool|null 57 | */ 58 | private static function normalize($data, string $field = null) 59 | { 60 | if (null === $data) { 61 | return null; 62 | } 63 | 64 | if ($data instanceof \DateTimeInterface) { 65 | $data = $data->format('Ymd'); 66 | } 67 | 68 | if ($data instanceof \Stringable) { 69 | $data = (string) $data; 70 | } 71 | 72 | if (is_string($data)) { 73 | Assert::notNull($field); 74 | if (in_array($field, static::getNormalizedFields(), true)) { 75 | $data = Normalizer::normalize($field, $data); 76 | } 77 | 78 | if (in_array($field, static::getHashedFields(), true)) { 79 | $data = Util::hash($data); 80 | } 81 | 82 | return $data; 83 | } 84 | 85 | if (is_int($data) || is_float($data) || is_bool($data)) { 86 | return $data; 87 | } 88 | 89 | Assert::isArray($data); 90 | 91 | /** @var mixed $datum */ 92 | foreach ($data as $key => &$datum) { 93 | if ($datum instanceof self) { 94 | $datum = $datum->getPayload(); 95 | } else { 96 | $datum = self::normalize($datum, is_string($key) ? $key : $field); 97 | } 98 | } 99 | unset($datum); 100 | 101 | // this will filter values we don't want to send to Meta/Facebook, i.e. nulls, empty strings, and empty arrays 102 | return array_filter($data, static function ($value): bool { 103 | return !(null === $value || '' === $value || [] === $value); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Event/User.php: -------------------------------------------------------------------------------- 1 | */ 13 | public array $email = []; 14 | 15 | /** @var list */ 16 | public array $phoneNumber = []; 17 | 18 | /** @var list */ 19 | public array $firstName = []; 20 | 21 | /** @var list */ 22 | public array $lastName = []; 23 | 24 | /** @var list */ 25 | public array $gender = []; 26 | 27 | /** @var list<\DateTimeInterface> */ 28 | public array $dateOfBirth = []; 29 | 30 | /** @var list */ 31 | public array $city = []; 32 | 33 | /** @var list */ 34 | public array $state = []; 35 | 36 | /** @var list */ 37 | public array $zipCode = []; 38 | 39 | /** @var list */ 40 | public array $country = []; 41 | 42 | /** @var list */ 43 | public array $externalId = []; 44 | 45 | public ?string $clientIpAddress = null; 46 | 47 | public ?string $clientUserAgent = null; 48 | 49 | /** @var string|Fbc|null */ 50 | public $fbc; 51 | 52 | /** @var string|Fbp|null */ 53 | public $fbp; 54 | 55 | public ?string $subscriptionId = null; 56 | 57 | public ?int $fbLoginId = null; 58 | 59 | public ?int $leadId = null; 60 | 61 | protected function getMapping(string $context): array 62 | { 63 | $mapping = [ 64 | 'em' => $this->email, 65 | 'ph' => $this->phoneNumber, 66 | 'fn' => $this->firstName, 67 | 'ln' => $this->lastName, 68 | 'ge' => $this->gender, 69 | 'db' => $this->dateOfBirth, 70 | 'ct' => $this->city, 71 | 'st' => $this->state, 72 | 'zp' => $this->zipCode, 73 | 'country' => $this->country, 74 | 'external_id' => $this->externalId, 75 | 'client_ip_address' => $this->clientIpAddress, 76 | 'client_user_agent' => $this->clientUserAgent, 77 | 'fbc' => $this->fbc, 78 | 'fbp' => $this->fbp, 79 | 'subscription_id' => $this->subscriptionId, 80 | 'fb_login_id' => $this->fbLoginId, 81 | 'lead_id' => $this->leadId, 82 | ]; 83 | 84 | if (self::PAYLOAD_CONTEXT_BROWSER === $context) { 85 | unset( 86 | $mapping['client_ip_address'], 87 | $mapping['client_user_agent'], 88 | $mapping['fbc'], 89 | $mapping['fbp'], 90 | ); 91 | } 92 | 93 | return $mapping; 94 | } 95 | 96 | /** 97 | * @see \FacebookAds\Object\ServerSide\UserData::normalize 98 | */ 99 | protected static function getNormalizedFields(): array 100 | { 101 | return [ 102 | 'em', 'ph', 'ge', 'db', 'ln', 'fn', 'ct', 'st', 'zp', 'country', 103 | ]; 104 | } 105 | 106 | /** 107 | * @see \FacebookAds\Object\ServerSide\UserData::normalize 108 | */ 109 | protected static function getHashedFields(): array 110 | { 111 | return [ 112 | 'em', 113 | 'ph', 114 | 'fn', 115 | 'ln', 116 | 'ge', 117 | 'db', 118 | 'ct', 119 | 'st', 120 | 'zp', 121 | 'country', 122 | ]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Exception/ClientException.php: -------------------------------------------------------------------------------- 1 | getMessage(), 18 | ); 19 | 20 | return new self($message, 0, $jsonException); 21 | } 22 | 23 | public static function invalidResponseFormat(string $json): self 24 | { 25 | return new self(sprintf( 26 | 'Expected a JSON response like %s, but got %s', 27 | '{"error":{"message":"string","type":"string","code":int,"fbtrace_id":"string"}}', 28 | $json, 29 | )); 30 | } 31 | 32 | public static function fromErrorResponse(ErrorResponse $errorResponse): self 33 | { 34 | $message = sprintf( 35 | "An error occurred sending an event to Meta/Facebook: %s (code: %d, type: %s, trace id: %s)\n\nRaw JSON response:\n\n%s", 36 | $errorResponse->message, 37 | $errorResponse->code, 38 | $errorResponse->type, 39 | $errorResponse->traceId, 40 | $errorResponse->json, 41 | ); 42 | 43 | return new self($message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Generator/FbqGenerator.php: -------------------------------------------------------------------------------- 1 | logger = new NullLogger(); 20 | } 21 | 22 | public function generateInit( 23 | array $pixels, 24 | array $userData = [], 25 | bool $includePageView = true, 26 | bool $includeScriptTag = true, 27 | ): string { 28 | try { 29 | $json = [] !== $userData ? json_encode($userData, \JSON_THROW_ON_ERROR) : null; 30 | } catch (\JsonException $e) { 31 | $this->logger->error($e->getMessage()); 32 | 33 | return ''; 34 | } 35 | 36 | $str = ''; 37 | 38 | foreach ($pixels as $pixel) { 39 | $str .= null === $json ? sprintf("fbq('init', '%s');", $pixel->id) : sprintf("fbq('init', '%s', %s);", $pixel->id, $json); 40 | } 41 | 42 | if ($includePageView) { 43 | $str .= "fbq('track', 'PageView');"; 44 | } 45 | 46 | if ($includeScriptTag) { 47 | $str = sprintf('', $str); 48 | } 49 | 50 | return $str; 51 | } 52 | 53 | public function generateTrack(Event $event, bool $includeScriptTag = true): string 54 | { 55 | $str = sprintf( 56 | "fbq('%s', '%s', %s, {eventID: '%s'});", 57 | $event->isCustom() ? 'trackCustom' : 'track', 58 | $event->eventName, 59 | json_encode($event->customData->getPayload(Parameters::PAYLOAD_CONTEXT_BROWSER), \JSON_THROW_ON_ERROR), 60 | $event->eventId, 61 | ); 62 | 63 | if ($includeScriptTag) { 64 | $str = sprintf('', $str); 65 | } 66 | 67 | return $str; 68 | } 69 | 70 | public function setLogger(LoggerInterface $logger): void 71 | { 72 | $this->logger = $logger; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Generator/FbqGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | $pixels 16 | */ 17 | public function generateInit( 18 | array $pixels, 19 | array $userData = [], 20 | bool $includePageView = true, 21 | bool $includeScriptTag = true, 22 | ): string; 23 | 24 | /** 25 | * Will generate the fbq() tracking call based on the given event and for each pixel ids defined in the event 26 | */ 27 | public function generateTrack(Event $event, bool $includeScriptTag = true): string; 28 | } 29 | -------------------------------------------------------------------------------- /src/Pixel/Pixel.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->accessToken = '' === $accessToken ? null : $accessToken; 25 | } 26 | 27 | public function __toString(): string 28 | { 29 | return $this->id; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ValueObject/Fb.php: -------------------------------------------------------------------------------- 1 | creationTime = (int) ceil(microtime(true) * 1000); 27 | } 28 | 29 | /** 30 | * @throws \InvalidArgumentException if the $value is not the correct format 31 | */ 32 | abstract public static function fromString(string $value): self; 33 | 34 | abstract public function value(): string; 35 | 36 | public function getSubdomainIndex(): int 37 | { 38 | return $this->subdomainIndex; 39 | } 40 | 41 | /** 42 | * @return static 43 | */ 44 | public function withSubdomainIndex(int $subdomainIndex): self 45 | { 46 | Assert::oneOf($subdomainIndex, [ 47 | self::SUBDOMAIN_INDEX_COM, 48 | self::SUBDOMAIN_INDEX_FACEBOOK_COM, 49 | self::SUBDOMAIN_INDEX_WWW_FACEBOOK_COM, 50 | ]); 51 | 52 | $obj = clone $this; 53 | $obj->subdomainIndex = $subdomainIndex; 54 | 55 | return $obj; 56 | } 57 | 58 | public function getCreationTime(): int 59 | { 60 | return $this->creationTime; 61 | } 62 | 63 | /** 64 | * @param int|\DateTimeInterface $creationTime 65 | * 66 | * @return static 67 | */ 68 | public function withCreationTime($creationTime): self 69 | { 70 | if ($creationTime instanceof \DateTimeInterface) { 71 | $creationTime = (int) $creationTime->format('Uv'); 72 | } 73 | 74 | /** @psalm-suppress RedundantConditionGivenDocblockType */ 75 | Assert::integer($creationTime); 76 | Assert::greaterThanEq($creationTime, 1_075_590_000_000); // Facebooks founding date xD 77 | Assert::lessThanEq($creationTime, (time() + 1) * 1000); 78 | 79 | $obj = clone $this; 80 | $obj->creationTime = $creationTime; 81 | 82 | return $obj; 83 | } 84 | 85 | public function getCreationTimeAsDateTime(): \DateTimeImmutable 86 | { 87 | $dateTime = \DateTimeImmutable::createFromFormat('U.v', (string) ($this->creationTime / 1000)); 88 | Assert::notFalse($dateTime); 89 | 90 | return $dateTime; 91 | } 92 | 93 | public function getCreationTimeAsSeconds(): int 94 | { 95 | return (int) ($this->creationTime / 1000); 96 | } 97 | 98 | public function __toString(): string 99 | { 100 | return $this->value(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/ValueObject/Fbc.php: -------------------------------------------------------------------------------- 1 | clickId = $clickId; 21 | } 22 | 23 | public static function fromString(string $value): self 24 | { 25 | // Must match strings like: fb.1.1657051589577.IwAR0rmfgHgxjdKoEopat9y2SPzyjGgfHm9AhdqygToWvarP59nPq15T07MiA 26 | if (preg_match(self::REGEXP_FBC, $value, $matches) !== 1) { 27 | throw new \InvalidArgumentException(sprintf( 28 | 'The value "%s" didn\'t match the expected pattern for fbc: "%s"', 29 | $value, 30 | self::REGEXP_FBC, 31 | )); 32 | } 33 | 34 | return (new self($matches[3])) 35 | ->withSubdomainIndex((int) $matches[1]) 36 | ->withCreationTime((int) $matches[2]) 37 | ; 38 | } 39 | 40 | public function value(): string 41 | { 42 | return sprintf('fb.%d.%d.%s', $this->getSubdomainIndex(), $this->getCreationTime(), $this->clickId); 43 | } 44 | 45 | /** 46 | * This is the facebook click id (i.e. fbclid query parameter) 47 | */ 48 | public function getClickId(): string 49 | { 50 | return $this->clickId; 51 | } 52 | 53 | public function withClickId(string $clickId): self 54 | { 55 | $obj = clone $this; 56 | $obj->clickId = $clickId; 57 | 58 | return $obj; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ValueObject/Fbp.php: -------------------------------------------------------------------------------- 1 | randomNumber = random_int(1_000_000_000, 1_999_999_999); 20 | } 21 | 22 | public static function fromString(string $value): self 23 | { 24 | // Must match something like this: fb.1.1656874832584.1088522659 25 | // NOTICE we match for 13 digits for the creation time. That number will be 14 digits in year 2286, so I guess it's safe to test for a specific number of digits ;) 26 | if (preg_match('/^fb\.([012])\.(\d{13})\.(\d+)$/', $value, $matches) !== 1) { 27 | throw new \InvalidArgumentException(sprintf('The value "%s" didn\'t match the expected pattern for fbp', $value)); 28 | } 29 | 30 | return (new self()) 31 | ->withSubdomainIndex((int) $matches[1]) 32 | ->withCreationTime((int) $matches[2]) 33 | ->withRandomNumber((int) $matches[3]) 34 | ; 35 | } 36 | 37 | public function value(): string 38 | { 39 | return sprintf( 40 | 'fb.%d.%d.%d', 41 | $this->getSubdomainIndex(), 42 | $this->getCreationTime(), 43 | $this->getRandomNumber(), 44 | ); 45 | } 46 | 47 | public function getRandomNumber(): int 48 | { 49 | return $this->randomNumber; 50 | } 51 | 52 | public function withRandomNumber(int $randomNumber): self 53 | { 54 | $obj = clone $this; 55 | $obj->randomNumber = $randomNumber; 56 | 57 | return $obj; 58 | } 59 | } 60 | --------------------------------------------------------------------------------