├── .github ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── ci.yml │ ├── clear-cache.yml │ └── cron.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Analytics.php ├── Event │ ├── AddPaymentInfo.php │ ├── AddShippingInfo.php │ ├── AddToCart.php │ ├── AddToWishlist.php │ ├── BeginCheckout.php │ ├── EarnVirtualCurrency.php │ ├── Exception.php │ ├── GenerateLead.php │ ├── JoinGroup.php │ ├── LevelUp.php │ ├── Login.php │ ├── PageView.php │ ├── PostScore.php │ ├── Purchase.php │ ├── Refund.php │ ├── RemoveFromCart.php │ ├── Search.php │ ├── SelectContent.php │ ├── SelectItem.php │ ├── SelectPromotion.php │ ├── Share.php │ ├── Signup.php │ ├── SpendVirtualCurrency.php │ ├── TutorialBegin.php │ ├── TutorialComplete.php │ ├── UnlockAchievement.php │ ├── ViewCart.php │ ├── ViewItem.php │ ├── ViewItemList.php │ ├── ViewPromotion.php │ └── ViewSearchResults.php ├── Exception │ ├── Ga4EventException.php │ ├── Ga4Exception.php │ ├── Ga4IOException.php │ └── Ga4UserPropertyException.php ├── Facade │ ├── Group │ │ ├── AddPaymentInfoFacade.php │ │ ├── AddShippingInfoFacade.php │ │ ├── AddToCartFacade.php │ │ ├── AddToWishlistFacade.php │ │ ├── AnalyticsFacade.php │ │ ├── BeginCheckoutFacade.php │ │ ├── EarnVirtualCurrencyFacade.php │ │ ├── ExceptionFacade.php │ │ ├── ExportFacade.php │ │ ├── GenerateLeadFacade.php │ │ ├── ItemFacade.php │ │ ├── JoinGroupFacade.php │ │ ├── LevelUpFacade.php │ │ ├── LoginFacade.php │ │ ├── PageViewFacade.php │ │ ├── PostScoreFacade.php │ │ ├── PurchaseFacade.php │ │ ├── RefundFacade.php │ │ ├── RemoveFromCartFacade.php │ │ ├── SearchFacade.php │ │ ├── SelectContentFacade.php │ │ ├── SelectItemFacade.php │ │ ├── SelectPromotionFacade.php │ │ ├── ShareFacade.php │ │ ├── SignUpFacade.php │ │ ├── SpendVirtualCurrencyFacade.php │ │ ├── UnlockAchievementFacade.php │ │ ├── ViewCartFacade.php │ │ ├── ViewItemFacade.php │ │ ├── ViewItemListFacade.php │ │ ├── ViewPromotionFacade.php │ │ ├── ViewSearchResultsFacade.php │ │ └── hasItemsFacade.php │ └── Type │ │ ├── AnalyticsType.php │ │ ├── DefaultEventParamsType.php │ │ ├── EventType.php │ │ ├── FirebaseType.php │ │ ├── Ga4ExceptionType.php │ │ ├── GtmEventType.php │ │ ├── IOType.php │ │ ├── ItemType.php │ │ └── UserPropertyType.php ├── Firebase.php ├── Helper │ ├── ConsentHelper.php │ ├── ConvertHelper.php │ ├── CountryIsoHelper.php │ ├── EventHelper.php │ ├── EventMainHelper.php │ ├── EventParamsHelper.php │ ├── IOHelper.php │ ├── UserDataHelper.php │ └── UserPropertyHelper.php ├── Item.php └── UserProperty.php └── test ├── FirebaseTestCase.php ├── MeasurementTestCase.php ├── Mocks ├── MockEventHelper.php ├── MockIOHelper.php └── MockUserPropertyHelper.php └── Unit ├── AbstractTest.php ├── AnalyticsTest.php ├── ConsentTest.php ├── EventTest.php ├── FirebaseTest.php ├── HelperTest.php ├── ItemTest.php ├── UserDataTest.php └── UserPropertyTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | day: "monday" 9 | assignees: 10 | - "octocat" 11 | 12 | # Maintain dependencies for Composer 13 | - package-ecosystem: "composer" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | day: "monday" 18 | assignees: 19 | - "octocat" 20 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | concurrency: 9 | group: ${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | phpunit-render-badge: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: "Checkout" 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: "Setup PHP" 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "8.3" 26 | extensions: mbstring, intl 27 | ini-values: post_max_size=256M, max_execution_time=180 28 | coverage: xdebug 29 | 30 | - name: "Composer State" 31 | run: composer update --no-install --with-all-dependencies 32 | 33 | - name: "Composer Name Hash" 34 | id: composer-hash 35 | uses: KEINOS/gh-action-hash-for-cache@main 36 | with: 37 | path: ./composer.lock 38 | 39 | - name: "Caching" 40 | id: cache-composer 41 | uses: actions/cache@v4 42 | with: 43 | path: vendor 44 | key: composer-default-${{ steps.composer-hash.outputs.hash }} 45 | restore-keys: composer-default-${{ steps.composer-hash.outputs.hash }} 46 | 47 | - name: "Install Dependencies" 48 | if: ${{ steps.cache-composer.outputs.cache-hit != 'true' }} 49 | run: composer install 50 | 51 | - name: "Linux: Restore Vendor Executable" 52 | run: chmod -R 0755 vendor 53 | 54 | - name: "PHPUnit" 55 | run: ./vendor/bin/phpunit 56 | 57 | - name: "Make code coverage badge" 58 | uses: timkrase/phpunit-coverage-badge@v1.2.1 59 | with: 60 | coverage_badge_path: .github/coverage.svg 61 | # push badge later on 62 | push_badge: false 63 | 64 | - name: "Git push badges to origin/image-data" 65 | uses: peaceiris/actions-gh-pages@v4 66 | with: 67 | publish_dir: .github 68 | publish_branch: image-data 69 | github_token: ${{ secrets.GITHUB_TOKEN }} 70 | user_name: "github-actions[bot]" 71 | user_email: "github-actions[bot]@users.noreply.github.com" 72 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | workflow_call: 5 | pull_request: 6 | branches: 7 | - "master" 8 | 9 | concurrency: 10 | group: ${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | validate-master: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: "Checkout" 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: "Validate Mergable" 23 | run: git merge origin/master --no-commit --ff-only 24 | 25 | phpunit-composer-latest: 26 | needs: validate-master 27 | runs-on: ${{ matrix.operating-system }} 28 | 29 | strategy: 30 | max-parallel: 3 31 | fail-fast: true 32 | matrix: 33 | operating-system: ["ubuntu-latest"] 34 | php-versions: ["8.0", "8.1", "8.2", "8.3", "8.4"] 35 | 36 | steps: 37 | - name: "Checkout" 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: "Setup PHP" 43 | uses: shivammathur/setup-php@v2 44 | with: 45 | php-version: ${{ matrix.php-versions }} 46 | extensions: mbstring, intl 47 | ini-values: post_max_size=256M, max_execution_time=180 48 | coverage: xdebug 49 | 50 | - name: "Composer State" 51 | run: composer update --no-install --with-all-dependencies 52 | 53 | - name: "Composer Name Hash" 54 | id: composer-hash 55 | uses: KEINOS/gh-action-hash-for-cache@main 56 | with: 57 | path: ./composer.lock 58 | 59 | - name: "Caching" 60 | id: cache-composer 61 | uses: actions/cache@v4 62 | with: 63 | path: vendor 64 | key: composer-default-${{ steps.composer-hash.outputs.hash }} 65 | restore-keys: composer-default-${{ steps.composer-hash.outputs.hash }} 66 | 67 | - name: "Install Dependencies" 68 | if: ${{ steps.cache-composer.outputs.cache-hit != 'true' }} 69 | run: composer install 70 | 71 | - name: "Linux: Restore Vendor Executable" 72 | run: chmod -R 0755 vendor 73 | 74 | - name: "PHPUnit" 75 | run: ./vendor/bin/phpunit 76 | 77 | phpunit-composer-lowest: 78 | needs: validate-master 79 | runs-on: ${{ matrix.operating-system }} 80 | 81 | strategy: 82 | max-parallel: 3 83 | fail-fast: true 84 | matrix: 85 | operating-system: ["ubuntu-latest"] 86 | php-versions: ["8.0", "8.1", "8.2", "8.3", "8.4"] 87 | 88 | steps: 89 | - name: "Checkout" 90 | uses: actions/checkout@v4 91 | with: 92 | fetch-depth: 0 93 | 94 | - name: "Setup PHP" 95 | uses: shivammathur/setup-php@v2 96 | with: 97 | php-version: ${{ matrix.php-versions }} 98 | extensions: mbstring, intl 99 | ini-values: post_max_size=256M, max_execution_time=180 100 | coverage: xdebug 101 | 102 | - name: "Composer State" 103 | run: composer update --prefer-lowest --no-install --with-all-dependencies 104 | 105 | - name: "Composer Name Hash" 106 | id: composer-hash 107 | uses: KEINOS/gh-action-hash-for-cache@main 108 | with: 109 | path: ./composer.lock 110 | 111 | - name: "Caching" 112 | id: cache-composer 113 | uses: actions/cache@v4 114 | with: 115 | path: vendor 116 | key: composer-lowest-${{ steps.composer-hash.outputs.hash }} 117 | restore-keys: composer-lowest-${{ steps.composer-hash.outputs.hash }} 118 | 119 | - name: "Install Dependencies" 120 | if: ${{ steps.cache-composer.outputs.cache-hit != 'true' }} 121 | run: composer install 122 | 123 | - name: "Linux: Restore Vendor Executable" 124 | if: matrix.operating-system == 'ubuntu-latest' 125 | run: chmod -R 0755 vendor 126 | 127 | - name: "PHPUnit" 128 | run: ./vendor/bin/phpunit 129 | -------------------------------------------------------------------------------- /.github/workflows/clear-cache.yml: -------------------------------------------------------------------------------- 1 | name: Clear Caches 2 | on: 3 | schedule: 4 | - cron: "30 2 1,15 * *" 5 | workflow_dispatch: 6 | 7 | jobs: 8 | remove-caches: 9 | name: Delete all caches 10 | runs-on: ubuntu-20.04 11 | 12 | steps: 13 | - name: Clear caches 14 | uses: easimon/wipe-cache@main 15 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron | Monthly Health Check 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: 0 2 1 * * 7 | 8 | jobs: 9 | validate-master: 10 | if: github.ref == 'refs/heads/master' 11 | uses: ./.github/workflows/ci.yml 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | /vendor 3 | /composer.lock 4 | 5 | # PHPUnit 6 | /.phpunit* 7 | /phpunit.xml.bak 8 | /clover.xml -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Thank you for being interested in this package and wanting to contribute to it. 4 | 5 | ## Coding 6 | 7 | New code should be introduced to the codebase through issues that specify what you are trying to accomplish. 8 | I understand that sometimes you need to add/update/delete code otherwhere in the codebase to achieve this goal. 9 | This is why my test always merges master in to ensure that your code stays functional/executable during development. 10 | 11 | **Please adhere these pointers:** 12 | * Support selected PHP Versions | Ref: [Master/Composer.json](https://github.com/AlexWestergaard/php-ga4/blob/master/composer.json) 13 | * Pass current tests without modification; unless clearly explaining why the change is necessary/required | `> vendor/bin/phpunit` 14 | * PHPUnit tests should confidently ensure that code doesn't fail/error in unwated ways (eg. E_WARNINGS or missing paranthesis) 15 | * At least try to follow PSR<1, 4, 12> and \*PSR<5, 10> for documentation | Ref: [PHP FIG.](https://www.php-fig.org/psr/) 16 | * Commits should explain what is achived and not what changed / eg. ask yourself: "This commit will...(?)" 17 | * Tabs/Tabulations should appear as 4 spaces 18 | 19 | ## Roadmap 20 | 21 | Future direction will primarily be directed by milestones of issues. 22 | If no milestone is present, issues marked with `Bug` and `Enhancement` are considered goals. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexwestergaard/php-ga4", 3 | "description": "PHP Library for Google Analytics 4 with Server Side Tagging", 4 | "keywords": [ 5 | "php", 6 | "php8", 7 | "php80", 8 | "php81", 9 | "php82", 10 | "php83", 11 | "php84", 12 | "analytics", 13 | "analytics 4", 14 | "google analytics", 15 | "google analytics 4", 16 | "server side tracking", 17 | "tracking", 18 | "ga4", 19 | "sst", 20 | "gdpr" 21 | ], 22 | "homepage": "https://github.com/aawnu/php-ga4", 23 | "type": "library", 24 | "license": "Unlicense", 25 | "authors": [ 26 | { 27 | "name": "Alex Ahlgreen Westergaard", 28 | "homepage": "https://aaw.nu", 29 | "role": "Developer" 30 | } 31 | ], 32 | "support": { 33 | "issues": "https://github.com/aawnu/php-ga4/issues", 34 | "source": "https://github.com/aawnu/php-ga4/releases/latest" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "AlexWestergaard\\PhpGa4\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "AlexWestergaard\\PhpGa4Test\\": "test/" 44 | } 45 | }, 46 | "require": { 47 | "php": ">=8.0,<8.5", 48 | "guzzlehttp/guzzle": "^7.0" 49 | }, 50 | "require-dev": { 51 | "phpunit/phpunit": "^9.5|^10.0" 52 | }, 53 | "minimum-stability": "stable", 54 | "prefer-stable": true 55 | } 56 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | test/Unit 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | ./src/Facade 32 | ./src/GA4Exception.php 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Analytics.php: -------------------------------------------------------------------------------- 1 | guzzle = new Guzzle(); 34 | $this->consent = new Helper\ConsentHelper(); 35 | $this->userdata = new Helper\UserDataHelper(); 36 | } 37 | 38 | public function getParams(): array 39 | { 40 | return [ 41 | 'non_personalized_ads', 42 | 'timestamp_micros', 43 | 'client_id', 44 | 'user_id', 45 | 'user_properties', 46 | 'events', 47 | ]; 48 | } 49 | 50 | public function getRequiredParams(): array 51 | { 52 | $return = []; 53 | 54 | // Either client_id OR user_id MUST to be set 55 | if ( 56 | (!isset($this->client_id) || empty($this->client_id)) 57 | && (!isset($this->user_id) || empty($this->user_id)) 58 | ) { 59 | $return[] = 'client_id'; 60 | } 61 | 62 | return $return; 63 | } 64 | 65 | public function setClientId(string $id) 66 | { 67 | $this->client_id = $id; 68 | return $this; 69 | } 70 | 71 | public function setUserId(string $id) 72 | { 73 | $this->user_id = $id; 74 | return $this; 75 | } 76 | 77 | public function setTimestampMicros(int|float $microOrUnix) 78 | { 79 | $min = Helper\ConvertHelper::timeAsMicro(strtotime('-3 days') + 10); 80 | $max = Helper\ConvertHelper::timeAsMicro(time() + 3); 81 | 82 | $time = Helper\ConvertHelper::timeAsMicro($microOrUnix); 83 | 84 | if ($time < $min || $time > $max) { 85 | throw Ga4Exception::throwMicrotimeExpired(); 86 | } 87 | 88 | $this->timestamp_micros = $time; 89 | return $this; 90 | } 91 | 92 | public function addUserProperty(Facade\Type\UserPropertyType ...$props) 93 | { 94 | foreach ($props as $prop) { 95 | $this->user_properties = array_replace($this->user_properties, $prop->toArray()); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | public function addEvent(Facade\Type\EventType ...$events) 102 | { 103 | foreach ($events as $event) { 104 | $this->events[] = $event->toArray(); 105 | } 106 | 107 | return $this; 108 | } 109 | 110 | public function consent(): Helper\ConsentHelper 111 | { 112 | return $this->consent; 113 | } 114 | 115 | public function userdata(): Helper\UserDataHelper 116 | { 117 | return $this->userdata; 118 | } 119 | 120 | public function post(): void 121 | { 122 | if (empty($this->measurement_id)) { 123 | throw Ga4Exception::throwMissingMeasurementId(); 124 | } 125 | 126 | if (empty($this->api_secret)) { 127 | throw Ga4Exception::throwMissingApiSecret(); 128 | } 129 | 130 | $url = $this->debug ? Facade\Type\AnalyticsType::URL_DEBUG : Facade\Type\AnalyticsType::URL_LIVE; 131 | $url .= '?' . http_build_query(['measurement_id' => $this->measurement_id, 'api_secret' => $this->api_secret]); 132 | 133 | $body = array_replace_recursive( 134 | $this->toArray(), 135 | ["user_data" => !empty($this->user_id) ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too 136 | ["user_properties" => $this->user_properties], 137 | ["consent" => $this->consent->toArray()], 138 | ); 139 | 140 | if (count($body["user_data"]) < 1) unset($body["user_data"]); 141 | if (count($body["user_properties"]) < 1) unset($body["user_properties"]); 142 | 143 | $chunkEvents = array_chunk($this->events, 25); 144 | 145 | if (count($chunkEvents) < 1) { 146 | throw Ga4Exception::throwMissingEvents(); 147 | } 148 | 149 | $this->userdata->reset(); 150 | $this->user_properties = []; 151 | $this->events = []; 152 | 153 | foreach ($chunkEvents as $events) { 154 | $body['events'] = $events; 155 | 156 | $kB = 1024; 157 | if (($size = mb_strlen(json_encode($body))) > ($kB * 130)) { 158 | Ga4Exception::throwRequestTooLarge(intval($size / $kB)); 159 | continue; 160 | } 161 | 162 | $jsonBody = json_encode($body); 163 | $jsonBody = strtr($jsonBody, [':[]' => ':{}']); 164 | 165 | $res = $this->guzzle->request('POST', $url, [ 166 | 'headers' => [ 167 | 'content-type' => 'application/json;charset=utf-8' 168 | ], 169 | 'body' => $jsonBody, 170 | ]); 171 | 172 | if (!in_array(($code = $res?->getStatusCode() ?? 0), Facade\Type\AnalyticsType::ACCEPT_RESPONSE_HEADERS)) { 173 | Ga4Exception::throwRequestWrongResponceCode($code); 174 | } 175 | 176 | if ($code !== 204) { 177 | $callback = @json_decode($res->getBody()->getContents(), true); 178 | 179 | if (json_last_error() != JSON_ERROR_NONE) { 180 | Ga4Exception::throwRequestInvalidResponse(); 181 | } elseif (empty($callback)) { 182 | Ga4Exception::throwRequestEmptyResponse(); 183 | } elseif (!empty($callback['validationMessages'])) { 184 | foreach ($callback['validationMessages'] as $msg) { 185 | Ga4Exception::throwRequestInvalidBody($msg); 186 | } 187 | } 188 | } 189 | } 190 | 191 | if (Ga4Exception::hasThrowStack()) { 192 | throw Ga4Exception::getThrowStack(); 193 | } 194 | } 195 | 196 | public static function new(string $measurement_id, string $api_secret, bool $debug = false): static 197 | { 198 | return new static($measurement_id, $api_secret, $debug); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Event/AddPaymentInfo.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 38 | || !isset($this->currency) && isset($this->value) 39 | ) { 40 | $return = [ 41 | 'currency', 42 | 'value' 43 | ]; 44 | } 45 | 46 | $return[] = 'items'; 47 | return $return; 48 | } 49 | 50 | public function setCurrency(null|string $iso) 51 | { 52 | $this->currency = $iso; 53 | return $this; 54 | } 55 | 56 | public function setValue(null|int|float $val) 57 | { 58 | $this->value = $val; 59 | return $this; 60 | } 61 | 62 | public function setCoupon(null|string $code) 63 | { 64 | $this->coupon = $code; 65 | return $this; 66 | } 67 | 68 | public function setPaymentType(null|string $type) 69 | { 70 | $this->payment_type = $type; 71 | return $this; 72 | } 73 | 74 | public function addItem(Facade\Type\ItemType $item) 75 | { 76 | $this->items[] = $item->toArray(); 77 | return $this; 78 | } 79 | 80 | public function resetItems() 81 | { 82 | $this->items = []; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Event/AddShippingInfo.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 38 | || !isset($this->currency) && isset($this->value) 39 | ) { 40 | $return = [ 41 | 'currency', 42 | 'value' 43 | ]; 44 | } 45 | 46 | $return[] = 'items'; 47 | return $return; 48 | } 49 | 50 | public function setCurrency(null|string $iso) 51 | { 52 | $this->currency = $iso; 53 | return $this; 54 | } 55 | 56 | public function setValue(null|int|float $val) 57 | { 58 | $this->value = $val; 59 | return $this; 60 | } 61 | 62 | public function setCoupon(null|string $code) 63 | { 64 | $this->coupon = $code; 65 | return $this; 66 | } 67 | 68 | public function setShippingTier(null|string $tier) 69 | { 70 | $this->shipping_tier = $tier; 71 | return $this; 72 | } 73 | 74 | public function addItem(Facade\Type\ItemType $item) 75 | { 76 | $this->items[] = $item->toArray(); 77 | return $this; 78 | } 79 | 80 | public function resetItems() 81 | { 82 | $this->items = []; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Event/AddToCart.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 34 | || !isset($this->currency) && isset($this->value) 35 | ) { 36 | $return = [ 37 | 'currency', 38 | 'value' 39 | ]; 40 | } 41 | 42 | $return[] = 'items'; 43 | return $return; 44 | } 45 | 46 | public function setCurrency(null|string $iso) 47 | { 48 | $this->currency = $iso; 49 | return $this; 50 | } 51 | 52 | public function setValue(null|int|float $val) 53 | { 54 | $this->value = $val; 55 | return $this; 56 | } 57 | 58 | public function addItem(Facade\Type\ItemType $item) 59 | { 60 | $this->items[] = $item->toArray(); 61 | return $this; 62 | } 63 | 64 | public function resetItems() 65 | { 66 | $this->items = []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Event/AddToWishlist.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 34 | || !isset($this->currency) && isset($this->value) 35 | ) { 36 | $return = [ 37 | 'currency', 38 | 'value' 39 | ]; 40 | } 41 | 42 | $return[] = 'items'; 43 | return $return; 44 | } 45 | 46 | public function setCurrency(null|string $iso) 47 | { 48 | $this->currency = $iso; 49 | return $this; 50 | } 51 | 52 | public function setValue(null|int|float $val) 53 | { 54 | $this->value = $val; 55 | return $this; 56 | } 57 | 58 | public function addItem(Facade\Type\ItemType $item) 59 | { 60 | $this->items[] = $item->toArray(); 61 | return $this; 62 | } 63 | 64 | public function resetItems() 65 | { 66 | $this->items = []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Event/BeginCheckout.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 36 | || !isset($this->currency) && isset($this->value) 37 | ) { 38 | $return = [ 39 | 'currency', 40 | 'value' 41 | ]; 42 | } 43 | 44 | $return[] = 'items'; 45 | return $return; 46 | } 47 | 48 | public function setCurrency(null|string $iso) 49 | { 50 | $this->currency = $iso; 51 | return $this; 52 | } 53 | 54 | public function setValue(null|int|float $val) 55 | { 56 | $this->value = $val; 57 | return $this; 58 | } 59 | 60 | public function setCoupon(null|string $code) 61 | { 62 | $this->coupon = $code; 63 | return $this; 64 | } 65 | 66 | public function addItem(Facade\Type\ItemType $item) 67 | { 68 | $this->items[] = $item->toArray(); 69 | return $this; 70 | } 71 | 72 | public function resetItems() 73 | { 74 | $this->items = []; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Event/EarnVirtualCurrency.php: -------------------------------------------------------------------------------- 1 | virtual_currency_name = $name; 34 | return $this; 35 | } 36 | 37 | public function setValue(null|int|float $num) 38 | { 39 | $this->value = $num; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Event/Exception.php: -------------------------------------------------------------------------------- 1 | description = $description; 35 | return $this; 36 | } 37 | 38 | public function setFatal(null|bool $isFatal) 39 | { 40 | $this->fatal = $isFatal; 41 | return $this; 42 | } 43 | 44 | public function parseException(\Exception $exception, $isFatal = false) 45 | { 46 | if ($exception instanceof Ga4ExceptionType) { 47 | return $this; 48 | } 49 | 50 | $this->setDescription($exception->getMessage()); 51 | $this->setFatal($isFatal); 52 | 53 | return $this; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Event/GenerateLead.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 32 | || !isset($this->currency) && isset($this->value) 33 | ) { 34 | $return = [ 35 | 'currency', 36 | 'value' 37 | ]; 38 | } 39 | 40 | return $return; 41 | } 42 | 43 | public function setCurrency(null|string $iso) 44 | { 45 | $this->currency = $iso; 46 | return $this; 47 | } 48 | 49 | public function setValue(null|int|float $val) 50 | { 51 | $this->value = $val; 52 | return $this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Event/JoinGroup.php: -------------------------------------------------------------------------------- 1 | group_id = $id; 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Event/LevelUp.php: -------------------------------------------------------------------------------- 1 | level = $lvl; 34 | return $this; 35 | } 36 | 37 | public function setCharacter(null|string $char) 38 | { 39 | $this->character = $char; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Event/Login.php: -------------------------------------------------------------------------------- 1 | method = $method; 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Event/PageView.php: -------------------------------------------------------------------------------- 1 | page_title = $title; 34 | return $this; 35 | } 36 | 37 | public function setPageLocation(null|string $url) 38 | { 39 | if ($url === null) { 40 | $this->page_location = null; 41 | } else { 42 | $model = parse_url($url); 43 | 44 | if (is_array($model) && !empty($model["scheme"]) && !empty($model["host"])) { 45 | $this->page_location = implode("", [ 46 | $model["scheme"] . "://" . $model["host"], 47 | "/" . ltrim($model["path"], "/"), 48 | !empty($model["query"]) ? "?" . $model["query"] : "", 49 | !empty($model["fragment"]) ? "#" . $model["fragment"] : "", 50 | ]); 51 | } 52 | } 53 | 54 | return $this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Event/PostScore.php: -------------------------------------------------------------------------------- 1 | score = $score; 36 | return $this; 37 | } 38 | 39 | public function setLevel(null|int $lvl) 40 | { 41 | $this->level = $lvl; 42 | return $this; 43 | } 44 | 45 | public function setCharacter(null|string $char) 46 | { 47 | $this->character = $char; 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Event/Purchase.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 44 | || !isset($this->currency) && isset($this->value) 45 | ) { 46 | $return = [ 47 | 'currency', 48 | 'value' 49 | ]; 50 | } 51 | 52 | $return[] = 'transaction_id'; 53 | $return[] = 'items'; 54 | return $return; 55 | } 56 | 57 | public function setCurrency(null|string $iso) 58 | { 59 | $this->currency = $iso; 60 | return $this; 61 | } 62 | 63 | public function setTransactionId(null|string $id) 64 | { 65 | $this->transaction_id = $id; 66 | return $this; 67 | } 68 | 69 | public function setValue(null|int|float $val) 70 | { 71 | $this->value = $val; 72 | return $this; 73 | } 74 | 75 | public function setAffiliation(null|string $affiliation) 76 | { 77 | $this->affiliation = $affiliation; 78 | return $this; 79 | } 80 | 81 | public function setCoupon(null|string $code) 82 | { 83 | $this->coupon = $code; 84 | return $this; 85 | } 86 | 87 | public function setShipping(null|int|float $cost) 88 | { 89 | $this->shipping = $cost; 90 | return $this; 91 | } 92 | 93 | public function setTax(null|int|float $tax) 94 | { 95 | $this->tax = $tax; 96 | return $this; 97 | } 98 | 99 | public function addItem(Facade\Type\ItemType $item) 100 | { 101 | $this->items[] = $item->toArray(); 102 | return $this; 103 | } 104 | 105 | public function resetItems() 106 | { 107 | $this->items = []; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Event/Refund.php: -------------------------------------------------------------------------------- 1 | isFullRefund = $is; 28 | return $this; 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return 'refund'; 34 | } 35 | 36 | public function getParams(): array 37 | { 38 | return [ 39 | 'currency', 40 | 'transaction_id', 41 | 'value', 42 | 'affiliation', 43 | 'coupon', 44 | 'shipping', 45 | 'tax', 46 | 'items', 47 | ]; 48 | } 49 | 50 | public function getRequiredParams(): array 51 | { 52 | $return = []; 53 | 54 | if ( 55 | isset($this->currency) && !isset($this->value) 56 | || !isset($this->currency) && isset($this->value) 57 | ) { 58 | $return = [ 59 | 'currency', 60 | 'value' 61 | ]; 62 | } 63 | 64 | $return[] = 'transaction_id'; 65 | 66 | if (!$this->isFullRefund) { 67 | $return[] = 'items'; 68 | } 69 | 70 | return $return; 71 | } 72 | 73 | public function setCurrency(null|string $iso) 74 | { 75 | $this->currency = $iso; 76 | return $this; 77 | } 78 | 79 | public function setTransactionId(null|string $id) 80 | { 81 | $this->transaction_id = $id; 82 | return $this; 83 | } 84 | 85 | public function setValue(null|int|float $val) 86 | { 87 | $this->value = $val; 88 | return $this; 89 | } 90 | 91 | public function setAffiliation(null|string $affiliation) 92 | { 93 | $this->affiliation = $affiliation; 94 | return $this; 95 | } 96 | 97 | public function setCoupon(null|string $code) 98 | { 99 | $this->coupon = $code; 100 | return $this; 101 | } 102 | 103 | public function setShipping(null|int|float $cost) 104 | { 105 | $this->shipping = $cost; 106 | return $this; 107 | } 108 | 109 | public function setTax(null|int|float $tax) 110 | { 111 | $this->tax = $tax; 112 | return $this; 113 | } 114 | 115 | public function addItem(Facade\Type\ItemType $item) 116 | { 117 | $this->items[] = $item->toArray(); 118 | return $this; 119 | } 120 | 121 | public function resetItems() 122 | { 123 | $this->items = []; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Event/RemoveFromCart.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 34 | || !isset($this->currency) && isset($this->value) 35 | ) { 36 | $return = [ 37 | 'currency', 38 | 'value' 39 | ]; 40 | } 41 | 42 | $return[] = 'items'; 43 | return $return; 44 | } 45 | 46 | public function setCurrency(null|string $iso) 47 | { 48 | $this->currency = $iso; 49 | return $this; 50 | } 51 | 52 | public function setValue(null|int|float $val) 53 | { 54 | $this->value = $val; 55 | return $this; 56 | } 57 | 58 | public function addItem(Facade\Type\ItemType $item) 59 | { 60 | $this->items[] = $item->toArray(); 61 | return $this; 62 | } 63 | 64 | public function resetItems() 65 | { 66 | $this->items = []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Event/Search.php: -------------------------------------------------------------------------------- 1 | search_term = $term; 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Event/SelectContent.php: -------------------------------------------------------------------------------- 1 | content_type = $type; 34 | return $this; 35 | } 36 | 37 | public function setItemId(null|string $id) 38 | { 39 | $this->item_id = $id; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Event/SelectItem.php: -------------------------------------------------------------------------------- 1 | item_list_id = $id; 37 | return $this; 38 | } 39 | 40 | public function setItemListName(null|string $name) 41 | { 42 | $this->item_list_name = $name; 43 | return $this; 44 | } 45 | 46 | public function setItem(Item $item) 47 | { 48 | $this->items = [$item->toArray()]; 49 | return $this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Event/SelectPromotion.php: -------------------------------------------------------------------------------- 1 | creative_name = $name; 42 | return $this; 43 | } 44 | 45 | public function setCreativeSlot(null|string $slot) 46 | { 47 | $this->creative_slot = $slot; 48 | return $this; 49 | } 50 | 51 | public function setLocationId(null|string $id) 52 | { 53 | $this->location_id = $id; 54 | return $this; 55 | } 56 | 57 | public function setPromotionId(null|string $id) 58 | { 59 | $this->promotion_id = $id; 60 | return $this; 61 | } 62 | 63 | public function setPromotionName(null|string $name) 64 | { 65 | $this->promotion_name = $name; 66 | return $this; 67 | } 68 | 69 | public function addItem(Facade\Type\ItemType $item) 70 | { 71 | $this->items[] = $item->toArray(); 72 | return $this; 73 | } 74 | 75 | public function resetItems() 76 | { 77 | $this->items = []; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Event/Share.php: -------------------------------------------------------------------------------- 1 | method = $method; 36 | return $this; 37 | } 38 | 39 | public function setContentType(null|string $type) 40 | { 41 | $this->content_type = $type; 42 | return $this; 43 | } 44 | 45 | public function setItemId(null|string $id) 46 | { 47 | $this->item_id = $id; 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Event/Signup.php: -------------------------------------------------------------------------------- 1 | method = $method; 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Event/SpendVirtualCurrency.php: -------------------------------------------------------------------------------- 1 | virtual_currency_name = $name; 39 | return $this; 40 | } 41 | 42 | public function setValue(null|int|float $num) 43 | { 44 | $this->value = $num; 45 | return $this; 46 | } 47 | 48 | public function setItemName(null|string $name) 49 | { 50 | $this->item_name = $name; 51 | return $this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Event/TutorialBegin.php: -------------------------------------------------------------------------------- 1 | achievement_id = $id; 34 | return $this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Event/ViewCart.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 34 | || !isset($this->currency) && isset($this->value) 35 | ) { 36 | $return = [ 37 | 'currency', 38 | 'value' 39 | ]; 40 | } 41 | 42 | $return[] = 'items'; 43 | return $return; 44 | } 45 | 46 | public function setCurrency(null|string $iso) 47 | { 48 | $this->currency = $iso; 49 | return $this; 50 | } 51 | 52 | public function setValue(null|int|float $val) 53 | { 54 | $this->value = $val; 55 | return $this; 56 | } 57 | 58 | public function addItem(Facade\Type\ItemType $item) 59 | { 60 | $this->items[] = $item->toArray(); 61 | return $this; 62 | } 63 | 64 | public function resetItems() 65 | { 66 | $this->items = []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Event/ViewItem.php: -------------------------------------------------------------------------------- 1 | currency) && !isset($this->value) 34 | || !isset($this->currency) && isset($this->value) 35 | ) { 36 | $return = [ 37 | 'currency', 38 | 'value' 39 | ]; 40 | } 41 | 42 | $return[] = 'items'; 43 | return $return; 44 | } 45 | 46 | public function setCurrency(null|string $iso) 47 | { 48 | $this->currency = $iso; 49 | return $this; 50 | } 51 | 52 | public function setValue(null|int|float $val) 53 | { 54 | $this->value = $val; 55 | return $this; 56 | } 57 | 58 | public function addItem(Facade\Type\ItemType $item) 59 | { 60 | $this->items[] = $item->toArray(); 61 | return $this; 62 | } 63 | 64 | public function resetItems() 65 | { 66 | $this->items = []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Event/ViewItemList.php: -------------------------------------------------------------------------------- 1 | item_list_id = $id; 36 | return $this; 37 | } 38 | 39 | public function setItemListName(null|string $name) 40 | { 41 | $this->item_list_name = $name; 42 | return $this; 43 | } 44 | 45 | public function addItem(Facade\Type\ItemType $item) 46 | { 47 | $this->items[] = $item->toArray(); 48 | return $this; 49 | } 50 | 51 | public function resetItems() 52 | { 53 | $this->items = []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Event/ViewPromotion.php: -------------------------------------------------------------------------------- 1 | creative_name = $name; 42 | return $this; 43 | } 44 | 45 | public function setCreativeSlot(null|string $slot) 46 | { 47 | $this->creative_slot = $slot; 48 | return $this; 49 | } 50 | 51 | public function setLocationId(null|string $id) 52 | { 53 | $this->location_id = $id; 54 | return $this; 55 | } 56 | 57 | public function setPromotionId(null|string $id) 58 | { 59 | $this->promotion_id = $id; 60 | return $this; 61 | } 62 | 63 | public function setPromotionName(null|string $name) 64 | { 65 | $this->promotion_name = $name; 66 | return $this; 67 | } 68 | 69 | public function addItem(Facade\Type\ItemType $item) 70 | { 71 | $this->items[] = $item->toArray(); 72 | return $this; 73 | } 74 | 75 | public function resetItems() 76 | { 77 | $this->items = []; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Event/ViewSearchResults.php: -------------------------------------------------------------------------------- 1 | search_term = $term; 36 | return $this; 37 | } 38 | 39 | public function addItem(Facade\Type\ItemType $item) 40 | { 41 | $this->items[] = $item->toArray(); 42 | return $this; 43 | } 44 | 45 | public function resetItems() 46 | { 47 | $this->items = []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/Ga4EventException.php: -------------------------------------------------------------------------------- 1 | ' . $msg['validationCode'] 89 | . (isset($msg['fieldPath']) ? ' [' . $msg['fieldPath'] . ']: ' : ': ') 90 | . $msg['description'], 91 | static::REQUEST_INVALID_BODY 92 | ); 93 | } 94 | 95 | public static function throwMissingEvents() 96 | { 97 | return new static("Request must include at least 1 event with a name", static::REQUEST_EMPTY_EVENTLIST); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Exception/Ga4IOException.php: -------------------------------------------------------------------------------- 1 | Message() 14 | */ 15 | public function setDescription(null|string $description); 16 | 17 | /** 18 | * Report if the exception is fatal 19 | * 20 | * @var fatal 21 | * @param bool $isFatal 22 | */ 23 | public function setFatal(null|bool $isFatal); 24 | 25 | /** 26 | * Attempt to parse the message from the Exception and error own known GA4 Exceptions 27 | * 28 | * @param \Exception $exception 29 | */ 30 | public function parseException(Exception $exception); 31 | } 32 | -------------------------------------------------------------------------------- /src/Facade/Group/ExportFacade.php: -------------------------------------------------------------------------------- 1 | */ 8 | public const RESERVED_NAMES = [ 9 | 'ad_activeview', 10 | 'ad_click', 11 | 'ad_exposure', 12 | 'ad_impression', 13 | 'ad_query', 14 | 'adunit_exposure', 15 | 'app_clear_data', 16 | 'app_install', 17 | 'app_update', 18 | 'app_remove', 19 | 'error', 20 | 'first_open', 21 | 'first_visit', 22 | 'in_app_purchase', 23 | 'notification_dismiss', 24 | 'notification_foreground', 25 | 'notification_open', 26 | 'notification_receive', 27 | 'os_update', 28 | 'screen_view', 29 | 'session_start', 30 | 'user_engagement', 31 | ]; 32 | 33 | /** 34 | * Return NAME of Event 35 | * 36 | * @return string snake_case 37 | */ 38 | public function getName(): string; 39 | } 40 | -------------------------------------------------------------------------------- /src/Facade/Type/FirebaseType.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getParams(): array; 18 | 19 | /** 20 | * Receive REQUIRED parameters of Event 21 | * 22 | * @return array 23 | */ 24 | public function getRequiredParams(): array; 25 | 26 | /** 27 | * Receive ALL parameters of Event 28 | * 29 | * @return array 30 | */ 31 | public function getAllParams(): array; 32 | 33 | /** 34 | * Return usable parameters as Array structure 35 | * 36 | * @return array 37 | */ 38 | public function toArray(): array; 39 | 40 | /** 41 | * Attempt to fill parameters of model and return as new instance 42 | * 43 | * @param AlexWestergaard\PhpGa4\Facade\Type\IO|array $importable 44 | * @return static 45 | */ 46 | public static function fromArray(IOType|array $importable): static; 47 | } 48 | -------------------------------------------------------------------------------- /src/Facade/Type/ItemType.php: -------------------------------------------------------------------------------- 1 | */ 8 | public const RESERVED_NAMES = [ 9 | 'first_open_time', 10 | 'first_visit_time', 11 | 'last_deep_link_referrer', 12 | 'user_id', 13 | 'first_open_after_install', 14 | ]; 15 | 16 | /** 17 | * Set name of User Property 18 | * 19 | * @param string $name 20 | * 21 | * @return static 22 | */ 23 | public function setName(string $name): static; 24 | 25 | /** 26 | * Set value of User Property 27 | * 28 | * @param int|float|string $value 29 | * 30 | * @return static 31 | */ 32 | public function setValue(int|float|string $value): static; 33 | } 34 | -------------------------------------------------------------------------------- /src/Firebase.php: -------------------------------------------------------------------------------- 1 | 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 | ["app_instance_id" => $this->app_instance_id], 127 | ["user_data" => !empty($this->user_id) ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too 128 | ["user_properties" => $this->user_properties], 129 | ["consent" => $this->consent->toArray()], 130 | ); 131 | 132 | if (count($body["user_data"]) < 1) unset($body["user_data"]); 133 | if (count($body["user_properties"]) < 1) unset($body["user_properties"]); 134 | 135 | $chunkEvents = array_chunk($this->events, 25); 136 | 137 | if (count($chunkEvents) < 1) { 138 | throw Ga4Exception::throwMissingEvents(); 139 | } 140 | 141 | $this->userdata->reset(); 142 | $this->user_properties = []; 143 | $this->events = []; 144 | 145 | foreach ($chunkEvents as $events) { 146 | $body['events'] = $events; 147 | 148 | $kB = 1024; 149 | if (($size = mb_strlen(json_encode($body))) > ($kB * 130)) { 150 | Ga4Exception::throwRequestTooLarge(intval($size / $kB)); 151 | continue; 152 | } 153 | 154 | $jsonBody = json_encode($body); 155 | $jsonBody = strtr($jsonBody, [':[]' => ':{}']); 156 | 157 | $res = $this->guzzle->request('POST', $url, [ 158 | 'headers' => [ 159 | 'content-type' => 'application/json;charset=utf-8' 160 | ], 161 | 'body' => $jsonBody, 162 | ]); 163 | 164 | if (!in_array(($code = $res?->getStatusCode() ?? 0), Facade\Type\AnalyticsType::ACCEPT_RESPONSE_HEADERS)) { 165 | Ga4Exception::throwRequestWrongResponceCode($code); 166 | } 167 | 168 | if ($code !== 204) { 169 | $callback = @json_decode($res->getBody()->getContents(), true); 170 | 171 | if (json_last_error() != JSON_ERROR_NONE) { 172 | Ga4Exception::throwRequestInvalidResponse(); 173 | } elseif (empty($callback)) { 174 | Ga4Exception::throwRequestEmptyResponse(); 175 | } elseif (!empty($callback['validationMessages'])) { 176 | foreach ($callback['validationMessages'] as $msg) { 177 | Ga4Exception::throwRequestInvalidBody($msg); 178 | } 179 | } 180 | } 181 | } 182 | 183 | if (Ga4Exception::hasThrowStack()) { 184 | throw Ga4Exception::getThrowStack(); 185 | } 186 | } 187 | 188 | public static function new(string $firebase_app_id, string $api_secret, bool $debug = false): static 189 | { 190 | return new static($firebase_app_id, $api_secret, $debug); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Helper/ConsentHelper.php: -------------------------------------------------------------------------------- 1 | ad_user_data = null; 16 | return $this; 17 | } 18 | 19 | public function setAdUserDataPermission(bool $allow = false): self 20 | { 21 | $this->ad_user_data = $allow ? self::GRANTED : self::DENIED; 22 | return $this; 23 | } 24 | 25 | public function getAdUserDataPermission(): null|string 26 | { 27 | return $this->ad_user_data == null ? null : $this->ad_user_data; 28 | } 29 | 30 | public function clearAdPersonalizationPermission(): self 31 | { 32 | $this->ad_personalization = null; 33 | return $this; 34 | } 35 | 36 | public function setAdPersonalizationPermission(bool $allow = false): self 37 | { 38 | $this->ad_personalization = $allow ? self::GRANTED : self::DENIED; 39 | return $this; 40 | } 41 | 42 | public function getAdPersonalizationPermission(): null|string 43 | { 44 | return $this->ad_personalization == null ? null : $this->ad_personalization; 45 | } 46 | 47 | public function toArray(): array 48 | { 49 | $e = []; 50 | 51 | if ($this->ad_user_data != null) { 52 | $e["ad_user_data"] = $this->getAdUserDataPermission(); 53 | } 54 | 55 | if ($this->ad_personalization != null) { 56 | $e["ad_personalization"] = $this->getAdPersonalizationPermission(); 57 | } 58 | 59 | return $e; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Helper/ConvertHelper.php: -------------------------------------------------------------------------------- 1 | $current) { 26 | throw Ga4Exception::throwMicrotimeInvalid($unixOrMicro); 27 | } 28 | 29 | return intval($secondAsMicro * $unixOrMicro); 30 | } 31 | 32 | /** 33 | * @param string $input 34 | * @return string snake_case 35 | */ 36 | public static function snake(string $input): string 37 | { 38 | return strtolower(preg_replace('/(?>>> $list [ ['eventname' => [ paramname => value ] ] ] 54 | * 55 | * @return array 56 | */ 57 | public static function parseEvents(array $list): array 58 | { 59 | $events = []; 60 | 61 | $eventPrefix = "AlexWestergaard\\PhpGa4\\Event\\"; 62 | 63 | foreach ($list as $packet) { 64 | foreach ($packet as $name => $params) { 65 | $event = $eventPrefix . $name; 66 | if (!class_exists($event)) { 67 | continue; 68 | } 69 | 70 | $event = $event::fromArray($params); 71 | if (!($event instanceof EventType)) { 72 | continue; 73 | } 74 | 75 | $events[] = $event; 76 | } 77 | } 78 | return $events; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Helper/CountryIsoHelper.php: -------------------------------------------------------------------------------- 1 | language = $lang; 20 | return $this; 21 | } 22 | 23 | public function setPageLocation(string $url) 24 | { 25 | $this->page_location = $url; 26 | return $this; 27 | } 28 | 29 | public function setPageReferrer(string $url) 30 | { 31 | $this->page_referrer = $url; 32 | return $this; 33 | } 34 | 35 | public function setPageTitle(string $title) 36 | { 37 | $this->page_title = $title; 38 | return $this; 39 | } 40 | 41 | public function setScreenResolution(string $wxh) 42 | { 43 | $this->screen_resolution = $wxh; 44 | return $this; 45 | } 46 | 47 | public function setEventPage(DefaultEventParamsType $page) 48 | { 49 | $args = $page->toArray(); 50 | 51 | $this->language = $args['language'] ?? null; 52 | $this->page_location = $args['page_location'] ?? null; 53 | $this->page_referrer = $args['page_referrer'] ?? null; 54 | $this->page_title = $args['page_title'] ?? null; 55 | $this->screen_resolution = $args['screen_resolution'] ?? null; 56 | 57 | return $this; 58 | } 59 | 60 | public function toArray(): array 61 | { 62 | $return = parent::toArray(); 63 | 64 | if (!empty($this->campaign)) { 65 | $return['params'] = array_replace( 66 | $return['params'], 67 | $this->campaign 68 | ); 69 | } 70 | 71 | return $return; 72 | } 73 | 74 | public function getAllParams(): array 75 | { 76 | return array_unique(array_merge( 77 | parent::getAllParams(), 78 | [ 79 | 'language', 80 | 'page_location', 81 | 'page_referrer', 82 | 'page_title', 83 | 'screen_resolution', 84 | ], 85 | $this->getParams(), 86 | $this->getRequiredParams(), 87 | )); 88 | } 89 | 90 | public static function new(): static 91 | { 92 | return new static(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Helper/EventMainHelper.php: -------------------------------------------------------------------------------- 1 | session_id = $id; 17 | return $this; 18 | } 19 | 20 | public function setEngagementTimeMSec(int $msec) 21 | { 22 | $this->engagement_time_msec = $msec; 23 | return $this; 24 | } 25 | 26 | public function toArray(): array 27 | { 28 | $return = []; 29 | 30 | if (!method_exists($this, 'getName')) { 31 | throw Ga4EventException::throwNameMissing(); 32 | } else { 33 | $name = $this->getName(); 34 | 35 | if (empty($name)) { 36 | throw Ga4EventException::throwNameMissing(); 37 | } elseif (strlen($name) > 40) { 38 | throw Ga4EventException::throwNameTooLong(); 39 | } elseif (preg_match('/[^\w\d\-]|^\-|\-$/', $name)) { 40 | throw Ga4EventException::throwNameInvalid(); 41 | } elseif (in_array($name, EventType::RESERVED_NAMES) && !($this instanceof GtmEventType)) { 42 | throw Ga4EventException::throwNameReserved($name); 43 | } else { 44 | $return['name'] = $name; 45 | } 46 | } 47 | 48 | $return['params'] = parent::toArray(); 49 | 50 | return $return; 51 | } 52 | 53 | public function getAllParams(): array 54 | { 55 | return array_unique(array_merge( 56 | $this->getParams(), 57 | $this->getRequiredParams() 58 | )); 59 | } 60 | 61 | public static function new(): static 62 | { 63 | return new static(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Helper/EventParamsHelper.php: -------------------------------------------------------------------------------- 1 | language = $lang; 20 | return $this; 21 | } 22 | 23 | public function setPageLocation(string $url) 24 | { 25 | $this->page_location = $url; 26 | return $this; 27 | } 28 | 29 | public function setPageReferrer(string $url) 30 | { 31 | $this->page_referrer = $url; 32 | return $this; 33 | } 34 | 35 | public function setPageTitle(string $title) 36 | { 37 | $this->page_title = $title; 38 | return $this; 39 | } 40 | 41 | public function setScreenResolution(string $wxh) 42 | { 43 | $this->screen_resolution = $wxh; 44 | return $this; 45 | } 46 | 47 | public function toArray(): array 48 | { 49 | return [ 50 | 'language' => $this->language, 51 | 'page_location' => $this->page_location, 52 | 'page_referrer' => $this->page_referrer, 53 | 'page_title' => $this->page_title, 54 | 'screen_resolution' => $this->screen_resolution, 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Helper/IOHelper.php: -------------------------------------------------------------------------------- 1 | allIteratorKeys = $this->getAllParams(); 44 | } 45 | 46 | /** 47 | * PSEUDO Check offset key with Magic Isset 48 | * 49 | * @param mixed $name 50 | * 51 | * @return bool 52 | */ 53 | public function __isset($name) 54 | { 55 | return $this->offsetExists($name); 56 | } 57 | 58 | /** 59 | * PSEUDO Remove offset key with Magic Unset 60 | * 61 | * @param mixed $name 62 | * 63 | * @return bool 64 | */ 65 | public function __unset($name) 66 | { 67 | return $this->offsetUnset($name); 68 | } 69 | 70 | /** 71 | * PSEUDO Retrieve offset key with magic Getter 72 | * 73 | * @param mixed $name 74 | * 75 | * @return mixed 76 | */ 77 | public function __get($name) 78 | { 79 | return $this->offsetGet($name); 80 | } 81 | 82 | /** 83 | * PSEUDO Fill offset key with magic Setter 84 | * 85 | * @param mixed $name 86 | * @param mixed $value 87 | * 88 | * @return void 89 | */ 90 | public function __set($name, $value) 91 | { 92 | $this->offsetSet($name, $value); 93 | } 94 | 95 | /** 96 | * Check if parameter exists and is gettable/settable 97 | * 98 | * @param mixed $offset 99 | * 100 | * @return bool 101 | */ 102 | public function offsetExists(mixed $offset): bool 103 | { 104 | $offset = is_string($offset) ? ConvertHelper::snake($offset) : $offset; 105 | return in_array($offset, $this->getAllParams()) && property_exists($this, $offset); 106 | } 107 | 108 | /** 109 | * Retrieves the parameter if it exists or null 110 | * 111 | * @param mixed $offset 112 | * 113 | * @return mixed 114 | */ 115 | public function offsetGet(mixed $offset): mixed 116 | { 117 | $offset = is_string($offset) ? ConvertHelper::snake($offset) : $offset; 118 | 119 | return $this->offsetExists($offset) && isset($this->$offset) ? $this->$offset : null; 120 | } 121 | 122 | /** 123 | * Attempts to fill parameter with given value 124 | * 125 | * @param mixed $offset 126 | * @param mixed $value 127 | * 128 | * @return void 129 | */ 130 | public function offsetSet(mixed $offset, mixed $value): void 131 | { 132 | $offset = is_string($offset) ? ConvertHelper::snake($offset) : $offset; 133 | 134 | if (!$this->offsetExists($offset)) return; 135 | 136 | $set = ConvertHelper::camel('set_' . $offset); 137 | $add = ConvertHelper::camel('add_' . $offset); 138 | $setSingle = ConvertHelper::camel('set_' . (substr($offset, -1) == 's' ? substr($offset, 0, -1) : $offset)); 139 | $addSingle = ConvertHelper::camel('add_' . (substr($offset, -1) == 's' ? substr($offset, 0, -1) : $offset)); 140 | 141 | if (method_exists($this, $set)) { 142 | $this->$set($value); 143 | } elseif (method_exists($this, $setSingle)) { 144 | $this->$setSingle($value); 145 | } elseif (method_exists($this, $add)) { 146 | $this->$add($value); 147 | } elseif (method_exists($this, $addSingle)) { 148 | $this->$addSingle($value); 149 | } 150 | } 151 | 152 | /** 153 | * Attempt to unset parameter with given value 154 | * 155 | * @param mixed $offset 156 | * 157 | * @return void 158 | */ 159 | public function offsetUnset(mixed $offset): void 160 | { 161 | $offset = is_string($offset) ? ConvertHelper::snake($offset) : $offset; 162 | 163 | if (!$this->offsetExists($offset)) return; 164 | 165 | if (gettype($this->$offset) == 'array') { 166 | $this->$offset = []; 167 | } else { 168 | $this->$offset = null; 169 | } 170 | } 171 | 172 | /** 173 | * Attempt to return current key of Iterator key 174 | * 175 | * @return mixed 176 | */ 177 | public function current(): mixed 178 | { 179 | return $this->offsetGet($this->key()); 180 | } 181 | 182 | /** 183 | * Attempt to return current key of Iterator key 184 | * 185 | * @return mixed 186 | */ 187 | public function key(): mixed 188 | { 189 | return $this->allIteratorKeys[$this->currentIteratorKey] ?? null; 190 | } 191 | 192 | /** 193 | * Moved iterator key 1 up 194 | * 195 | * @return void 196 | */ 197 | public function next(): void 198 | { 199 | $this->currentIteratorKey++; 200 | } 201 | 202 | /** 203 | * Return the irator key to first element of array 204 | * 205 | * @return void 206 | */ 207 | public function rewind(): void 208 | { 209 | $this->currentIteratorKey = 0; 210 | } 211 | 212 | /** 213 | * Return the irator key to first element of array 214 | * 215 | * @return void 216 | */ 217 | public function valid(): bool 218 | { 219 | $key = $this->allIteratorKeys[$this->currentIteratorKey] ?? null; 220 | return $key !== null && property_exists($this, $key); 221 | } 222 | 223 | /** 224 | * Return count of all parameters based on usable + required params \ 225 | * presented when constructing Model 226 | * 227 | * @return int 228 | */ 229 | public function count(): int 230 | { 231 | return count($this->allIteratorKeys); 232 | } 233 | 234 | /** 235 | * Returns serializable set of parameters specified in getParams and getRequiredParams utilizing the toArray() method 236 | * 237 | * @return mixed 238 | */ 239 | public function jsonSerialize(): mixed 240 | { 241 | return $this->toArray(); 242 | } 243 | 244 | /** 245 | * Returns new class of same parameters 246 | */ 247 | public function __clone() 248 | { 249 | $this->isCloning = true; 250 | $new = static::fromArray($this->toArray()); 251 | $this->isCloning = false; 252 | return $new; 253 | } 254 | 255 | /** 256 | * Return list of all USABLE and REQUIRED parameters 257 | * 258 | * @return array 259 | */ 260 | public function getAllParams(): array 261 | { 262 | return array_unique(array_merge($this->getParams(), $this->getRequiredParams())); 263 | } 264 | 265 | /** 266 | * Return array of parameters specified in getParams and getRequiredParams 267 | * 268 | * @return array 269 | */ 270 | public function toArray(): array 271 | { 272 | if (!$this->isCloning) { 273 | foreach ($this->getRequiredParams() as $required) { 274 | if ( 275 | !in_array($required, $this->allIteratorKeys) 276 | || empty($this[$required]) && $this[$required] !== 0 277 | ) { 278 | throw Ga4IOException::throwMissingRequiredParam($required); 279 | } 280 | } 281 | } 282 | 283 | $return = []; 284 | foreach ($this as $key => $val) { 285 | if ($val === null || is_array($val) && count($val) === 0) continue; 286 | $return[$key] = $val; 287 | } 288 | 289 | return $return; 290 | } 291 | 292 | /** 293 | * Attempt to create new model and fill parameters from array 294 | * 295 | * @param \AlexWestergaard\PhpGa4\Facade\Type\IO|array $importable 296 | * 297 | * @return static 298 | */ 299 | public static function fromArray(IOType|array $importable): static 300 | { 301 | $static = new static(); 302 | 303 | $importable = $importable instanceof IOType ? $importable->toArray() : $importable; 304 | 305 | foreach ($importable as $key => $val) { 306 | if (is_array($val)) { 307 | foreach ($val as $single) { 308 | if (in_array($key, ['item', 'items']) && !($importable instanceof ItemType)) { 309 | $single = Item::fromArray($single); 310 | } 311 | 312 | $static[$key] = $single; 313 | } 314 | } else { 315 | $static[$key] = $val; 316 | } 317 | } 318 | 319 | return $static; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Helper/UserDataHelper.php: -------------------------------------------------------------------------------- 1 | sha256_email_address = null; 21 | $this->sha256_phone_number = null; 22 | $this->sha256_first_name = null; 23 | $this->sha256_last_name = null; 24 | $this->sha256_street = null; 25 | $this->city = null; 26 | $this->region = null; 27 | $this->postal_code = null; 28 | $this->country = null; 29 | } 30 | 31 | /** 32 | * @param string $email 33 | * @return bool 34 | */ 35 | public function setEmail(string $email): bool 36 | { 37 | $email = str_replace(" ", "", mb_strtolower($email)); 38 | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; 39 | 40 | // https://support.google.com/mail/answer/7436150 41 | if ( 42 | substr($email, -mb_strlen("@gmail.com")) == "@gmail.com" || 43 | substr($email, -mb_strlen("@googlemail.com")) == "@googlemail.com" 44 | ) { 45 | [$addr, $host] = explode("@", $email, 2); 46 | // https://support.google.com/mail/thread/125577450/gmail-and-googlemail 47 | if ($host == "googlemail.com") { 48 | $host = "gmail.com"; 49 | } 50 | // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html 51 | $addr = explode("+", $addr, 2)[0]; 52 | $addr = str_replace(".", "", $addr); 53 | $email = implode("@", [trim($addr), trim($host)]); 54 | } 55 | 56 | $this->sha256_email_address = hash("sha256", $email); 57 | return true; 58 | } 59 | 60 | /** 61 | * @param int $number International number (without prefix "+" and dashes) eg. \ 62 | * "+1-123-4567890" for USA or\ 63 | * "+44-1234-5678900" for UK or\ 64 | * "+45-12345678" for DK 65 | * @return bool 66 | */ 67 | public function setPhone(int $number): bool 68 | { 69 | $sNumber = strval($number); 70 | if (strlen($sNumber) < 3 || strlen($sNumber) > 15) { 71 | return false; 72 | } 73 | 74 | $this->sha256_phone_number = hash("sha256", "+{$sNumber}"); 75 | return true; 76 | } 77 | 78 | /** 79 | * @param string $firstName Users first name 80 | * @return bool 81 | */ 82 | public function setFirstName(string $firstName): bool 83 | { 84 | if (empty($firstName)) return false; 85 | $this->sha256_first_name = hash("sha256", $this->strip($firstName, true)); 86 | return true; 87 | } 88 | 89 | /** 90 | * @param string $lastName Users last name 91 | * @return bool 92 | */ 93 | public function setLastName(string $lastName): bool 94 | { 95 | if (empty($lastName)) return false; 96 | $this->sha256_last_name = hash("sha256", $this->strip($lastName, true)); 97 | return true; 98 | } 99 | 100 | /** 101 | * @param string $street Users street name 102 | * @return bool 103 | */ 104 | public function setStreet(string $street): bool 105 | { 106 | if (empty($street)) return false; 107 | $this->sha256_street = hash("sha256", $this->strip($street)); 108 | return true; 109 | } 110 | 111 | /** 112 | * @param string $city Users city name 113 | * @return bool 114 | */ 115 | public function setCity(string $city): bool 116 | { 117 | if (empty($city)) return false; 118 | $this->city = $this->strip($city, true); 119 | return true; 120 | } 121 | 122 | /** 123 | * @param string $region Users region name 124 | * @return bool 125 | */ 126 | public function setRegion(string $region): bool 127 | { 128 | if (empty($region)) return false; 129 | $this->region = $this->strip($region, true); 130 | return true; 131 | } 132 | 133 | /** 134 | * @param string $postalCode Users postal code 135 | * @return bool 136 | */ 137 | public function setPostalCode(string $postalCode): bool 138 | { 139 | if (empty($postalCode)) return false; 140 | $this->postal_code = $this->strip($postalCode); 141 | return true; 142 | } 143 | 144 | /** 145 | * @param string $iso Users country (ISO) 146 | * @return bool 147 | */ 148 | public function setCountry(string $iso): bool 149 | { 150 | if (!CountryIsoHelper::valid($iso)) { 151 | return false; 152 | } 153 | 154 | $this->country = mb_strtoupper(trim($iso)); 155 | return true; 156 | } 157 | 158 | public function toArray(): array 159 | { 160 | $res = []; 161 | 162 | if (!empty($this->sha256_email_address)) { 163 | $res["sha256_email_address"] = $this->sha256_email_address; 164 | } 165 | 166 | if (!empty($this->sha256_phone_number)) { 167 | $res["sha256_phone_number"] = $this->sha256_phone_number; 168 | } 169 | 170 | $addr = []; 171 | 172 | if (!empty($this->sha256_first_name)) { 173 | $addr["sha256_first_name"] = $this->sha256_first_name; 174 | } 175 | 176 | if (!empty($this->sha256_last_name)) { 177 | $addr["sha256_last_name"] = $this->sha256_last_name; 178 | } 179 | 180 | if (!empty($this->sha256_street)) { 181 | $addr["sha256_street"] = $this->sha256_street; 182 | } 183 | 184 | if (!empty($this->city)) { 185 | $addr["city"] = $this->city; 186 | } 187 | 188 | if (!empty($this->region)) { 189 | $addr["region"] = $this->region; 190 | } 191 | 192 | if (!empty($this->postal_code)) { 193 | $addr["postal_code"] = $this->postal_code; 194 | } 195 | 196 | if (!empty($this->country)) { 197 | $addr["country"] = $this->country; 198 | } 199 | 200 | if (!empty($this->sha256_phone_number)) { 201 | $res["sha256_phone_number"] = $this->sha256_phone_number; 202 | } 203 | 204 | if (count($addr) > 0) { 205 | $res["address"] = $addr; 206 | } 207 | 208 | return $res; 209 | } 210 | 211 | private function strip(string $s, bool $removeDigits = false): string 212 | { 213 | $d = $removeDigits ? '0-9' : ''; 214 | 215 | $s = preg_replace("[^a-zA-Z{$d}\-\_\.\,\s]", "", $s); 216 | $s = mb_strtolower($s); 217 | return trim($s); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Helper/UserPropertyHelper.php: -------------------------------------------------------------------------------- 1 | 24) { 20 | throw Ga4UserPropertyException::throwNameTooLong($name); 21 | } 22 | 23 | $this->name = $name; 24 | return $this; 25 | } 26 | 27 | public function toArray(): array 28 | { 29 | $return = []; 30 | 31 | if (!isset($this->name)) { 32 | throw Ga4UserPropertyException::throwNameMissing(); 33 | } 34 | 35 | $value = isset($this->value) ? $this->value : null; 36 | if (!is_array($value)) { 37 | $value = ['value' => $value]; 38 | } 39 | 40 | $return[$this->name] = $value; 41 | 42 | return $return; 43 | } 44 | 45 | public static function new(): static 46 | { 47 | return new static(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Item.php: -------------------------------------------------------------------------------- 1 | item_id = $id; 32 | return $this; 33 | } 34 | 35 | public function setItemName(string $name) 36 | { 37 | $this->item_name = $name; 38 | return $this; 39 | } 40 | 41 | public function setAffiliation(string $affiliation) 42 | { 43 | $this->affiliation = $affiliation; 44 | return $this; 45 | } 46 | 47 | public function setCoupon(string $code) 48 | { 49 | $this->coupon = $code; 50 | return $this; 51 | } 52 | 53 | public function setCurrency(string $iso) 54 | { 55 | $this->currency = $iso; 56 | return $this; 57 | } 58 | 59 | public function setDiscount(int|float $amount) 60 | { 61 | $this->discount = $amount; 62 | return $this; 63 | } 64 | 65 | public function setIndex(int $i) 66 | { 67 | $this->index = $i; 68 | return $this; 69 | } 70 | 71 | public function setItemBrand(string $brand) 72 | { 73 | $this->item_brand = $brand; 74 | return $this; 75 | } 76 | 77 | public function addItemCategory(string $category) 78 | { 79 | $this->item_category[] = $category; 80 | return $this; 81 | } 82 | 83 | public function setItemListId(string $id) 84 | { 85 | $this->item_list_id = $id; 86 | return $this; 87 | } 88 | 89 | public function setItemListName(string $name) 90 | { 91 | $this->item_list_name = $name; 92 | return $this; 93 | } 94 | 95 | public function setItemVariant(string $variant) 96 | { 97 | $this->item_variant = $variant; 98 | return $this; 99 | } 100 | 101 | public function setLocationId(string $id) 102 | { 103 | $this->location_id = $id; 104 | return $this; 105 | } 106 | 107 | public function setPrice(int|float $amount) 108 | { 109 | $this->price = 0 + $amount; 110 | return $this; 111 | } 112 | 113 | public function setQuantity(int $amount) 114 | { 115 | $this->quantity = $amount; 116 | return $this; 117 | } 118 | 119 | public function getParams(): array 120 | { 121 | return [ 122 | 'item_id', 123 | 'item_name', 124 | 'affiliation', 125 | 'coupon', 126 | 'currency', 127 | 'discount', 128 | 'index', 129 | 'item_brand', 130 | 'item_category', 131 | 'item_list_id', 132 | 'item_list_name', 133 | 'item_variant', 134 | 'location_id', 135 | 'price', 136 | 'quantity', 137 | ]; 138 | } 139 | 140 | public function getRequiredParams(): array 141 | { 142 | $return = []; 143 | 144 | if ( 145 | (!isset($this->item_id) || empty($this->item_id) && strval($this->item_id) !== '0') 146 | && (!isset($this->item_name) || empty($this->item_name) && strval($this->item_name) !== '0') 147 | ) { 148 | $return = [ 149 | 'item_id', 150 | 'item_name', 151 | ]; 152 | } 153 | 154 | return $return; 155 | } 156 | 157 | public function toArray(): array 158 | { 159 | $res = parent::toArray(); 160 | 161 | if (!isset($res['item_category'])) { 162 | return $res; 163 | } 164 | 165 | $categories = $res['item_category']; 166 | unset($res['item_category']); 167 | 168 | if (is_array($categories)) { 169 | foreach ($categories as $k => $val) { 170 | $tag = 'item_category' . ($k > 0 ? $k + 1 : ''); 171 | 172 | $res[$tag] = $val; 173 | } 174 | } 175 | 176 | return $res; 177 | } 178 | 179 | public static function new(): static 180 | { 181 | return new static(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/UserProperty.php: -------------------------------------------------------------------------------- 1 | value = $value; 38 | return $this; 39 | } 40 | 41 | public function getParams(): array 42 | { 43 | return ['name', 'value']; 44 | } 45 | 46 | public function getRequiredParams(): array 47 | { 48 | return ['name', 'value']; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/FirebaseTestCase.php: -------------------------------------------------------------------------------- 1 | prefill = [ 22 | // Analytics 23 | 'firebase_app_id' => 'sdha62HsGjk63lKe2hkyLs', 24 | 'api_secret' => 'gDS1gs423dDSH34sdfa', 25 | 'app_instance_id' => 'e85ab98bdbbf3713a74b8f0409363d20', 26 | 'user_id' => 'm6435', 27 | // Default Vars 28 | 'currency' => 'EUR', 29 | 'currency_virtual' => 'GA4Coins', 30 | ]; 31 | 32 | $this->firebase = Firebase::new($this->prefill['firebase_app_id'], $this->prefill['api_secret'], /* DEBUG */ true) 33 | ->setAppInstanceId($this->prefill['app_instance_id']) 34 | ->setUserId($this->prefill['user_id']); 35 | 36 | $this->item = Item::new() 37 | ->setItemId('1') 38 | ->setItemName('First Product') 39 | ->setCurrency($this->prefill['currency']) 40 | ->setPrice(7.39) 41 | ->setQuantity(2); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/MeasurementTestCase.php: -------------------------------------------------------------------------------- 1 | prefill = [ 22 | // Analytics 23 | 'measurement_id' => 'G-XXXXXXXX', 24 | 'api_secret' => 'gDS1gs423dDSH34sdfa', 25 | 'client_id' => 'GA0.43535.234234', 26 | 'user_id' => 'm6435', 27 | // Default Vars 28 | 'currency' => 'EUR', 29 | 'currency_virtual' => 'GA4Coins', 30 | ]; 31 | 32 | $this->analytics = Analytics::new($this->prefill['measurement_id'], $this->prefill['api_secret'], /* DEBUG */ true) 33 | ->setClientId($this->prefill['client_id']) 34 | ->setUserId($this->prefill['user_id']); 35 | 36 | $this->item = Item::new() 37 | ->setItemId('1') 38 | ->setItemName('First Product') 39 | ->setCurrency($this->prefill['currency']) 40 | ->setPrice(7.39) 41 | ->setQuantity(2); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Mocks/MockEventHelper.php: -------------------------------------------------------------------------------- 1 | test = $val; 32 | } 33 | 34 | public function setTestRequired($val) 35 | { 36 | $this->test_required = $val; 37 | } 38 | 39 | public function addTestItem(Facade\Type\ItemType $val) 40 | { 41 | $this->test_items[] = $val->toArray(); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /test/Mocks/MockIOHelper.php: -------------------------------------------------------------------------------- 1 | test = $val; 26 | } 27 | 28 | public function setTestRequired($val) 29 | { 30 | $this->test_required = $val; 31 | } 32 | 33 | public function addTestArray($val) 34 | { 35 | $this->test_array[] = $val; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /test/Mocks/MockUserPropertyHelper.php: -------------------------------------------------------------------------------- 1 | value = $value; 15 | return $this; 16 | } 17 | 18 | public function getParams(): array 19 | { 20 | return ['name', 'value']; 21 | } 22 | 23 | public function getRequiredParams(): array 24 | { 25 | return ['name', 'value']; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/Unit/AbstractTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Helper\IOHelper::class, $io, get_class($io)); 23 | $this->assertArrayHasKey('test', $io); 24 | $this->assertArrayHasKey('test_required', $io); 25 | $this->assertArrayHasKey('test_array', $io); 26 | 27 | $io['test'] = 'optionalTest'; 28 | $this->assertEquals('optionalTest', $io['test']); 29 | $this->assertEquals('optionalTest', $io->test); 30 | 31 | $io['test_required'] = 'requiredTest'; 32 | $this->assertEquals('requiredTest', $io['test_required']); 33 | $this->assertEquals('requiredTest', $io->test_required); 34 | 35 | $io['test_array'] = 'optionalArrayElement'; 36 | $this->assertIsArray($io['test_array']); 37 | $this->assertIsArray($io->test_array); 38 | /** @var array */ $test_array = $io['test_array']; 39 | $this->assertContains('optionalArrayElement', $test_array); 40 | $this->assertContains('optionalArrayElement', $io->test_array); 41 | 42 | $export = $io->toArray(); 43 | $this->assertIsArray($export); 44 | $this->assertArrayHasKey('test', $export); 45 | $this->assertArrayHasKey('test_required', $export); 46 | } 47 | 48 | public function test_abstract_io_throws_on_missing_required_param() 49 | { 50 | $io = new Mocks\MockIOHelper(); 51 | 52 | $this->expectException(Exception\Ga4IOException::class); 53 | $this->expectExceptionCode(Exception\Ga4Exception::PARAM_MISSING_REQUIRED); 54 | 55 | $io->toArray(); 56 | } 57 | 58 | public function test_abstract_io_unsets_array_as_empty_array() 59 | { 60 | $io = new Mocks\MockIOHelper(); 61 | $this->assertIsArray($io['test_array']); 62 | unset($io['test_array']); 63 | $this->assertIsArray($io['test_array']); 64 | } 65 | 66 | public function test_abstract_io_can_iterate_as_arrayable() 67 | { 68 | $io = new Mocks\MockIOHelper(); 69 | $io['test'] = 'optionalTest'; 70 | $io['test_required'] = 'requiredTest'; 71 | $io['test_array'] = 'optionalArrayElement'; 72 | 73 | foreach ($io as $param => $val) { 74 | $this->assertContains($param, $io->getAllParams()); 75 | $this->assertNotEmpty($val); 76 | } 77 | } 78 | 79 | /****************************************************************** 80 | * ABSTRACT EVENT 81 | */ 82 | 83 | public function test_abstract_event_interface_capabilities() 84 | { 85 | $event = new Mocks\MockEventHelper(); 86 | 87 | $this->assertInstanceOf(Helper\EventHelper::class, $event, get_class($event)); 88 | $this->assertArrayHasKey('test', $event); 89 | $this->assertArrayHasKey('test_required', $event); 90 | $this->assertArrayHasKey('test_items', $event); 91 | 92 | $event['test_required'] = 1; 93 | 94 | $event['test_items'] = Item::new() 95 | ->setItemId('1') 96 | ->setItemName('First Product') 97 | ->setCurrency('DKK') 98 | ->setPrice(7.39) 99 | ->setQuantity(2); 100 | 101 | $this->assertIsArray($event['test_items'], 'arrayable'); 102 | $this->assertIsArray($event->test_items); 103 | 104 | $export = $event->toArray(); 105 | $this->assertIsArray($export); 106 | $this->assertArrayHasKey('name', $export); 107 | $this->assertArrayHasKey('params', $export); 108 | 109 | $exportParams = $export['params']; 110 | $this->assertArrayNotHasKey('test', $exportParams); 111 | $this->assertArrayHasKey('test_required', $exportParams); 112 | $this->assertArrayHasKey('test_items', $exportParams); 113 | $this->assertCount(2, $exportParams); 114 | $this->assertCount(1, $exportParams['test_items']); 115 | } 116 | 117 | public function test_abstract_event_throws_on_missing_name() 118 | { 119 | $event = new class extends Mocks\MockEventHelper 120 | { 121 | public function getName(): string 122 | { 123 | return ''; 124 | } 125 | }; 126 | 127 | $this->expectException(Exception\Ga4EventException::class); 128 | $this->expectExceptionCode(Exception\Ga4Exception::EVENT_NAME_MISSING); 129 | 130 | $event->toArray(); 131 | } 132 | 133 | public function test_abstract_event_throws_on_reserved_name() 134 | { 135 | $event = new class extends Mocks\MockEventHelper 136 | { 137 | public function getName(): string 138 | { 139 | return Type\EventType::RESERVED_NAMES[0]; 140 | } 141 | }; 142 | 143 | $this->expectException(Exception\Ga4EventException::class); 144 | $this->expectExceptionCode(Exception\Ga4Exception::EVENT_NAME_RESERVED); 145 | 146 | $event->toArray(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /test/Unit/AnalyticsTest.php: -------------------------------------------------------------------------------- 1 | prefill['measurement_id'], 18 | $this->prefill['api_secret'], 19 | $debug = true 20 | ) 21 | ->setClientId($this->prefill['client_id']) 22 | ->setTimestampMicros($time = time()) 23 | ->addEvent($event = Event\JoinGroup::fromArray(['group_id' => 1])) 24 | ->addUserProperty($userProperty = UserProperty::fromArray(['name' => 'test', 'value' => 'testvalue'])); 25 | 26 | $asArray = $analytics->toArray(); 27 | $this->assertIsArray($asArray); 28 | 29 | $this->assertArrayHasKey('timestamp_micros', $asArray); 30 | $this->assertArrayHasKey('client_id', $asArray); 31 | $this->assertArrayNotHasKey('user_id', $asArray); 32 | $this->assertArrayHasKey('user_properties', $asArray); 33 | $this->assertArrayHasKey('events', $asArray); 34 | 35 | $timeAsMicro = $time * 1_000_000; 36 | 37 | $this->assertEquals($timeAsMicro, $asArray['timestamp_micros']); 38 | $this->assertEquals($this->prefill['client_id'], $asArray['client_id']); 39 | $this->assertEquals($userProperty->toArray(), $asArray['user_properties']); 40 | $this->assertEquals([$event->toArray()], $asArray['events']); 41 | } 42 | 43 | public function test_can_configure_only_user_id_and_export() 44 | { 45 | $analytics = Analytics::new( 46 | $this->prefill['measurement_id'], 47 | $this->prefill['api_secret'], 48 | $debug = true 49 | ) 50 | ->setUserId($this->prefill['user_id']) 51 | ->setTimestampMicros($time = time()) 52 | ->addEvent($event = Event\JoinGroup::fromArray(['group_id' => 1])) 53 | ->addUserProperty($userProperty = UserProperty::fromArray(['name' => 'test', 'value' => 'testvalue'])); 54 | 55 | $asArray = $analytics->toArray(); 56 | $this->assertIsArray($asArray); 57 | 58 | $this->assertArrayHasKey('timestamp_micros', $asArray); 59 | $this->assertArrayNotHasKey('client_id', $asArray); 60 | $this->assertArrayHasKey('user_id', $asArray); 61 | $this->assertArrayHasKey('user_properties', $asArray); 62 | $this->assertArrayHasKey('events', $asArray); 63 | 64 | $timeAsMicro = $time * 1_000_000; 65 | 66 | $this->assertEquals($timeAsMicro, $asArray['timestamp_micros']); 67 | $this->assertEquals($this->prefill['user_id'], $asArray['user_id']); 68 | $this->assertEquals($userProperty->toArray(), $asArray['user_properties']); 69 | $this->assertEquals([$event->toArray()], $asArray['events']); 70 | } 71 | 72 | public function test_can_configure_and_export() 73 | { 74 | $analytics = Analytics::new( 75 | $this->prefill['measurement_id'], 76 | $this->prefill['api_secret'], 77 | $debug = true 78 | ) 79 | ->setClientId($this->prefill['client_id']) 80 | ->setUserId($this->prefill['user_id']) 81 | ->setTimestampMicros($time = time()) 82 | ->addEvent($event = Event\JoinGroup::fromArray(['group_id' => 1])) 83 | ->addUserProperty($userProperty = UserProperty::fromArray(['name' => 'test', 'value' => 'testvalue'])); 84 | 85 | $asArray = $analytics->toArray(); 86 | $this->assertIsArray($asArray); 87 | 88 | $this->assertArrayHasKey('timestamp_micros', $asArray); 89 | $this->assertArrayHasKey('client_id', $asArray); 90 | $this->assertArrayHasKey('user_id', $asArray); 91 | $this->assertArrayHasKey('user_properties', $asArray); 92 | $this->assertArrayHasKey('events', $asArray); 93 | 94 | $timeAsMicro = $time * 1_000_000; 95 | 96 | $this->assertEquals($timeAsMicro, $asArray['timestamp_micros']); 97 | $this->assertEquals($this->prefill['client_id'], $asArray['client_id']); 98 | $this->assertEquals($this->prefill['user_id'], $asArray['user_id']); 99 | $this->assertEquals($userProperty->toArray(), $asArray['user_properties']); 100 | $this->assertEquals([$event->toArray()], $asArray['events']); 101 | } 102 | 103 | public function test_can_post_only_client_id_to_google() 104 | { 105 | $this->expectNotToPerformAssertions(); 106 | Analytics::new( 107 | $this->prefill['measurement_id'], 108 | $this->prefill['api_secret'], 109 | $debug = true 110 | ) 111 | ->setClientId($this->prefill['user_id']) 112 | ->addEvent(Login::new())->post(); 113 | } 114 | 115 | public function test_can_post_to_google() 116 | { 117 | $this->expectNotToPerformAssertions(); 118 | $this->analytics->addEvent(Login::new())->post(); 119 | } 120 | 121 | public function test_converts_to_full_microtime_stamp() 122 | { 123 | $this->analytics->setTimestampMicros(microtime(true)); 124 | 125 | $arr = $this->analytics->toArray(); 126 | 127 | $this->assertTrue($arr['timestamp_micros'] > 1_000_000); 128 | } 129 | 130 | public function test_throws_if_microtime_older_than_three_days() 131 | { 132 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 133 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_EXPIRED); 134 | 135 | $this->analytics->setTimestampMicros(strtotime('-1 week')); 136 | } 137 | 138 | public function test_exports_userproperty_to_array() 139 | { 140 | $this->analytics->addEvent(Login::new()); 141 | 142 | $userProperty = UserProperty::new() 143 | ->setName('customer_tier') 144 | ->setValue('premium'); 145 | 146 | $this->assertInstanceOf(UserProperty::class, $userProperty); 147 | $this->assertIsArray($userProperty->toArray()); 148 | 149 | $this->analytics->addUserProperty($userProperty); 150 | 151 | $arr = $this->analytics->toArray(); 152 | $this->assertArrayHasKey('user_properties', $arr); 153 | 154 | $arr = $arr['user_properties']; 155 | $this->assertArrayHasKey('customer_tier', $arr); 156 | 157 | $this->analytics->post(); 158 | } 159 | 160 | public function test_exports_events_to_array() 161 | { 162 | $event = Event\JoinGroup::new() 163 | ->setGroupId('1'); 164 | 165 | $this->assertInstanceOf(Facade\Type\EventType::class, $event); 166 | $this->assertIsArray($event->toArray()); 167 | 168 | $this->analytics->addEvent($event); 169 | 170 | $arr = $this->analytics->toArray(); 171 | $this->assertArrayHasKey('events', $arr); 172 | $this->assertCount(1, $arr['events']); 173 | 174 | $this->analytics->post(); 175 | } 176 | 177 | public function test_throws_missing_measurement_id() 178 | { 179 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 180 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_MISSING_MEASUREMENT_ID); 181 | 182 | Analytics::new('', $this->prefill['api_secret'], true)->post(); 183 | } 184 | 185 | public function test_throws_missing_apisecret() 186 | { 187 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 188 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_MISSING_API_SECRET); 189 | 190 | Analytics::new($this->prefill['measurement_id'], '', true)->post(); 191 | } 192 | 193 | public function test_throws_on_too_large_request_package() 194 | { 195 | $kB = 1024; 196 | $preparyKB = ''; 197 | while (mb_strlen($preparyKB) < $kB) { 198 | $preparyKB .= 'AAAAAAAA'; // 8 bytes 199 | } 200 | 201 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 202 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_TOO_LARGE); 203 | 204 | $userProperty = UserProperty::new()->setName('large_package'); 205 | 206 | $overflowValue = ''; 207 | while (mb_strlen(json_encode($userProperty->toArray())) <= ($kB * 131)) { 208 | $overflowValue .= $preparyKB; 209 | $userProperty->setValue($overflowValue); 210 | } 211 | 212 | $this->analytics->addEvent(Login::new())->addUserProperty($userProperty)->post(); 213 | } 214 | 215 | public function test_timeasmicro_throws_exceeding_max() 216 | { 217 | $time = time() + 60; 218 | 219 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 220 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_EXPIRED); 221 | 222 | $this->analytics->setTimestampMicros($time); 223 | } 224 | 225 | public function test_timeasmicro_throws_exceeding_min() 226 | { 227 | $time = strtotime('-1 month'); 228 | 229 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 230 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_EXPIRED); 231 | 232 | $this->analytics->setTimestampMicros($time); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /test/Unit/ConsentTest.php: -------------------------------------------------------------------------------- 1 | analytics->addEvent(Login::new()); 14 | 15 | $export = $this->analytics->consent()->toArray(); 16 | $this->assertIsArray($export); 17 | $this->assertCount(0, $export); 18 | } 19 | 20 | public function test_consent_ad_user_data_granted() 21 | { 22 | $this->analytics->addEvent(Login::new()); 23 | 24 | $this->analytics->consent()->setAdUserDataPermission(true); 25 | 26 | $export = $this->analytics->consent()->toArray(); 27 | $this->assertIsArray($export); 28 | $this->assertCount(1, $export); 29 | $this->assertArrayHasKey("ad_user_data", $export); 30 | $this->assertArrayNotHasKey("ad_personalization", $export); 31 | $this->assertEquals(ConsentHelper::GRANTED, $export["ad_user_data"]); 32 | } 33 | 34 | public function test_consent_ad_personalization_granted() 35 | { 36 | $this->analytics->addEvent(Login::new()); 37 | 38 | $this->analytics->consent()->setAdPersonalizationPermission(true); 39 | 40 | $export = $this->analytics->consent()->toArray(); 41 | $this->assertIsArray($export); 42 | $this->assertCount(1, $export); 43 | $this->assertArrayHasKey("ad_personalization", $export); 44 | $this->assertArrayNotHasKey("ad_user_data", $export); 45 | $this->assertEquals(ConsentHelper::GRANTED, $export["ad_personalization"]); 46 | } 47 | 48 | public function test_consent_granted() 49 | { 50 | $this->analytics->addEvent(Login::new()); 51 | 52 | $this->analytics->consent()->setAdUserDataPermission(true); 53 | $this->analytics->consent()->setAdPersonalizationPermission(true); 54 | 55 | $export = $this->analytics->consent()->toArray(); 56 | $this->assertIsArray($export); 57 | $this->assertCount(2, $export); 58 | $this->assertArrayHasKey("ad_user_data", $export); 59 | $this->assertArrayHasKey("ad_personalization", $export); 60 | $this->assertEquals(ConsentHelper::GRANTED, $export["ad_user_data"]); 61 | $this->assertEquals(ConsentHelper::GRANTED, $export["ad_personalization"]); 62 | } 63 | 64 | public function test_consent_granted_posted() 65 | { 66 | $this->analytics->addEvent(Login::new()); 67 | 68 | $this->analytics->consent()->setAdUserDataPermission(true); 69 | $this->analytics->consent()->setAdPersonalizationPermission(true); 70 | 71 | $export = $this->analytics->consent()->toArray(); 72 | $this->assertIsArray($export); 73 | $this->assertCount(2, $export); 74 | $this->assertArrayHasKey("ad_user_data", $export); 75 | $this->assertArrayHasKey("ad_personalization", $export); 76 | $this->assertEquals(ConsentHelper::GRANTED, $export["ad_user_data"]); 77 | $this->assertEquals(ConsentHelper::GRANTED, $export["ad_personalization"]); 78 | $this->analytics->post(); 79 | } 80 | 81 | public function test_consent_ad_user_data_denied() 82 | { 83 | $this->analytics->addEvent(Login::new()); 84 | 85 | $this->analytics->consent()->setAdUserDataPermission(false); 86 | 87 | $export = $this->analytics->consent()->toArray(); 88 | $this->assertIsArray($export); 89 | $this->assertCount(1, $export); 90 | $this->assertArrayHasKey("ad_user_data", $export); 91 | $this->assertArrayNotHasKey("ad_personalization", $export); 92 | $this->assertEquals(ConsentHelper::DENIED, $export["ad_user_data"]); 93 | } 94 | 95 | public function test_consent_ad_personalization_denied() 96 | { 97 | $this->analytics->addEvent(Login::new()); 98 | 99 | $this->analytics->consent()->setAdPersonalizationPermission(false); 100 | 101 | $export = $this->analytics->consent()->toArray(); 102 | $this->assertIsArray($export); 103 | $this->assertCount(1, $export); 104 | $this->assertArrayHasKey("ad_personalization", $export); 105 | $this->assertArrayNotHasKey("ad_user_data", $export); 106 | $this->assertEquals(ConsentHelper::DENIED, $export["ad_personalization"]); 107 | } 108 | 109 | public function test_consent_denied() 110 | { 111 | $this->analytics->addEvent(Login::new()); 112 | 113 | $this->analytics->consent()->setAdUserDataPermission(false); 114 | $this->analytics->consent()->setAdPersonalizationPermission(false); 115 | 116 | $export = $this->analytics->consent()->toArray(); 117 | $this->assertIsArray($export); 118 | $this->assertCount(2, $export); 119 | $this->assertArrayHasKey("ad_user_data", $export); 120 | $this->assertArrayHasKey("ad_personalization", $export); 121 | $this->assertEquals(ConsentHelper::DENIED, $export["ad_user_data"]); 122 | $this->assertEquals(ConsentHelper::DENIED, $export["ad_personalization"]); 123 | } 124 | 125 | public function test_consent_denied_posted() 126 | { 127 | $this->analytics->addEvent(Login::new()); 128 | 129 | $this->analytics->consent()->setAdUserDataPermission(false); 130 | $this->analytics->consent()->setAdPersonalizationPermission(false); 131 | 132 | $export = $this->analytics->consent()->toArray(); 133 | $this->assertIsArray($export); 134 | $this->assertCount(2, $export); 135 | $this->assertArrayHasKey("ad_user_data", $export); 136 | $this->assertArrayHasKey("ad_personalization", $export); 137 | $this->assertEquals(ConsentHelper::DENIED, $export["ad_user_data"]); 138 | $this->assertEquals(ConsentHelper::DENIED, $export["ad_personalization"]); 139 | $this->analytics->post(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/Unit/FirebaseTest.php: -------------------------------------------------------------------------------- 1 | prefill['firebase_app_id'], 18 | $this->prefill['api_secret'], 19 | $debug = true 20 | ) 21 | ->setAppInstanceId($this->prefill['app_instance_id']) 22 | ->setTimestampMicros($time = time()) 23 | ->addEvent($event = Event\JoinGroup::fromArray(['group_id' => 1])) 24 | ->addUserProperty($userProperty = UserProperty::fromArray(['name' => 'test', 'value' => 'testvalue'])); 25 | 26 | $asArray = $analytics->toArray(); 27 | $this->assertIsArray($asArray); 28 | 29 | $this->assertArrayHasKey('timestamp_micros', $asArray); 30 | $this->assertArrayHasKey('app_instance_id', $asArray); 31 | $this->assertArrayNotHasKey('user_id', $asArray); 32 | $this->assertArrayHasKey('user_properties', $asArray); 33 | $this->assertArrayHasKey('events', $asArray); 34 | 35 | $timeAsMicro = $time * 1_000_000; 36 | 37 | $this->assertEquals($timeAsMicro, $asArray['timestamp_micros']); 38 | $this->assertEquals($this->prefill['app_instance_id'], $asArray['app_instance_id']); 39 | $this->assertEquals($userProperty->toArray(), $asArray['user_properties']); 40 | $this->assertEquals([$event->toArray()], $asArray['events']); 41 | } 42 | 43 | public function test_can_configure_and_export() 44 | { 45 | $analytics = Firebase::new( 46 | $this->prefill['firebase_app_id'], 47 | $this->prefill['api_secret'], 48 | $debug = true 49 | ) 50 | ->setAppInstanceId($this->prefill['app_instance_id']) 51 | ->setUserId($this->prefill['user_id']) 52 | ->setTimestampMicros($time = time()) 53 | ->addEvent($event = Event\JoinGroup::fromArray(['group_id' => 1])) 54 | ->addUserProperty($userProperty = UserProperty::fromArray(['name' => 'test', 'value' => 'testvalue'])); 55 | 56 | $asArray = $analytics->toArray(); 57 | $this->assertIsArray($asArray); 58 | 59 | $this->assertArrayHasKey('timestamp_micros', $asArray); 60 | $this->assertArrayHasKey('app_instance_id', $asArray); 61 | $this->assertArrayHasKey('user_id', $asArray); 62 | $this->assertArrayHasKey('user_properties', $asArray); 63 | $this->assertArrayHasKey('events', $asArray); 64 | 65 | $timeAsMicro = $time * 1_000_000; 66 | 67 | $this->assertEquals($timeAsMicro, $asArray['timestamp_micros']); 68 | $this->assertEquals($this->prefill['app_instance_id'], $asArray['app_instance_id']); 69 | $this->assertEquals($this->prefill['user_id'], $asArray['user_id']); 70 | $this->assertEquals($userProperty->toArray(), $asArray['user_properties']); 71 | $this->assertEquals([$event->toArray()], $asArray['events']); 72 | } 73 | 74 | public function test_can_post_only_client_id_to_google() 75 | { 76 | $this->expectNotToPerformAssertions(); 77 | Firebase::new( 78 | $this->prefill['firebase_app_id'], 79 | $this->prefill['api_secret'], 80 | $debug = true 81 | ) 82 | ->setAppInstanceId($this->prefill['app_instance_id']) 83 | ->addEvent(Login::new()) 84 | ->post(); 85 | } 86 | 87 | public function test_can_post_to_google() 88 | { 89 | $this->expectNotToPerformAssertions(); 90 | $this->firebase->addEvent(Login::new())->post(); 91 | } 92 | 93 | public function test_converts_to_full_microtime_stamp() 94 | { 95 | $this->firebase->setTimestampMicros(microtime(true)); 96 | 97 | $arr = $this->firebase->toArray(); 98 | 99 | $this->assertTrue($arr['timestamp_micros'] > 1_000_000); 100 | } 101 | 102 | public function test_throws_if_app_instance_id_missing() 103 | { 104 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 105 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_MISSING_FIREBASE_APP_INSTANCE_ID); 106 | 107 | Firebase::new($this->prefill['firebase_app_id'], $this->prefill['api_secret'], /* DEBUG */ true)->post(); 108 | } 109 | 110 | public function test_throws_if_microtime_older_than_three_days() 111 | { 112 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 113 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_EXPIRED); 114 | 115 | $this->firebase->setTimestampMicros(strtotime('-1 week')); 116 | } 117 | 118 | public function test_exports_userproperty_to_array() 119 | { 120 | $this->firebase->addEvent(Login::new()); 121 | 122 | $userProperty = UserProperty::new() 123 | ->setName('customer_tier') 124 | ->setValue('premium'); 125 | 126 | $this->assertInstanceOf(UserProperty::class, $userProperty); 127 | $this->assertIsArray($userProperty->toArray()); 128 | 129 | $this->firebase->addUserProperty($userProperty); 130 | 131 | $arr = $this->firebase->toArray(); 132 | $this->assertArrayHasKey('user_properties', $arr); 133 | 134 | $arr = $arr['user_properties']; 135 | $this->assertArrayHasKey('customer_tier', $arr); 136 | 137 | $this->firebase->post(); 138 | } 139 | 140 | public function test_exports_events_to_array() 141 | { 142 | $event = Event\JoinGroup::new() 143 | ->setGroupId('1'); 144 | 145 | $this->assertInstanceOf(Facade\Type\EventType::class, $event); 146 | $this->assertIsArray($event->toArray()); 147 | 148 | $this->firebase->addEvent($event); 149 | 150 | $arr = $this->firebase->toArray(); 151 | $this->assertArrayHasKey('events', $arr); 152 | $this->assertCount(1, $arr['events']); 153 | 154 | $this->firebase->post(); 155 | } 156 | 157 | public function test_throws_missing_firebase_app_id() 158 | { 159 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 160 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_MISSING_FIREBASE_APP_ID); 161 | 162 | Firebase::new('', $this->prefill['api_secret'], true)->post(); 163 | } 164 | 165 | public function test_throws_missing_apisecret() 166 | { 167 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 168 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_MISSING_API_SECRET); 169 | 170 | Firebase::new($this->prefill['firebase_app_id'], '', true)->post(); 171 | } 172 | 173 | public function test_throws_on_too_large_request_package() 174 | { 175 | $kB = 1024; 176 | $preparyKB = ''; 177 | while (mb_strlen($preparyKB) < $kB) { 178 | $preparyKB .= 'AAAAAAAA'; // 8 bytes 179 | } 180 | 181 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 182 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::REQUEST_TOO_LARGE); 183 | 184 | $userProperty = UserProperty::new()->setName('large_package'); 185 | 186 | $overflowValue = ''; 187 | while (mb_strlen(json_encode($userProperty->toArray())) <= ($kB * 131)) { 188 | $overflowValue .= $preparyKB; 189 | $userProperty->setValue($overflowValue); 190 | } 191 | 192 | $this->firebase->addEvent(Login::new())->addUserProperty($userProperty)->post(); 193 | } 194 | 195 | public function test_timeasmicro_throws_exceeding_max() 196 | { 197 | $time = time() + 60; 198 | 199 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 200 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_EXPIRED); 201 | 202 | $this->firebase->setTimestampMicros($time); 203 | } 204 | 205 | public function test_timeasmicro_throws_exceeding_min() 206 | { 207 | $time = strtotime('-1 month'); 208 | 209 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 210 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_EXPIRED); 211 | 212 | $this->firebase->setTimestampMicros($time); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /test/Unit/HelperTest.php: -------------------------------------------------------------------------------- 1 | []], 16 | ['UnlockAchievement' => ['achievement_id' => '123']], 17 | ['NotAnEvent' => ['skip' => 'me']] 18 | ]); 19 | 20 | $this->assertIsArray($list); 21 | $this->assertCount(2, $list); 22 | $this->assertInstanceOf(Event\TutorialBegin::class, $list[0]); 23 | $this->assertInstanceOf(Event\UnlockAchievement::class, $list[1]); 24 | 25 | $this->analytics->addEvent(...$list); 26 | $this->assertCount(2, $this->analytics['events']); 27 | } 28 | 29 | public function test_convert_helper_transforms_events_with_items() 30 | { 31 | $request = [ 32 | [ 33 | 'AddToCart' => [ 34 | 'currency' => 'AUD', 35 | 'value' => 38, 36 | 'items' => [ 37 | [ 38 | 'item_id' => 29, 39 | 'item_name' => '500g Musk Scrolls', 40 | 'price' => 38, 41 | 'quantity' => 1, 42 | ], 43 | ], 44 | ], 45 | ], 46 | ]; 47 | 48 | $list = Helper\ConvertHelper::parseEvents($request); 49 | $this->analytics->addEvent(...$list); 50 | $this->assertCount(1, $this->analytics['events']); 51 | } 52 | 53 | public function test_snakecase_helper_transforms_camelcase_names() 54 | { 55 | $output = Helper\ConvertHelper::snake('snakeCase'); 56 | $this->assertEquals('snake_case', $output); 57 | } 58 | 59 | public function test_camelcase_helper_transforms_snakecase_names() 60 | { 61 | $output = Helper\ConvertHelper::camel('snake_case'); 62 | $this->assertEquals('snakeCase', $output); 63 | } 64 | 65 | public function test_timeasmicro_converts_to_microseconds() 66 | { 67 | $time = time(); 68 | $secondAsMicro = 1_000_000; 69 | $timeAsMicro = $time * $secondAsMicro; 70 | 71 | $convert = Helper\ConvertHelper::timeAsMicro($time); 72 | 73 | $this->assertEquals($timeAsMicro, $convert); 74 | } 75 | 76 | public function test_timeasmicro_throws_too_large() 77 | { 78 | $time = time() * 100; 79 | 80 | $this->expectException(Facade\Type\Ga4ExceptionType::class); 81 | $this->expectExceptionCode(Facade\Type\Ga4ExceptionType::MICROTIME_INVALID_FORMAT); 82 | 83 | $this->analytics->setTimestampMicros($time); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/Unit/ItemTest.php: -------------------------------------------------------------------------------- 1 | setItemId('1') 14 | ->setItemName('First Product') 15 | ->setAffiliation('unit test') 16 | ->setCoupon('code') 17 | ->setCurrency($this->prefill['currency']) 18 | ->setDiscount(1.11) 19 | ->setIndex(1) 20 | ->setItemBrand('phpunit') 21 | ->addItemCategory('unit') 22 | ->setItemListId('test-list') 23 | ->setItemListName('test list') 24 | ->setItemVariant('test') 25 | ->setLocationId('L1') 26 | ->setPrice(7.39) 27 | ->setQuantity(2); 28 | 29 | $asArray = $item->toArray(); 30 | 31 | $this->assertIsArray($asArray); 32 | $this->assertArrayHasKey('item_id', $asArray); 33 | $this->assertArrayHasKey('item_name', $asArray); 34 | $this->assertArrayHasKey('affiliation', $asArray); 35 | $this->assertArrayHasKey('coupon', $asArray); 36 | $this->assertArrayHasKey('currency', $asArray); 37 | $this->assertArrayHasKey('item_brand', $asArray); 38 | $this->assertArrayHasKey('item_list_id', $asArray); 39 | $this->assertArrayHasKey('item_list_name', $asArray); 40 | $this->assertArrayHasKey('item_variant', $asArray); 41 | $this->assertArrayHasKey('location_id', $asArray); 42 | $this->assertArrayHasKey('discount', $asArray); 43 | $this->assertArrayHasKey('price', $asArray); 44 | $this->assertArrayHasKey('quantity', $asArray); 45 | $this->assertArrayHasKey('index', $asArray); 46 | $this->assertArrayHasKey('item_category', $asArray); 47 | $this->assertIsNotArray($asArray['item_category']); 48 | } 49 | 50 | public function test_can_configure_arrayable() 51 | { 52 | $item = Item::new(); 53 | 54 | $item['item_id'] = '1'; 55 | $item['item_name'] = 'First Product'; 56 | $item['affiliation'] = 'unit test'; 57 | $item['coupon'] = 'code'; 58 | $item['currency'] = $this->prefill['currency']; 59 | $item['item_brand'] = 'phpunit'; 60 | $item['item_list_id'] = 'test-list'; 61 | $item['item_list_name'] = 'test list'; 62 | $item['item_variant'] = 'test'; 63 | $item['location_id'] = 'L1'; 64 | $item['discount'] = 1.11; 65 | $item['price'] = 7.39; 66 | $item['quantity'] = 2; 67 | $item['index'] = 1; 68 | $item['item_category'] = 'unit'; 69 | 70 | $this->assertArrayHasKey('item_id', $item); 71 | $this->assertArrayHasKey('item_name', $item); 72 | $this->assertArrayHasKey('affiliation', $item); 73 | $this->assertArrayHasKey('coupon', $item); 74 | $this->assertArrayHasKey('currency', $item); 75 | $this->assertArrayHasKey('item_brand', $item); 76 | $this->assertArrayHasKey('item_list_id', $item); 77 | $this->assertArrayHasKey('item_list_name', $item); 78 | $this->assertArrayHasKey('item_variant', $item); 79 | $this->assertArrayHasKey('location_id', $item); 80 | $this->assertArrayHasKey('discount', $item); 81 | $this->assertArrayHasKey('price', $item); 82 | $this->assertArrayHasKey('quantity', $item); 83 | $this->assertArrayHasKey('index', $item); 84 | $this->assertArrayHasKey('item_category', $item); 85 | $this->assertIsArray($item['item_category']); 86 | } 87 | 88 | public function test_can_export_to_array() 89 | { 90 | $this->assertInstanceOf(Item::class, $this->item); 91 | 92 | $arr = $this->item->toArray(); 93 | $this->assertIsArray($arr); 94 | $this->assertArrayHasKey('item_id', $arr); 95 | $this->assertArrayHasKey('item_name', $arr); 96 | $this->assertArrayHasKey('currency', $arr); 97 | $this->assertArrayHasKey('price', $arr); 98 | $this->assertArrayHasKey('quantity', $arr); 99 | } 100 | 101 | public function test_can_import_from_array() 102 | { 103 | $item = Item::fromArray([ 104 | 'item_id' => '2', 105 | 'item_name' => 'Second Product', 106 | 'currency' => $this->prefill['currency'], 107 | 'price' => 9.99, 108 | 'quantity' => 4, 109 | ]); 110 | 111 | $this->assertInstanceOf(Item::class, $item); 112 | 113 | $arr = $item->toArray(); 114 | $this->assertIsArray($arr); 115 | $this->assertArrayHasKey('item_id', $arr); 116 | $this->assertArrayHasKey('item_name', $arr); 117 | $this->assertArrayHasKey('currency', $arr); 118 | $this->assertArrayHasKey('price', $arr); 119 | $this->assertArrayHasKey('quantity', $arr); 120 | } 121 | 122 | public function test_can_convert_item_category_to_index_names() 123 | { 124 | $arr = Item::new() 125 | ->setItemId("123") 126 | ->addItemCategory("a") 127 | ->addItemCategory("b") 128 | ->addItemCategory("c") 129 | ->toArray(); 130 | 131 | $this->assertArrayHasKey('item_category', $arr); 132 | $this->assertArrayHasKey('item_category2', $arr); 133 | $this->assertArrayHasKey('item_category3', $arr); 134 | 135 | $this->assertArrayNotHasKey('item_category0', $arr); 136 | $this->assertArrayNotHasKey('item_category1', $arr); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/Unit/UserDataTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($uda->setEmail($setEmail = "test@gmail.com")); 15 | $this->assertTrue($uda->setPhone($setPhone = 4500000000)); 16 | $this->assertTrue($uda->setFirstName($setFirstName = "test")); 17 | $this->assertTrue($uda->setLastName($setLastName = "person")); 18 | $this->assertTrue($uda->setStreet($setStreet = "some street 11")); 19 | $this->assertTrue($uda->setCity($setCity = "somewhere")); 20 | $this->assertTrue($uda->setRegion($setRegion = "inthere")); 21 | $this->assertTrue($uda->setPostalCode($setPostalCode = "1234")); 22 | $this->assertTrue($uda->setCountry($setCountry = "DK")); 23 | 24 | $export = $uda->toArray(); 25 | $this->assertIsArray($export); 26 | $this->assertEquals(hash("sha256", $setEmail), $export["sha256_email_address"], $setEmail); 27 | $this->assertEquals(hash("sha256", '+' . $setPhone), $export["sha256_phone_number"], $setPhone); 28 | 29 | $this->assertArrayHasKey("address", $export); 30 | $this->assertIsArray($export["address"]); 31 | $this->assertEquals(hash("sha256", $setFirstName), $export["address"]["sha256_first_name"], $setFirstName); 32 | $this->assertEquals(hash("sha256", $setLastName), $export["address"]["sha256_last_name"], $setLastName); 33 | $this->assertEquals(hash("sha256", $setStreet), $export["address"]["sha256_street"], $setStreet); 34 | $this->assertEquals($setCity, $export["address"]["city"], $setCity); 35 | $this->assertEquals($setRegion, $export["address"]["region"], $setRegion); 36 | $this->assertEquals($setPostalCode, $export["address"]["postal_code"], $setPostalCode); 37 | $this->assertEquals($setCountry, $export["address"]["country"], $setCountry); 38 | } 39 | 40 | public function test_user_data_is_sendable() 41 | { 42 | $this->expectNotToPerformAssertions(); 43 | 44 | $uad = $this->analytics->userdata(); 45 | $uad->setEmail("test@gmail.com"); 46 | $uad->setPhone(4500000000); 47 | $uad->setFirstName("test"); 48 | $uad->setLastName("person"); 49 | $uad->setStreet("some street 11"); 50 | $uad->setCity("somewhere"); 51 | $uad->setRegion("inthere"); 52 | $uad->setPostalCode("1234"); 53 | $uad->setCountry("DK"); 54 | 55 | $this->analytics->addEvent(Login::new()); 56 | $this->analytics->post(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/Unit/UserPropertyTest.php: -------------------------------------------------------------------------------- 1 | setName($name = 'testname'); 16 | $userProperty->setValue($value = 'testvalue'); 17 | 18 | $export = $userProperty->toArray(); 19 | 20 | $this->assertArrayHasKey($name, $export); 21 | $this->assertArrayHasKey('value', $export[$name]); 22 | $this->assertEquals($value, $export[$name]['value']); 23 | } 24 | 25 | public function test_throws_on_reserved_name() 26 | { 27 | $userProperty = new UserProperty(); 28 | 29 | $this->expectException(Exception\Ga4UserPropertyException::class); 30 | $this->expectExceptionCode(Exception\Ga4Exception::PARAM_RESERVED); 31 | 32 | $userProperty->setName(UserProperty::RESERVED_NAMES[0]); 33 | } 34 | 35 | public function test_throws_on_too_long_name() 36 | { 37 | $userProperty = new UserProperty(); 38 | 39 | $this->expectException(Exception\Ga4UserPropertyException::class); 40 | $this->expectExceptionCode(Exception\Ga4Exception::PARAM_TOO_LONG); 41 | 42 | $tooLongName = ''; 43 | while (mb_strlen($tooLongName) <= 24) { 44 | $tooLongName .= range('a', 'z')[rand(0, 25)]; 45 | } 46 | 47 | $userProperty->setName($tooLongName); 48 | } 49 | } 50 | --------------------------------------------------------------------------------