Skip to content

Commit 27ed2fe

Browse files
committed
Add Firebase handler
1 parent b7c256c commit 27ed2fe

File tree

4 files changed

+296
-0
lines changed

4 files changed

+296
-0
lines changed

src/Exception/Ga4Exception.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ public static function throwMissingMeasurementId()
3737
return new static("Missing Measurement ID", static::REQUEST_MISSING_MEASUREMENT_ID);
3838
}
3939

40+
public static function throwMissingFirebaseAppId()
41+
{
42+
return new static("Missing Firebase APP ID", static::REQUEST_MISSING_FIREBASE_APP_ID);
43+
}
44+
45+
public static function throwMissingAppInstanceId()
46+
{
47+
return new static("Missing Application Instance ID", static::REQUEST_MISSING_FIREBASE_APP_INSTANCE_ID);
48+
}
49+
4050
public static function throwMissingApiSecret()
4151
{
4252
return new static("Missing API Secret", static::REQUEST_MISSING_API_SECRET);

src/Facade/Type/FirebaseType.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4\Facade\Type;
4+
5+
interface FirebaseType extends IOType
6+
{
7+
const URL_LIVE = 'https://www.google-analytics.com/mp/collect';
8+
const URL_DEBUG = 'https://www.google-analytics.com/debug/mp/collect';
9+
10+
const ACCEPT_RESPONSE_HEADERS = [200, 204];
11+
12+
/**
13+
* Uniquely identifies a installation instance of an application.
14+
*
15+
* @var client_id
16+
* @param string $id eg. Cookie._ga or Cookie._gid
17+
*/
18+
public function setAppInstanceId(string $id);
19+
20+
/**
21+
* A unique identifier for a user. See User-ID for cross-platform analysis for more information on this identifier.
22+
*
23+
* @var user_id
24+
* @param string $id eg. Unique User Id
25+
*/
26+
public function setUserId(string $id);
27+
28+
/**
29+
* A Unix timestamp (in microseconds) for the time to associate with the event. This should only be set to record events that happened in the past. \
30+
* This value can be overridden via user_property or event timestamps. Events can be backdated up to 3 calendar days based on the property's timezone.
31+
*
32+
* @var timestamp_micros
33+
* @param integer|float $microOrUnix microtime(true) or time()
34+
*/
35+
public function setTimestampMicros(int|float $microOrUnix);
36+
37+
/**
38+
* Indicate if these events should be used for personalized ads.
39+
*
40+
* @var non_personalized_ads
41+
* @param boolean $allow
42+
*/
43+
public function setNonPersonalizedAds(bool $allow);
44+
45+
/**
46+
* The user properties for the measurement (Up to 25 custom per project, see link)
47+
*
48+
* @var user_properties
49+
* @param AlexWestergaard\PhpGa4\Facade\Type\UserProperty $prop
50+
* @link https://support.google.com/analytics/answer/14240153
51+
*/
52+
public function addUserProperty(UserPropertyType ...$props);
53+
54+
/**
55+
* An array of event items. Up to 25 events can be sent per request
56+
*
57+
* @var events
58+
* @param AlexWestergaard\PhpGa4\Facade\Type\Event $event
59+
*/
60+
public function addEvent(EventType ...$events);
61+
62+
/**
63+
* Validate params and send it to Google Analytics
64+
*
65+
* @return void
66+
* @throws AlexWestergaard\PhpGa4\Exception\Ga4Exception
67+
*/
68+
public function post();
69+
}

src/Facade/Type/Ga4ExceptionType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ interface Ga4ExceptionType
2525
const REQUEST_MISSING_MEASUREMENT_ID = 104006;
2626
const REQUEST_MISSING_API_SECRET = 104007;
2727
const REQUEST_EMPTY_EVENTLIST = 104008;
28+
29+
const REQUEST_MISSING_FIREBASE_APP_ID = 105001;
30+
const REQUEST_MISSING_FIREBASE_APP_INSTANCE_ID = 105002;
2831
}

