diff --git a/.gitignore b/.gitignore index 42cd73d..ee0f495 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/vendor/ \ No newline at end of file +/vendor/ +/test/log/*.log \ No newline at end of file diff --git a/README.md b/README.md index a40d044..1334e73 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,194 @@ # php-ga4 PHP Wrapper for Google Analytics 4 + +## Pre-built Events +List of all pre-defined events ready to be used as recommended by the Google Analytics Measurement Protocol. + +- Share +- Signup +- Login +- Search +- SelectContent +- SelectItem +- SelectPromotion +- ViewItem +- ViewItemList +- ViewPromotion +- ViewSearchResults + +### E-commerce +- GenerateLead +- AddToWishlist +- AddToCart +- ViewCart +- RemoveFromCart +- BeginCheckout +- AddPaymentInfo +- AddShippingInfo +- Purchase +- Refund + +### Engagement (Gaming?) +- EarnVirtualCurrency +- SpendVirtualCurrency +- LevelUp +- PostScore +- TutorialBegin +- TutorialComplete +- UnlockAchievement +- JoinGroup + +## Example +```php +setClientId($_COOKIE['_ga'] ?? $_COOKIE['_gid'] ?? $fallback); + if ($loggedIn) { + $analytics->setUserId($uniqueUserId); + } + + $viewCart = new Event\ViewCart(); + $viewCart->setCurrency('EUR'); + + $totalPrice = 0; + foreach ($cartItems as $item) { + $product = new Item(); + $product->setItemId($item['id']); + $product->setItemName($item['name']); + $product->setQuantity($item['qty']); + $totalPrice += $item['price_total']; + $product->setPrice(round($item['price_total'] / $item['qty'], 2)); // unit price + $product->setItemVariant($item['colorName']); + + $viewCart->addItem($product); + } + + $viewCart->setValue($totalPrice); + + $analytics->addEvent($viewCart); + + if (!$analytics->post()) { + // Handling if post was unsuccessfull + } + + // Handling if post was successfull +} catch (GA4Exception $gErr) { + // Handle exception + // Exceptions might be stacked, make sure to check $gErr->getPrevious(); +} +``` + +### Request +```json +{ + "client_id": "GA0.43535.234234", + "user_id": "m6435", + "events": [ + { + "name": "view_cart", + "params": { + "currency": "EUR", + "value": 50.55, + "items": [ + { + "item_id": "1", + "item_name": "product name", + "item_variant": "white", + "price": 17.79, + "quantity": 2 + }, + { + "item_id": "2", + "item_name": "another product name", + "item_variant": "gold", + "price": 4.99, + "quantity": 3 + } + ] + } + } + ] +} +``` + +### Response +```json +{ + "validationMessages": [] +} +``` + +## Custom Events +You can build your own custom events by extending on the Model\Event abstraction class; example + +```php +my_variable = $value; + } + + public function setMyRequiredVariable(string $value) + { + $this->my_required_variable = $value; + } +} +``` + +It's important that you extend the Model\Event class because Analytics checks inheritance towards that class to ensure we grap all parameters and ensures required parameters. + +Property name and value will be used as parameter name and value. + +Just make sure not to use any [Reserved Event Names](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_event_names) + +## Dependencies +- [GuzzleHttp/Guzzle: 6.x](https://packagist.org/packages/guzzlehttp/guzzle) + - Please update [composer.json](composer.json) Guzzle to version 7.x for PHP 8.0+ + +## Source Documentation +- [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4) +- [Measurement Protocol: Reference](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag) +- [Measurement Protocol: User Properties](https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?client_type=gtag) +- [Measurement Protocol: Events](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events) +- [Measurement Protocol: Validation](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag) \ No newline at end of file diff --git a/composer.json b/composer.json index 0d2ad77..5514dd4 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,6 @@ } }, "require": { - "guzzlehttp/guzzle": "^7.4" + "guzzlehttp/guzzle": "^6.0" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..6f0bd2e --- /dev/null +++ b/composer.lock @@ -0,0 +1,672 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0f3b77a86fc2682f5ebbff08bf3f8e45", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "6.5.8", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-06-20T21:43:03+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/src/Analytics.php b/src/Analytics.php new file mode 100644 index 0000000..e55c7ec --- /dev/null +++ b/src/Analytics.php @@ -0,0 +1,173 @@ +measurement_id = $measurementId; + $this->api_secret = $apiSecret; + $this->debug = $debug; + } + + public function getParams(): array + { + return [ + 'non_personalized_ads', + 'timestamp_micros', + 'client_id', + 'user_id', + 'user_properties', + 'events', + ]; + } + + public function getRequiredParams(): array + { + return [ + 'client_id' + ]; + } + + public function allowPersonalisedAds(bool $allow) + { + $this->non_personalized_ads = !$allow; + } + + public function setClientId(string $id) + { + $this->client_id = $id; + } + + public function setUserId(string $id) + { + $this->user_id = $id; + } + + public function setTimestamp(int|float $microOrUnix) + { + $this->timestamp_micros = floor($microOrUnix * 1000); + } + + /** + * Add user property to your analytics request \ + * Maximum is 25 per request + * + * @param AlexWestergaard\PhpGa4\UserProperty $event + * @return int How many events you have added + * @throws AlexWestergaard\PhpGa4\GA4Exception + */ + public function addUserProperty(UserProperty $prop) + { + if (count($this->user_properties) >= 25) { + throw new GA4Exception("Can't add more than 25 user properties"); + } + + $catch = $prop->toArray(); + + $this->user_properties[$catch['name']] = $catch['value']; + return count($this->user_properties); + } + + /** + * Add event to your analytics request \ + * Maximum is 25 per request + * + * @param AlexWestergaard\PhpGa4\Model\Event $event + * @return int How many events you have added + * @throws AlexWestergaard\PhpGa4\GA4Exception + */ + public function addEvent(Model\Event $event) + { + if (count($this->events) >= 25) { + throw new GA4Exception("Can't add more than 25 events"); + } + + $this->events[] = $event->toArray(); + return count($this->events); + } + + /** + * Push your current stack to Google Analytics + * + * @param boolean $validate Same as debug but outputs request and response + * @return bool Whether the request returned status 200 + * @throws AlexWestergaard\PhpGa4\GA4Exception + */ + public function post(bool $validate = false) + { + $errorStack = null; + + $url = $this->debug || $validate ? $this::URL_DEBUG : $this::URL_LIVE; + $url .= '?' . http_build_query(['measurement_id' => $this->measurement_id, 'api_secret' => $this->api_secret]); + + $catch = parent::toArray(true, $errorStack); + $errorStack = $catch['error']; + $reqBody = $catch['data']; + + $kB = 1024; + if (mb_strlen(json_encode($reqBody)) > ($kB * 130)) { + $errorStack = new GA4Exception("Request body exceeds 130kB", $errorStack); + } + + if ($errorStack instanceof GA4Exception) { + throw $errorStack; + } + + $guzzle = new Guzzle(); + $res = $guzzle->request('POST', $url, ['json' => $reqBody]); + + $resCode = $res->getStatusCode() ?? 0; + if ($resCode !== 200) { + $errorStack = new GA4Exception("Request received code {$resCode}", $errorStack); + } + + $resBody = $res->getBody()->getContents(); + $data = @json_decode($resBody, true); + + if (empty($resBody)) { + $errorStack = new GA4Exception("Received not body", $errorStack); + } elseif (json_last_error() != JSON_ERROR_NONE || $data === null) { + $errorStack = new GA4Exception("Could not parse response", $errorStack); + } elseif (!empty($data['validationMessages'])) { + foreach ($data['validationMessages'] as $msg) { + $errorStack = new GA4Exception('Validation Message: ' . $msg['validationCode'] . '[' . $msg['fieldPath'] . ']: ' . $msg['description'], $errorStack); + } + } + + if ($validate) { + echo "Request \\ ", $url, "\r\n", json_encode($reqBody, JSON_PRETTY_PRINT), "\r\n\r\n"; + echo "Response \\ ", $resCode, "\r\n", json_encode($data, JSON_PRETTY_PRINT), "\r\n\r\n"; + } + + if ($errorStack instanceof GA4Exception) { + throw $errorStack; + } + + return $resCode === 200; + } + + public function toArray(bool $isParent = false, $childErrors = null): array + { + return parent::toArray($isParent, $childErrors); + } +} diff --git a/src/Event/AddPaymentInfo.php b/src/Event/AddPaymentInfo.php new file mode 100644 index 0000000..f60fce7 --- /dev/null +++ b/src/Event/AddPaymentInfo.php @@ -0,0 +1,75 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function setCoupon(string $code) + { + $this->coupon = $code; + } + + public function setPaymentType(string $type) + { + $this->payment_type = $type; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/AddShippingInfo.php b/src/Event/AddShippingInfo.php new file mode 100644 index 0000000..e30b916 --- /dev/null +++ b/src/Event/AddShippingInfo.php @@ -0,0 +1,75 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function setCoupon(string $code) + { + $this->coupon = $code; + } + + public function setShippingTier(string $tier) + { + $this->shipping_tier = $tier; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/AddToCart.php b/src/Event/AddToCart.php new file mode 100644 index 0000000..b807cc0 --- /dev/null +++ b/src/Event/AddToCart.php @@ -0,0 +1,61 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/AddToWishlist.php b/src/Event/AddToWishlist.php new file mode 100644 index 0000000..8f5217f --- /dev/null +++ b/src/Event/AddToWishlist.php @@ -0,0 +1,61 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/BeginCheckout.php b/src/Event/BeginCheckout.php new file mode 100644 index 0000000..888661c --- /dev/null +++ b/src/Event/BeginCheckout.php @@ -0,0 +1,68 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function setCoupon(string $code) + { + $this->coupon = $code; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/EarnVirtualCurrency.php b/src/Event/EarnVirtualCurrency.php new file mode 100644 index 0000000..752591e --- /dev/null +++ b/src/Event/EarnVirtualCurrency.php @@ -0,0 +1,40 @@ +virtual_currency_name = $name; + } + + public function setValue(int $num) + { + $this->value = $num; + } +} diff --git a/src/Event/GenerateLead.php b/src/Event/GenerateLead.php new file mode 100644 index 0000000..a42f742 --- /dev/null +++ b/src/Event/GenerateLead.php @@ -0,0 +1,52 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } +} diff --git a/src/Event/JoinGroup.php b/src/Event/JoinGroup.php new file mode 100644 index 0000000..9c4e7c1 --- /dev/null +++ b/src/Event/JoinGroup.php @@ -0,0 +1,33 @@ +group_id = $id; + } +} diff --git a/src/Event/LevelUp.php b/src/Event/LevelUp.php new file mode 100644 index 0000000..f236136 --- /dev/null +++ b/src/Event/LevelUp.php @@ -0,0 +1,40 @@ +level = $lvl; + } + + public function setCharacter(string $char) + { + $this->character = $char; + } +} diff --git a/src/Event/Login.php b/src/Event/Login.php new file mode 100644 index 0000000..a47171b --- /dev/null +++ b/src/Event/Login.php @@ -0,0 +1,33 @@ +method = $method; + } +} diff --git a/src/Event/PostScore.php b/src/Event/PostScore.php new file mode 100644 index 0000000..98e0abd --- /dev/null +++ b/src/Event/PostScore.php @@ -0,0 +1,47 @@ +level = $score; + } + + public function setLevel(int $lvl) + { + $this->level = $lvl; + } + + public function setCharacter(string $char) + { + $this->character = $char; + } +} diff --git a/src/Event/Purchase.php b/src/Event/Purchase.php new file mode 100644 index 0000000..e19804f --- /dev/null +++ b/src/Event/Purchase.php @@ -0,0 +1,97 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'transaction_id'; + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setTransactionId(string $id) + { + $this->transaction_id = $id; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function setAffiliation(string $affiliation) + { + $this->affiliation = $affiliation; + } + + public function setCoupon(string $code) + { + $this->coupon = $code; + } + + public function setShipping(int $cost) + { + $this->shipping = $cost; + } + + public function setTax(int $tax) + { + $this->tax = $tax; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/Refund.php b/src/Event/Refund.php new file mode 100644 index 0000000..fa7c536 --- /dev/null +++ b/src/Event/Refund.php @@ -0,0 +1,97 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'transaction_id'; + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setTransactionId(string $id) + { + $this->transaction_id = $id; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function setAffiliation(string $affiliation) + { + $this->affiliation = $affiliation; + } + + public function setCoupon(string $code) + { + $this->coupon = $code; + } + + public function setShipping(int $cost) + { + $this->shipping = $cost; + } + + public function setTax(int $tax) + { + $this->tax = $tax; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/RemoveFromCart.php b/src/Event/RemoveFromCart.php new file mode 100644 index 0000000..32a6c3c --- /dev/null +++ b/src/Event/RemoveFromCart.php @@ -0,0 +1,61 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/Search.php b/src/Event/Search.php new file mode 100644 index 0000000..d8a3d60 --- /dev/null +++ b/src/Event/Search.php @@ -0,0 +1,33 @@ +search_term = $term; + } +} diff --git a/src/Event/SelectContent.php b/src/Event/SelectContent.php new file mode 100644 index 0000000..1532747 --- /dev/null +++ b/src/Event/SelectContent.php @@ -0,0 +1,40 @@ +content_type = $type; + } + + public function setItemId(string $id) + { + $this->item_id = $id; + } +} diff --git a/src/Event/SelectItem.php b/src/Event/SelectItem.php new file mode 100644 index 0000000..8e4d263 --- /dev/null +++ b/src/Event/SelectItem.php @@ -0,0 +1,48 @@ +item_list_id = $id; + } + + public function setItemListName(string $name) + { + $this->item_list_name = $name; + } + + public function setItem(Item $item) + { + $this->items = $item->toArray(); + } +} diff --git a/src/Event/SelectPromotion.php b/src/Event/SelectPromotion.php new file mode 100644 index 0000000..079472c --- /dev/null +++ b/src/Event/SelectPromotion.php @@ -0,0 +1,69 @@ +creative_name = $name; + } + + public function setCreativeSlot(string $slot) + { + $this->creative_slot = $slot; + } + + public function setLocationId(string $id) + { + $this->location_id = $id; + } + + public function setPromotionId(string $id) + { + $this->promotion_id = $id; + } + + public function setPromotionName(string $name) + { + $this->promotion_name = $name; + } + + public function addItem(Item $item) + { + $this->items = $item->toArray(); + } +} diff --git a/src/Event/Share.php b/src/Event/Share.php new file mode 100644 index 0000000..f561040 --- /dev/null +++ b/src/Event/Share.php @@ -0,0 +1,47 @@ +method = $method; + } + + public function setContentType(string $type) + { + $this->content_type = $type; + } + + public function setItemId(string $id) + { + $this->item_id = $id; + } +} diff --git a/src/Event/Signup.php b/src/Event/Signup.php new file mode 100644 index 0000000..b5fd457 --- /dev/null +++ b/src/Event/Signup.php @@ -0,0 +1,33 @@ +method = $method; + } +} diff --git a/src/Event/SpendVirtualCurrency.php b/src/Event/SpendVirtualCurrency.php new file mode 100644 index 0000000..e530f63 --- /dev/null +++ b/src/Event/SpendVirtualCurrency.php @@ -0,0 +1,50 @@ +virtual_currency_name = $name; + } + + public function setValue(int $num) + { + $this->value = $num; + } + + public function setItemName(int $name) + { + $this->item_name = $name; + } +} diff --git a/src/Event/TutorialBegin.php b/src/Event/TutorialBegin.php new file mode 100644 index 0000000..f4dce0c --- /dev/null +++ b/src/Event/TutorialBegin.php @@ -0,0 +1,24 @@ +achievement_id = $id; + } +} diff --git a/src/Event/ViewCart.php b/src/Event/ViewCart.php new file mode 100644 index 0000000..6ca995d --- /dev/null +++ b/src/Event/ViewCart.php @@ -0,0 +1,61 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/ViewItem.php b/src/Event/ViewItem.php new file mode 100644 index 0000000..ed6108b --- /dev/null +++ b/src/Event/ViewItem.php @@ -0,0 +1,61 @@ +currency) && !isset($this->value) + || !isset($this->currency) && isset($this->value) + ) { + $return = [ + 'currency', + 'value' + ]; + } + + $return[] = 'items'; + return $return; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setValue(int|float $val) + { + $this->value = $val; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/ViewItemList.php b/src/Event/ViewItemList.php new file mode 100644 index 0000000..bd6b008 --- /dev/null +++ b/src/Event/ViewItemList.php @@ -0,0 +1,48 @@ +item_list_id = $id; + } + + public function setItemListName(string $name) + { + $this->item_list_name = $name; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/Event/ViewPromotion.php b/src/Event/ViewPromotion.php new file mode 100644 index 0000000..e1d3fc4 --- /dev/null +++ b/src/Event/ViewPromotion.php @@ -0,0 +1,69 @@ +creative_name = $name; + } + + public function setCreativeSlot(string $slot) + { + $this->creative_slot = $slot; + } + + public function setLocationId(string $id) + { + $this->location_id = $id; + } + + public function setPromotionId(string $id) + { + $this->promotion_id = $id; + } + + public function setPromotionName(string $name) + { + $this->promotion_name = $name; + } + + public function addItem(Item $item) + { + $this->items = $item->toArray(); + } +} diff --git a/src/Event/ViewSearchResults.php b/src/Event/ViewSearchResults.php new file mode 100644 index 0000000..2351d06 --- /dev/null +++ b/src/Event/ViewSearchResults.php @@ -0,0 +1,43 @@ +search_term = $term; + } + + public function addItem(Item $item) + { + $this->items[] = $item->toArray(); + } +} diff --git a/src/GA4Exception.php b/src/GA4Exception.php new file mode 100644 index 0000000..d45124a --- /dev/null +++ b/src/GA4Exception.php @@ -0,0 +1,11 @@ +item_id = $id; + } + + public function setItemName(string $name) + { + $this->item_name = $name; + } + + public function setAffiliation(string $affiliation) + { + $this->affiliation = $affiliation; + } + + public function setCoupon(string $code) + { + $this->coupon = $code; + } + + public function setCurrency(string $iso) + { + $this->currency = $iso; + } + + public function setDiscount(int|float $amount) + { + $this->discount = $amount; + } + + public function setIndex(int $i) + { + $this->index = $i; + } + + public function setItemBrand(string $brand) + { + $this->item_brand = $brand; + } + + public function addItemCategory(string $category) + { + $this->item_category[] = $category; + } + + public function setItemListId(string $id) + { + $this->item_list_id = $id; + } + + public function setItemListName(string $name) + { + $this->item_list_name = $name; + } + + public function setItemVariant(string $variant) + { + $this->item_variant = $variant; + } + + public function setLocationId(string $id) + { + $this->location_id = $id; + } + + public function setPrice(int|float $amount) + { + $this->price = $amount; + } + + public function setQuantity(int $amount) + { + $this->quantity = $amount; + } + + public function getParams(): array + { + return [ + 'item_id', + 'item_name', + 'affiliation', + 'coupon', + 'currency', + 'discount', + 'index', + 'item_brand', + 'item_category', + 'item_list_id', + 'item_list_name', + 'item_variant', + 'location_id', + 'price', + 'quantity', + ]; + } + + public function getRequiredParams(): array + { + $return = []; + + if ( + (!isset($this->item_id) || empty($this->item_id) && strval($this->item_id) !== '0') + && (!isset($this->item_name) || empty($this->item_name) && strval($this->item_name) !== '0') + ) { + $return = [ + 'item_id', + 'item_name', + ]; + } + + return $return; + } + + public function toArray(bool $isParent = false, $childErrors = null): array + { + $return = parent::toArray($isParent, $childErrors); + + if (isset($return['item_category'])) { + $cats = $return['item_category']; + unset($return['item_category']); + + foreach ($cats as $i => $v) { + $id = $i > 0 ? $i + 1 : ''; + $return['item_category' . $id] = $v; + } + } + + return $return; + } +} diff --git a/src/Model/Event.php b/src/Model/Event.php new file mode 100644 index 0000000..b7b6073 --- /dev/null +++ b/src/Model/Event.php @@ -0,0 +1,79 @@ +getName(); + if (empty($name)) { + $errorStack = new GA4Exception("Name '{$name}' can not be empty", $errorStack); + } elseif (strlen($name) > 40) { + $errorStack = new GA4Exception("Name '{$name}' can not be longer than 40 characters", $errorStack); + } elseif (preg_match('/[^\w\d\-]/', $name)) { + $errorStack = new GA4Exception("Name '{$name}' can only be letters, numbers, underscores and dashes", $errorStack); + } elseif (in_array($name, [ + 'ad_activeview', + 'ad_click', + 'ad_exposure', + 'ad_impression', + 'ad_query', + 'adunit_exposure', + 'app_clear_data', + 'app_install', + 'app_update', + 'app_remove', + 'error', + 'first_open', + 'first_visit', + 'in_app_purchase', + 'notification_dismiss', + 'notification_foreground', + 'notification_open', + 'notification_receive', + 'os_update', + 'screen_view', + 'session_start', + 'user_engagement', + ])) { + $errorStack = new GA4Exception("Name '{$name}' is reserved", $errorStack); + } else { + $return['name'] = $name; + } + } + + $catch = parent::toArray(true, $errorStack); + $errorStack = $catch['error']; + + if (is_array($catch['data']) && !empty($catch['data'])) { + $return['params'] = $catch['data']; + } + + if ($errorStack instanceof GA4Exception) { + throw $errorStack; + } + + return $return; + } +} diff --git a/src/Model/ToArray.php b/src/Model/ToArray.php new file mode 100644 index 0000000..53fd05b --- /dev/null +++ b/src/Model/ToArray.php @@ -0,0 +1,67 @@ +getRequiredParams(); + $params = array_unique(array_merge($this->getParams(), $required)); + + foreach ($params as $param) { + if (!property_exists($this, $param)) { + $errorStack = new GA4Exception("Param '{$param}' is not defined", $errorStack); + continue; + } elseif (!isset($this->{$param})) { + if (in_array($param, $required)) { + $errorStack = new GA4Exception("Param '{$param}' is required but not set", $errorStack); + } + continue; + } elseif (empty($this->{$param}) && (is_array($this->{$param}) || strval($this->{$param}) !== '0')) { + if (in_array($param, $required)) { + $errorStack = new GA4Exception("Param '{$param}' is required but empty", $errorStack); + } + continue; + } + + if (strlen($param) > 40) { + $errorStack = new GA4Exception("Param '{$param}' is too long, maximum is 40 characters", $errorStack); + } + + $value = $this->{$param}; + + // Array values be handled and validated within setter, fx addItem/setItem + if (!is_array($value) && mb_strlen($value) > 100) { + $errorStack = new GA4Exception("Value '{$value}' is too long, maximum is 100 characters", $errorStack); + } + + $return[$param] = $value; + } + + if ($isParent) { + return [ + 'data' => $return, + 'error' => $errorStack + ]; + } elseif ($errorStack instanceof GA4Exception) { + throw $errorStack; + } + + return $return; + } +} diff --git a/src/UserProperty.php b/src/UserProperty.php new file mode 100644 index 0000000..2a70048 --- /dev/null +++ b/src/UserProperty.php @@ -0,0 +1,66 @@ + 24) { + throw new GA4Exception("Name '{$name}' is longer than 24 characters"); + } + + $this->name = $name; + } + + public function setValue($value) + { + if (!is_string($value) && !is_numeric($value)) { + throw new GA4Exception("Value '{$value}' should be a string or number"); + } + + $this->value = $value; + } + + public function getParams(): array + { + return ['name', 'value']; + } + + public function getRequiredParams(): array + { + return ['name', 'value']; + } + + public function toArray(): array + { + $return = [ + 'name' => $this->name, + 'value' => $this->value, + ]; + + if (!is_array($this->value)) { + $return['value'] = ['value' => $return['value']]; + } + + return $return; + } +}