src/Firebase.php

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4;
4+
5+
use GuzzleHttp\Client as Guzzle;
6+
use AlexWestergaard\PhpGa4\Helper;
7+
use AlexWestergaard\PhpGa4\Facade;
8+
use AlexWestergaard\PhpGa4\Exception\Ga4Exception;
9+
10+
class Firebase extends Helper\IOHelper implements Facade\Type\FirebaseType
11+
{
12+
private Guzzle $guzzle;
13+
14+
private Helper\ConsentHelper $consent;
15+
private Helper\UserDataHelper $userdata;
16+
17+
protected null|bool $non_personalized_ads = false;
18+
protected null|int $timestamp_micros;
19+
protected null|string $app_instance_id;
20+
protected null|string $user_id;
21+
protected array $user_properties = [];
22+
protected array $events = [];
23+
24+
public function __construct(
25+
private string $firebase_app_id,
26+
private string $api_secret,
27+
private bool $debug = false
28+
) {
29+
parent::__construct();
30+
$this->guzzle = new Guzzle();
31+
$this->consent = new Helper\ConsentHelper();
32+
$this->userdata = new Helper\UserDataHelper();
33+
}
34+
35+
public function getParams(): array
36+
{
37+
return [
38+
'non_personalized_ads',
39+
'timestamp_micros',
40+
'app_instance_id',
41+
'user_id',
42+
'user_properties',
43+
'events',
44+
];
45+
}
46+
47+
public function getRequiredParams(): array
48+
{
49+
return ['app_instance_id'];
50+
}
51+
52+
public function setAppInstanceId(string $id)
53+
{
54+
$this->app_instance_id = $id;
55+
return $this;
56+
}
57+
58+
public function setUserId(string $id)
59+
{
60+
$this->user_id = $id;
61+
return $this;
62+
}
63+
64+
public function setTimestampMicros(int|float $microOrUnix)
65+
{
66+
$min = Helper\ConvertHelper::timeAsMicro(strtotime('-3 days') + 10);
67+
$max = Helper\ConvertHelper::timeAsMicro(time() + 3);
68+
69+
$time = Helper\ConvertHelper::timeAsMicro($microOrUnix);
70+
71+
if ($time < $min || $time > $max) {
72+
throw Ga4Exception::throwMicrotimeExpired();
73+
}
74+
75+
$this->timestamp_micros = $time;
76+
return $this;
77+
}
78+
79+
public function addUserProperty(Facade\Type\UserPropertyType ...$props)
80+
{
81+
foreach ($props as $prop) {
82+
$this->user_properties = array_replace($this->user_properties, $prop->toArray());
83+
}
84+
85+
return $this;
86+
}
87+
88+
public function addEvent(Facade\Type\EventType ...$events)
89+
{
90+
foreach ($events as $event) {
91+
$this->events[] = $event->toArray();
92+
}
93+
94+
return $this;
95+
}
96+
97+
public function consent(): Helper\ConsentHelper
98+
{
99+
return $this->consent;
100+
}
101+
102+
public function userdata(): Helper\UserDataHelper
103+
{
104+
return $this->userdata;
105+
}
106+
107+
public function post(): void
108+
{
109+
if (empty($this->firebase_app_id)) {
110+
throw Ga4Exception::throwMissingFirebaseAppId();
111+
}
112+
113+
if (empty($this->api_secret)) {
114+
throw Ga4Exception::throwMissingApiSecret();
115+
}
116+
117+
if (empty($this->app_instance_id)) {
118+
throw Ga4Exception::throwMissingAppInstanceId();
119+
}
120+
121+
$url = $this->debug ? Facade\Type\AnalyticsType::URL_DEBUG : Facade\Type\AnalyticsType::URL_LIVE;
122+
$url .= '?' . http_build_query(['firebase_app_id' => $this->firebase_app_id, 'api_secret' => $this->api_secret]);
123+
124+
$body = array_replace_recursive(
125+
$this->toArray(),
126+
["user_data" => !empty($this->user_id) ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too
127+
["user_properties" => $this->user_properties],
128+
["consent" => $this->consent->toArray()],
129+
);
130+
131+
if (count($body["user_data"]) < 1) unset($body["user_data"]);
132+
if (count($body["user_properties"]) < 1) unset($body["user_properties"]);
133+
134+
$chunkEvents = array_chunk($this->events, 25);
135+
136+
if (count($chunkEvents) < 1) {
137+
throw Ga4Exception::throwMissingEvents();
138+
}
139+
140+
$this->userdata->reset();
141+
$this->user_properties = [];
142+
$this->events = [];
143+
144+
foreach ($chunkEvents as $events) {
145+
$body['events'] = $events;
146+
147+
$kB = 1024;
148+
if (($size = mb_strlen(json_encode($body))) > ($kB * 130)) {
149+
Ga4Exception::throwRequestTooLarge(intval($size / $kB));
150+
continue;
151+
}
152+
153+
$jsonBody = json_encode($body);
154+
$jsonBody = strtr($jsonBody, [':[]' => ':{}']);
155+
156+
$res = $this->guzzle->request('POST', $url, [
157+
'headers' => [
158+
'content-type' => 'application/json;charset=utf-8'
159+
],
160+
'body' => $jsonBody,
161+
]);
162+
163+
if (!in_array(($code = $res?->getStatusCode() ?? 0), Facade\Type\AnalyticsType::ACCEPT_RESPONSE_HEADERS)) {
164+
Ga4Exception::throwRequestWrongResponceCode($code);
165+
}
166+
167+
if ($code !== 204) {
168+
$callback = @json_decode($res->getBody()->getContents(), true);
169+
170+
if (json_last_error() != JSON_ERROR_NONE) {
171+
Ga4Exception::throwRequestInvalidResponse();
172+
} elseif (empty($callback)) {
173+
Ga4Exception::throwRequestEmptyResponse();
174+
} elseif (!empty($callback['validationMessages'])) {
175+
foreach ($callback['validationMessages'] as $msg) {
176+
Ga4Exception::throwRequestInvalidBody($msg);
177+
}
178+
}
179+
}
180+
}
181+
182+
if (Ga4Exception::hasThrowStack()) {
183+
throw Ga4Exception::getThrowStack();
184+
}
185+
}
186+
187+
public static function new(string $firebase_app_id, string $api_secret, bool $debug = false): static
188+
{
189+
return new static($firebase_app_id, $api_secret, $debug);
190+
}
191+
192+
/**
193+
* Deprecated references
194+
*/
195+
196+
/** @deprecated 1.1.9 Please use `Analytics->consent->setAdPersonalizationPermission()` instead */
197+
public function setNonPersonalizedAds(bool $exclude)
198+
{
199+
$this->consent->setAdPersonalizationPermission(!$exclude);
200+
return $this;
201+
}
202+
203+
/** @deprecated 1.1.1 Please use `Analytics->consent->setAdPersonalizationPermission()` instead */
204+
public function allowPersonalisedAds(bool $allow)
205+
{
206+
$this->consent->setAdPersonalizationPermission($allow);
207+
}
208+
209+
/** @deprecated 1.1.1 Please use `Analytics->setTimestampMicros()` instead */
210+
public function setTimestamp(int|float $microOrUnix)
211+
{
212+
$this->setTimestampMicros($microOrUnix);
213+
}
214+
}

0 commit comments

Comments
 (0)