diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f93c3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +.DS_Store +.idea/ +resources/ + +# Created by https://www.toptal.com/developers/gitignore/api/phpunit,phpstorm,composer +# Edit at https://www.toptal.com/developers/gitignore?templates=phpunit,phpstorm,composer + +### Composer ### +composer.phar +/vendor/ + +# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock + +### PhpStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PhpStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### PHPUnit ### +# Covers PHPUnit +# Reference: https://phpunit.de/ + +# Generated files +.phpunit.result.cache +.phpunit.cache + +# PHPUnit +/app/phpunit.xml +/phpunit.xml + +# Build data +/build/ + +# End of https://www.toptal.com/developers/gitignore/api/phpunit,phpstorm,composer \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..08b652a --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "statsig/statsig-core-php", + "type": "library", + "license": "ISC", + "description": "Statsig PHP SDK", + "autoload": { + "psr-4": { + "Statsig\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Statsig\\Tests\\": "tests/" + } + }, + "require": {}, + "require-dev": { + "phpunit/phpunit": "^10", + "donatj/mock-webserver": "^2.7.2", + "ext-zlib": "*" + }, + "scripts": { + "test": "phpunit tests --testdox --colors=always", + "test:verbose": "phpunit tests --testdox --colors=always --debug", + "post-install-cmd": [ + "php post-install.php" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..8b0c5b2 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1765 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "885462739bfab37dbfef9895f5dc7fff", + "packages": [], + "packages-dev": [ + { + "name": "donatj/mock-webserver", + "version": "v2.7.2", + "source": { + "type": "git", + "url": "https://github.com/donatj/mock-webserver.git", + "reference": "1887dcbd8b4a52dde23a43f0d303c4509a065e83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/donatj/mock-webserver/zipball/1887dcbd8b4a52dde23a43f0d303c4509a065e83", + "reference": "1887dcbd8b4a52dde23a43f0d303c4509a065e83", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-sockets": "*", + "php": ">=7.1", + "ralouphie/getallheaders": "~2.0 || ~3.0" + }, + "require-dev": { + "corpus/coding-standard": "^0.6.0 || ^0.8.0", + "donatj/drop": "^1.0", + "ext-curl": "*", + "friendsofphp/php-cs-fixer": "^3.1", + "phpunit/phpunit": "~7|~9", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "donatj\\MockWebServer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jesse G. Donat", + "email": "donatj@gmail.com", + "homepage": "https://donatstudios.com", + "role": "Lead" + } + ], + "description": "Simple mock web server for unit testing", + "keywords": [ + "dev", + "http", + "mock", + "phpunit", + "testing", + "unit testing", + "webserver" + ], + "support": { + "issues": "https://github.com/donatj/mock-webserver/issues", + "source": "https://github.com/donatj/mock-webserver/tree/v2.7.2" + }, + "funding": [ + { + "url": "https://www.paypal.me/donatj/15", + "type": "custom" + }, + { + "url": "https://github.com/donatj", + "type": "github" + }, + { + "url": "https://ko-fi.com/donatj", + "type": "ko_fi" + } + ], + "time": "2024-01-22T20:41:09+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-11-08T17:47:46+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.3.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + }, + "time": "2024-10-08T18:51:32+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.39", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "4e89eff200b801db58f3d580ad7426431949eaa9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4e89eff200b801db58f3d580ad7426431949eaa9", + "reference": "4e89eff200b801db58f3d580ad7426431949eaa9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.39" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-12-11T10:51:07+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-18T14:56:07+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:17:12+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": { + "ext-zlib": "*" + }, + "plugin-api-version": "2.6.0" +} diff --git a/post-install.php b/post-install.php new file mode 100644 index 0000000..2c2e195 --- /dev/null +++ b/post-install.php @@ -0,0 +1,124 @@ + "statsig-ffi-x86_64-apple-darwin.zip", + "macos-aarch64" => "statsig-ffi-aarch64-apple-darwin.zip", + "linux-aarch64" => "statsig-ffi-aarch64-unknown-linux-gnu.zip", + "linux-x86_64" => "statsig-ffi-x86_64-unknown-linux-gnu.zip", + "windows-x86_64" => "statsig-ffi-x86_64-pc-windows-msvc.zip", + "windows-aarch64" => "statsig-ffi-aarch64-pc-windows-msvc.zip", + ]; + + $binary_file = $binary_map[$system_info[0] . "-" . $system_info[1]] ?? null; + + if ($binary_file === null) { + echo "No binary found for: {$system_info[0]} {$system_info[1]}\n"; + exit(1); + } + + $url = "https://github.com/statsig-io/statsig-core-php/releases/download/" . VERSION . "/" . $binary_file; + + echo "Downloading binary from $url\n"; + + $output_path = OUTPUT_DIR . "/" . $binary_file; + + file_put_contents($output_path, file_get_contents($url)); + + return $output_path; +} + +function unzip_binary($zip_file_path) +{ + echo "Unzipping binary\n"; + $zip = new ZipArchive(); + if ($zip->open($zip_file_path) === TRUE) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $filename = $zip->getNameIndex($i); + if (in_array($filename, ['libstatsig_ffi.dylib', 'statsig_ffi.dll', 'libstatsig_ffi.so'])) { + $zip->extractTo(OUTPUT_DIR, $filename); + echo "Extracted $filename\n"; + } + } + $zip->close(); + echo "Binary unzipped to " . OUTPUT_DIR . "\n"; + } else { + echo "Failed to open zip file\n"; + exit(1); + } + + echo "Binary unzipped to " . OUTPUT_DIR . "\n"; + + echo "Deleting zip file\n"; + if (unlink($zip_file_path)) { + echo "Successfully deleted $zip_file_path\n"; + } else { + echo "Failed to delete $zip_file_path\n"; + } +} + +function download_header() +{ + $url = "https://github.com/statsig-io/statsig-core-php/releases/download/" . VERSION . "/statsig_ffi.h"; + echo "Downloading header from $url\n"; + + $output_path = OUTPUT_DIR . "/statsig_ffi.h"; + file_put_contents($output_path, file_get_contents($url)); +} + + +$system_info = get_system_info(); +ensure_bin_dir_exists(); +$zip_file_path = download_binary($system_info); +unzip_binary($zip_file_path); +download_header(); diff --git a/src/EvaluationTypes/BaseEvaluation.php b/src/EvaluationTypes/BaseEvaluation.php new file mode 100644 index 0000000..c44a8e0 --- /dev/null +++ b/src/EvaluationTypes/BaseEvaluation.php @@ -0,0 +1,53 @@ +__raw_result = $raw_result; + + if (!is_array($result)) { + $this->error = 'Invalid JSON input'; + $result = []; + } else { + $this->error = null; + } + + $this->__evaluation = $result['__evaluation'] ?? []; + + $this->details = $result['details'] ?? []; + $this->id_type = (string)($result['id_type'] ?? ''); + $this->name = (string)($result['name'] ?? ''); + $this->rule_id = (string)($result['rule_id'] ?? ''); + } + + protected function getValueImpl(array $value, string $param_name, $fallback, $exposure_func) + { + if (!array_key_exists($param_name, $value)) { + return $fallback; + } + + $val = $value[$param_name]; + + if ($fallback !== null && gettype($val) !== gettype($fallback)) { + return $fallback; + } + + if ($exposure_func !== null) { + $exposure_func($param_name); + } + + return $val; + } +} diff --git a/src/EvaluationTypes/DynamicConfig.php b/src/EvaluationTypes/DynamicConfig.php new file mode 100644 index 0000000..c2a3658 --- /dev/null +++ b/src/EvaluationTypes/DynamicConfig.php @@ -0,0 +1,20 @@ +value = $result['value'] ?? []; + } + + public function get(string $param_name, $fallback) + { + return $this->getValueImpl($this->value, $param_name, $fallback, null); + } +} diff --git a/src/EvaluationTypes/Experiment.php b/src/EvaluationTypes/Experiment.php new file mode 100644 index 0000000..98a3bd6 --- /dev/null +++ b/src/EvaluationTypes/Experiment.php @@ -0,0 +1,22 @@ +value = $result['value'] ?? []; + $this->groupName = $result['group_name'] ?? null; + } + + public function get(string $param_name, $fallback) + { + return $this->getValueImpl($this->value, $param_name, $fallback, null); + } +} diff --git a/src/EvaluationTypes/FeatureGate.php b/src/EvaluationTypes/FeatureGate.php new file mode 100644 index 0000000..624956e --- /dev/null +++ b/src/EvaluationTypes/FeatureGate.php @@ -0,0 +1,15 @@ +value = (bool)($result['value'] ?? false); + } +} diff --git a/src/EvaluationTypes/Layer.php b/src/EvaluationTypes/Layer.php new file mode 100644 index 0000000..641f478 --- /dev/null +++ b/src/EvaluationTypes/Layer.php @@ -0,0 +1,40 @@ +__statsig_ref = $__statsig_ref; + $this->__value = $result['__value'] ?? []; + $this->groupName = $result['group_name'] ?? null; + $this->allocatedExperimentName = $result['allocated_experiment_name'] ?? null; + } + + public function get(string $param_name, $fallback) + { + return $this->getValueImpl( + $this->__value, + $param_name, + $fallback, + function ($param_name) { + $this->logParameterExposure($param_name); + } + ); + } + + private function logParameterExposure(string $param_name): void + { + StatsigFFI::get()->statsig_log_layer_param_exposure($this->__statsig_ref, $this->__raw_result, $param_name); + } +} diff --git a/src/Statsig.php b/src/Statsig.php new file mode 100644 index 0000000..0045e12 --- /dev/null +++ b/src/Statsig.php @@ -0,0 +1,126 @@ +__ref : (new StatsigOptions)->__ref; + + $ffi = StatsigFFI::get(); + $this->__ref = $ffi->statsig_create($sdk_key, $options_ref); + } + + public function __destruct() + { + if (is_null($this->__ref)) { + return; + } + + $this->flushEvents(); + StatsigFFI::get()->statsig_release($this->__ref); + $this->__ref = null; + } + + public function initialize(): void + { + StatsigFFI::get()->statsig_initialize_blocking($this->__ref); + } + + public function flushEvents(): void + { + StatsigFFI::get()->statsig_flush_events_blocking($this->__ref); + } + + public function logEvent( + StatsigEventData $event_data, + StatsigUser $user, + ): void { + $data = json_encode($event_data); + StatsigFFI::get()->statsig_log_event($this->__ref, $user->__ref, $data); + } + + public function getClientInitializeResponse(StatsigUser $user, ?array $options = null): string + { + return StatsigFFI::get()->statsig_get_client_init_response($this->__ref, $user->__ref, encode_or_null($options)); + } + + /** + * Feature Gate Functions + */ + + public function checkGate(StatsigUser $user, string $name, ?array $options = null): bool + { + return StatsigFFI::get()->statsig_check_gate($this->__ref, $user->__ref, $name, encode_or_null($options)); + } + + public function getFeatureGate(StatsigUser $user, string $name, ?array $options = null): FeatureGate + { + $raw_result = StatsigFFI::get()->statsig_get_feature_gate($this->__ref, $user->__ref, $name, encode_or_null($options)); + return new FeatureGate($raw_result); + } + + public function manuallyLogGateExposure(StatsigUser $user, string $name): void + { + StatsigFFI::get()->statsig_manually_log_gate_exposure($this->__ref, $user->__ref, $name); + } + + /** + * Dynamic Config Functions + */ + + public function getDynamicConfig(StatsigUser $user, string $name, ?array $options = null): DynamicConfig + { + $raw_result = StatsigFFI::get()->statsig_get_dynamic_config($this->__ref, $user->__ref, $name, encode_or_null($options)); + return new DynamicConfig($raw_result); + } + + public function manuallyLogDynamicConfigExposure(StatsigUser $user, string $name): void + { + StatsigFFI::get()->statsig_manually_log_dynamic_config_exposure($this->__ref, $user->__ref, $name); + } + + /** + * Experiment Functions + */ + + public function getExperiment(StatsigUser $user, string $name, ?array $options = null): Experiment + { + $raw_result = StatsigFFI::get()->statsig_get_experiment($this->__ref, $user->__ref, $name, encode_or_null($options)); + return new Experiment($raw_result); + } + + public function manuallyLogExperimentExposure(StatsigUser $user, string $name): void + { + StatsigFFI::get()->statsig_manually_log_experiment_exposure($this->__ref, $user->__ref, $name); + } + + /** + * Layer Functions + */ + + public function getLayer(StatsigUser $user, string $name, ?array $options = null): Layer + { + $raw_result = StatsigFFI::get()->statsig_get_layer($this->__ref, $user->__ref, $name, encode_or_null($options)); + return new Layer($raw_result, $this->__ref); + } + + public function manuallyLogLayerParameterExposure(StatsigUser $user, string $layer_name, string $param_name): void + { + StatsigFFI::get()->statsig_manually_log_layer_parameter_exposure($this->__ref, $user->__ref, $layer_name, $param_name); + } +} + +function encode_or_null(?array $options): ?string +{ + return is_null($options) ? null : json_encode($options); +} diff --git a/src/StatsigEventData.php b/src/StatsigEventData.php new file mode 100644 index 0000000..9ca3717 --- /dev/null +++ b/src/StatsigEventData.php @@ -0,0 +1,17 @@ +name = $name; + $this->value = $value; + $this->metadata = $metadata; + } +} diff --git a/src/StatsigFFI.php b/src/StatsigFFI.php new file mode 100644 index 0000000..067d2da --- /dev/null +++ b/src/StatsigFFI.php @@ -0,0 +1,95 @@ + 'libstatsig_ffi.dylib', + 'Windows' => 'statsig_ffi.dll', + default => 'libstatsig_ffi.so', + }; + + $path = $dir . '/' . $file_name; + + if (file_exists($path)) { + return $path; + } + + return null; + } + + private static function find_header_file_in_dir(string $dir): ?string + { + $path = $dir . '/statsig_ffi.h'; + + if (file_exists($path)) { + return $path; + } + + return null; + } + + private static function update_statsig_metadata(FFI $ffi): void + { + $os = PHP_OS_FAMILY; + $arch = php_uname('m'); + $php_version = PHP_VERSION; + + $ffi->statsig_metadata_update_values("statsig-server-core-php", $os, $arch, $php_version); + } +} diff --git a/src/StatsigLocalFileEventLoggingAdapter.php b/src/StatsigLocalFileEventLoggingAdapter.php new file mode 100644 index 0000000..327d424 --- /dev/null +++ b/src/StatsigLocalFileEventLoggingAdapter.php @@ -0,0 +1,34 @@ +__ref = StatsigFFI::get()->statsig_local_file_event_logging_adapter_create( + $sdk_key, + $output_directory, + $log_event_url + ); + } + + public function __destruct() + { + if (!is_null($this->__ref)) { + StatsigFFI::get()->statsig_local_file_event_logging_adapter_release($this->__ref); + } + + $this->__ref = null; + } + + public function sendPendingEvents(): void + { + StatsigFFI::get()->statsig_local_file_event_logging_adapter_send_pending_events($this->__ref); + } +} diff --git a/src/StatsigLocalFileSpecsAdapter.php b/src/StatsigLocalFileSpecsAdapter.php new file mode 100644 index 0000000..b16c214 --- /dev/null +++ b/src/StatsigLocalFileSpecsAdapter.php @@ -0,0 +1,31 @@ +__ref = StatsigFFI::get()->statsig_local_file_specs_adapter_create($sdk_key, $output_directory, $specs_url, $fallback_to_statsig_api); + } + + public function __destruct() + { + if (!is_null($this->__ref)) { + StatsigFFI::get()->statsig_local_file_specs_adapter_release($this->__ref); + } + + $this->__ref = null; + } + + public function syncSpecsFromNetwork(): void + { + StatsigFFI::get()->statsig_local_file_specs_adapter_fetch_and_write_to_file($this->__ref); + } +} diff --git a/src/StatsigOptions.php b/src/StatsigOptions.php new file mode 100644 index 0000000..307cc52 --- /dev/null +++ b/src/StatsigOptions.php @@ -0,0 +1,41 @@ +__ref = $ffi->statsig_options_create( + $specs_url, + $log_event_url, + is_null($specs_adapter) ? null : $specs_adapter->__ref, + is_null($event_logging_adapter) ? null : $event_logging_adapter->__ref, + $environment, + $event_logging_flush_interval_ms ?? -1, + $event_logging_max_queue_size ?? -1, + $specs_sync_interval_ms ?? -1, + ); + } + + public function __destruct() + { + if (is_null($this->__ref)) { + return; + } + + StatsigFFI::get()->statsig_options_release($this->__ref); + $this->__ref = null; + } +} diff --git a/src/StatsigUser.php b/src/StatsigUser.php new file mode 100644 index 0000000..07984a8 --- /dev/null +++ b/src/StatsigUser.php @@ -0,0 +1,45 @@ +__ref = $ffi->statsig_user_create( + $user_id, + json_encode($custom_ids), + $email, + $ip, + $user_agent, + $country, + $locale, + $app_version, + json_encode($custom), + json_encode($private_attributes), + ); + } + + public function __destruct() + { + if (is_null($this->__ref)) { + return; + } + + StatsigFFI::get()->statsig_user_release($this->__ref); + $this->__ref = null; + } +} diff --git a/src/StatsigUserBuilder.php b/src/StatsigUserBuilder.php new file mode 100644 index 0000000..bc9a393 --- /dev/null +++ b/src/StatsigUserBuilder.php @@ -0,0 +1,97 @@ +user_id = $user_id; + $this->custom_ids = $custom_ids; + } + + public function withEmail(string $email): StatsigUserBuilder + { + $this->email = $email; + return $this; + } + + public function withIP(string $ip): StatsigUserBuilder + { + $this->ip = $ip; + return $this; + } + + public function withUserAgent(string $user_agent): StatsigUserBuilder + { + $this->user_agent = $user_agent; + return $this; + } + + public function withCountry(string $country): StatsigUserBuilder + { + $this->country = $country; + return $this; + } + + public function withLocale(string $locale): StatsigUserBuilder + { + $this->locale = $locale; + return $this; + } + + public function withAppVersion(string $app_version): StatsigUserBuilder + { + $this->app_version = $app_version; + return $this; + } + + public function withCustom(array $custom): StatsigUserBuilder + { + $this->custom = $custom; + return $this; + } + + public function withPrivateAttributes(array $private_attributes): StatsigUserBuilder + { + $this->private_attributes = $private_attributes; + return $this; + } + + public function build(): StatsigUser + { + return new StatsigUser( + $this->user_id ?? "", + $this->custom_ids ?? [], + $this->email, + $this->ip, + $this->user_agent, + $this->country, + $this->locale, + $this->app_version, + $this->custom, + $this->private_attributes + ); + } +} diff --git a/tests/MockServer.php b/tests/MockServer.php new file mode 100644 index 0000000..291a2cc --- /dev/null +++ b/tests/MockServer.php @@ -0,0 +1,97 @@ +server = new MockWebServer; + $this->server->setDefaultResponse(new NotFoundResponse); + + $this->server->start(); + } + + public function stop(): void + { + $this->server->stop(); + } + + public function getUrl(): string + { + return $this->server->getServerRoot(); + } + + public function mock($path, $response, $options = []): void + { + $status = $options['status'] ?? 200; + + $this->server->setResponseOfPath( + $path, + new Response( + $response, + ['Cache-Control' => 'no-cache'], + $status + ) + ); + } + + public function getRequests(): array + { + $requests = []; + + for ($i = 0; $i < 999; $i++) { + $request = $this->server->getRequestByOffset($i); + if ($request) { + $requests[] = [ + 'uri' => $request->getRequestUri(), + 'method' => $request->getRequestMethod(), + 'params' => $request->getGet(), + 'body' => $request->getInput(), + 'headers' => $request->getHeaders(), + 'path' => $request->getParsedUri()['path'] + ]; + } else { + break; + } + } + + return $requests; + } + + public function getLoggedEvents(bool $include_diagnostics = false): array + { + $requests = $this->getRequests(); + + $events = []; + foreach ($requests as $request) { + if ($request['path'] !== '/v1/log_event' && $request['path'] !== '/v1/rgstr') { + continue; + } + + $bytes = $request['body']; + $json = gzdecode($bytes); + $body = json_decode($json, true); + + if ($include_diagnostics) { + $events = array_merge($events, $body['events']); + } else { + foreach ($body['events'] as $event) { + if ($event['eventName'] !== 'statsig::diagnostics') { + $events[] = $event; + } + } + } + } + + return $events; + } +} diff --git a/tests/MockServerTest.php b/tests/MockServerTest.php new file mode 100644 index 0000000..b7ec0cb --- /dev/null +++ b/tests/MockServerTest.php @@ -0,0 +1,52 @@ +server = new MockServer(); + $this->server->mock('/foo', 'RESULT'); + } + + protected function tearDown(): void + { + $this->server->stop(); + } + + public function testGetRequest() + { + file_get_contents($this->server->getUrl() . '/foo?buzz=1'); + + $request = $this->server->getRequests()[0]; + $this->assertEquals('/foo', $request['path']); + $this->assertEquals('GET', $request['method']); + $this->assertEquals(['buzz' => '1'], $request['params']); + } + + public function testPostRequest() + { + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json', + 'content' => json_encode(['buzz' => '1']) + ] + ]); + file_get_contents($this->server->getUrl() . '/foo', false, $context); + + $request = $this->server->getRequests()[0]; + $this->assertEquals('/foo', $request['path']); + $this->assertEquals('POST', $request['method']); + $this->assertEquals('{"buzz":"1"}', $request['body']); + $this->assertArrayHasKey('Content-Type', $request['headers']); + $this->assertEquals('application/json', $request['headers']['Content-Type']); + } +} diff --git a/tests/StatsigAdapterUsageTest.php b/tests/StatsigAdapterUsageTest.php new file mode 100644 index 0000000..1e53d97 --- /dev/null +++ b/tests/StatsigAdapterUsageTest.php @@ -0,0 +1,76 @@ +server = new MockServer(); + $this->server->mock('/v2/download_config_specs/secret-key.json', $data); + $this->server->mock('/v2/download_config_specs/server-event-logging-usage-test.json', $data); + } + + public function testLocalFileSpecsAdapterUsage() + { + $adapter = new StatsigLocalFileSpecsAdapter( + "secret-key", + "/tmp", + $this->server->getUrl() . "/v2/download_config_specs" + ); + $adapter->syncSpecsFromNetwork(); + + $options = new StatsigOptions( + null, + null, + $adapter + ); + + $statsig = new Statsig("secret-key", $options); + $statsig->initialize(); + + $user = new StatsigUser("a-user"); + $gate = $statsig->getFeatureGate($user, "test_50_50"); + $this->assertTrue($gate->value); + } + + public function testEventLogging() + { + $sdk_key = "server-event-logging-usage-test"; + $specs_adapter = new StatsigLocalFileSpecsAdapter( + $sdk_key, + "/tmp", + $this->server->getUrl() . "/v2/download_config_specs" + ); + + $specs_adapter->syncSpecsFromNetwork(); + + $event_adapter = new StatsigLocalFileEventLoggingAdapter($sdk_key, "/tmp"); + + $statsig = new Statsig($sdk_key, new StatsigOptions(null, null, $specs_adapter, $event_adapter)); + + $statsig->initialize(); + + $this->assertTrue(true); + } +} diff --git a/tests/StatsigLocalFileSpecsAdapterTest.php b/tests/StatsigLocalFileSpecsAdapterTest.php new file mode 100644 index 0000000..e711abd --- /dev/null +++ b/tests/StatsigLocalFileSpecsAdapterTest.php @@ -0,0 +1,60 @@ +server = new MockServer(); + $this->server->mock('/v2/download_config_specs/'.self::SDK_KEY.'.json', $data); + } + + public function testCreateAndRelease() + { + $adapter = new StatsigLocalFileSpecsAdapter(self::SDK_KEY, "/tmp"); + $this->assertNotNull($adapter->__ref); + + $adapter->__destruct(); + + $this->assertNull($adapter->__ref); + } + + public function testFetchingFromNetwork() + { + $adapter = new StatsigLocalFileSpecsAdapter( + self::SDK_KEY, + "/tmp", + $this->server->getUrl() . "/v2/download_config_specs" + ); + + $adapter->syncSpecsFromNetwork(); + + $json = json_decode(file_get_contents("/tmp/".self::FILE_NAME), true); + $this->assertArrayHasKey("dynamic_configs", $json); + $this->assertArrayHasKey("layer_configs", $json); + $this->assertArrayHasKey("feature_gates", $json); + } +} diff --git a/tests/StatsigOptionsTest.php b/tests/StatsigOptionsTest.php new file mode 100644 index 0000000..b8fe185 --- /dev/null +++ b/tests/StatsigOptionsTest.php @@ -0,0 +1,21 @@ +assertNotNull($options->__ref); + + $options->__destruct(); + + $this->assertNull($options->__ref); + } +} diff --git a/tests/StatsigRepeatedUsageTest.php b/tests/StatsigRepeatedUsageTest.php new file mode 100644 index 0000000..098bbfb --- /dev/null +++ b/tests/StatsigRepeatedUsageTest.php @@ -0,0 +1,79 @@ +server = new MockServer(); + $this->server->mock('/v2/download_config_specs/' . SDK_KEY . '.json', $data); + $this->server->mock('/v1/log_event', '{ "success": true }', ['status' => 202]); + + TestHelpers::ensureEmptyDir(TEST_DIR); + + $adapter = new StatsigLocalFileSpecsAdapter(SDK_KEY, TEST_DIR, $this->server->getUrl() . "/v2/download_config_specs"); + $adapter->syncSpecsFromNetwork(); + } + + protected function tearDown(): void + { + $this->server->stop(); + } + + protected function getNewStatsigInstance(): Statsig + { + $options = new StatsigOptions( + null, + null, + new StatsigLocalFileSpecsAdapter(SDK_KEY, TEST_DIR, $this->server->getUrl() . "/v2/download_config_specs"), + new StatsigLocalFileEventLoggingAdapter(SDK_KEY, TEST_DIR, $this->server->getUrl() . "/v1/log_event") + ); + + $statsig = new Statsig(SDK_KEY, $options); + $statsig->initialize(); + return $statsig; + } + + public function testRepeatedUsage() + { + $iterations = 10; + for ($i = 0; $i < $iterations; $i++) { + $statsig = $this->getNewStatsigInstance(); + + $user = StatsigUserBuilder::withUserID("user-" . $i)->build(); + + $statsig->checkGate($user, "test_public"); + unset($statsig); + } + + $logging_adapter = new StatsigLocalFileEventLoggingAdapter(SDK_KEY, TEST_DIR, $this->server->getUrl() . "/v1/log_event"); + $logging_adapter->sendPendingEvents(); + + // logged each exposure + $this->assertCount($iterations, $this->server->getLoggedEvents()); + + // did not include a diagnostics event with each exposure + $this->assertLessThan($iterations * 2, count($this->server->getLoggedEvents(true))); + } +} diff --git a/tests/StatsigScheduledEventLoggingAdapterTest.php b/tests/StatsigScheduledEventLoggingAdapterTest.php new file mode 100644 index 0000000..7fd5867 --- /dev/null +++ b/tests/StatsigScheduledEventLoggingAdapterTest.php @@ -0,0 +1,65 @@ +server = new MockServer(); + $this->server->mock('/v1/log_event', '{"success": true}'); + } + + public function testCreateAndRelease() + { + $adapter = new StatsigLocalFileEventLoggingAdapter(self::SDK_KEY, "/tmp"); + $this->assertNotNull($adapter->__ref); + + $adapter->__destruct(); + + $this->assertNull($adapter->__ref); + } + + public function testSendingEvents() + { + $request_json = json_encode([[ + "eventName" => "foo", + "metadata" => [ "key" => "value" ], + "secondaryExposures" => null, + "time" => 1734476293616, + "user" => ["statsigEnvironment" => null, "userID" => "a-user"], + "value" => "bar" + ]]); + + file_put_contents(self::FILE_PATH, $request_json); + + $adapter = new StatsigLocalFileEventLoggingAdapter( + self::SDK_KEY, + "/tmp", + $this->server->getUrl() . "/v1/log_event" + ); + + $adapter->sendPendingEvents(); + + $request = $this->server->getRequests()[0]; + $this->assertEquals('/v1/log_event', $request['path']); + } +} diff --git a/tests/StatsigTest.php b/tests/StatsigTest.php new file mode 100644 index 0000000..8daee9a --- /dev/null +++ b/tests/StatsigTest.php @@ -0,0 +1,197 @@ +user = new StatsigUser('a-user'); + + $dir = dirname(__FILE__); + $data = file_get_contents($dir . '/../../statsig-lib/tests/data/eval_proj_dcs.json'); + + $this->server = new MockServer(); + $this->server->mock('/v2/download_config_specs/secret-key.json', $data); + $this->server->mock('/v1/log_event', '{ "success": true }', ['status' => 202]); + } + + + protected function tearDown(): void + { + $this->server->stop(); + } + + protected function getInitializedStatsig(): Statsig + { + $options = new StatsigOptions( + $this->server->getUrl() . '/v2/download_config_specs', + $this->server->getUrl() . '/v1/log_event' + ); + $statsig = new Statsig('secret-key', $options); + + $statsig->initialize(); + + return $statsig; + } + + public function testCreateAndRelease() + { + $statsig = new Statsig('secret-key'); + $this->assertNotNull($statsig->__ref); + + $statsig->__destruct(); + + $this->assertNull($statsig->__ref); + } + + public function testDoubleRelease() + { + $statsig = new Statsig('secret-key'); + $statsig->__destruct(); + $statsig->__destruct(); + + $this->assertNull($statsig->__ref); + } + + public function testInitialization() + { + $statsig = $this->getInitializedStatsig(); + $this->assertEquals(Statsig::class, get_class($statsig)); + + $request = $this->server->getRequests()[0]; + $this->assertEquals('/v2/download_config_specs/secret-key.json', $request['path']); + } + + public function testCheckGate() + { + $statsig = $this->getInitializedStatsig(); + $this->assertTrue($statsig->checkGate($this->user, 'test_public')); + } + + public function testGetFeatureGate() + { + $statsig = $this->getInitializedStatsig(); + + $gate = $statsig->getFeatureGate($this->user, 'test_50_50'); + $this->assertTrue($gate->value); + } + + public function testGetDynamicConfig() + { + $statsig = $this->getInitializedStatsig(); + + $config = $statsig->getDynamicConfig($this->user, 'test_email_config'); + $this->assertEquals('everyone else', $config->get('header_text', 'err')); + } + + public function testGetExperiment() + { + $statsig = $this->getInitializedStatsig(); + + $experiment = $statsig->getExperiment($this->user, 'exp_with_obj_and_array'); + $this->assertEquals(['group' => 'test'], $experiment->get('obj_param', ['fallback' => ''])); + } + + public function testGetLayer() + { + $statsig = $this->getInitializedStatsig(); + + $layer = $statsig->getLayer($this->user, 'layer_with_many_params'); + $this->assertEquals('layer', $layer->get('a_string', 'err')); + } + + public function testExposureLogCounts() + { + $statsig = $this->getInitializedStatsig(); + + $statsig->getFeatureGate($this->user, 'test_50_50'); + $statsig->getDynamicConfig($this->user, 'test_email_config'); + $statsig->getExperiment($this->user, 'exp_with_obj_and_array'); + $statsig->getLayer($this->user, 'layer_with_many_params')->get('a_string', ''); + + $statsig->flushEvents(); + + $request = $this->server->getRequests()[1]; + $this->assertEquals('/v1/log_event', $request['path']); + + $bytes = $request['body']; + $json = gzdecode($bytes); + $body = json_decode($json, true); + $events = array_filter($body['events'], function ($event) { + return $event['eventName'] !== 'statsig::diagnostics'; + }); + $this->assertCount(4, $events); + } + + public function testEventFormat() + { + $statsig = $this->getInitializedStatsig(); + + $statsig->getFeatureGate($this->user, 'test_50_50'); + $statsig->flushEvents(); + + $request = $this->server->getRequests()[1]; + + $bytes = $request['body']; + $json = gzdecode($bytes); + $body = json_decode($json, true); + + $events = $this->server->getLoggedEvents(); + + // Event + [ + 'eventName' => $event_name, + 'time' => $time, + 'user' => $user, + 'value' => $value, + 'metadata' => $metadata + ] = $events[0]; + $this->assertEquals('statsig::gate_exposure', $event_name); + $this->assertIsNumeric($time); + $this->assertEquals('a-user', $user['userID']); + $this->assertEquals('true', $metadata['gateValue']); + $this->assertEquals('test_50_50', $metadata['gate']); + $this->assertEquals('Network:Recognized', $metadata['reason']); + + // Statsig Metadata + [ + 'sdkType' => $sdk_type, + 'sdkVersion' => $sdk_version, + 'sessionID' => $session_id, + 'os' => $os, + 'arch' => $arch, + 'languageVersion' => $lang_version + ] = $body['statsigMetadata']; + $this->assertEquals('statsig-server-core-php', $sdk_type); + $this->assertIsString($sdk_version); + $this->assertMatchesRegularExpression('/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/', $session_id); + $this->assertIsString($os); + $this->assertIsString($arch); + $this->assertIsString($lang_version); + } + + public function testGcir() + { + $statsig = $this->getInitializedStatsig(); + + $raw_result = $statsig->getClientInitializeResponse($this->user); + $result = json_decode($raw_result, true); + + $this->assertCount(62, $result['dynamic_configs']); + $this->assertCount(65, $result['feature_gates']); + $this->assertCount(12, $result['layer_configs']); + } +} diff --git a/tests/StatsigUserBuilderTest.php b/tests/StatsigUserBuilderTest.php new file mode 100644 index 0000000..b11964e --- /dev/null +++ b/tests/StatsigUserBuilderTest.php @@ -0,0 +1,93 @@ +server = new MockServer(); + $this->server->mock('/v2/download_config_specs/secret-key.json', $data); + $this->server->mock('/v1/log_event', '{ "success": true }', ['status' => 202]); + + $options = new StatsigOptions( + $this->server->getUrl() . "/v2/download_config_specs", + $this->server->getUrl() . "/v1/log_event" + ); + + $this->statsig = new Statsig("secret-key", $options); + $this->statsig->initialize(); + } + + protected function tearDown(): void + { + $this->server->stop(); + } + + + public function testBuildingFullUser() + { + $user = StatsigUserBuilder::withUserID("a-user") + ->withEmail("a-user@example.com") + ->withIP("127.0.0.1") + ->withUserAgent("Mozilla/5.0") + ->withCountry("US") + ->withLocale("en_US") + ->withAppVersion("1.0.0") + ->withCustom(["custom" => "value"]) + ->withPrivateAttributes(["private" => "value"]) + ->build(); + + $this->assertTrue($this->statsig->checkGate($user, "test_public")); + $this->statsig->flushEvents(); + + $logged_user = $this->server->getLoggedEvents()[0]['user']; + + $this->assertEquals("a-user", $logged_user['userID']); + $this->assertEquals("a-user@example.com", $logged_user['email']); + $this->assertEquals("127.0.0.1", $logged_user['ip']); + $this->assertEquals("Mozilla/5.0", $logged_user['userAgent']); + $this->assertEquals("US", $logged_user['country']); + $this->assertEquals("en_US", $logged_user['locale']); + $this->assertEquals("1.0.0", $logged_user['appVersion']); + $this->assertEquals(["custom" => "value"], $logged_user['custom']); + $this->assertArrayNotHasKey('privateAttributes', $logged_user); + } + + public function testBuildingPartialUser() + { + $user = StatsigUserBuilder::withCustomIDs(["employeeID" => "an_employee"]) + ->build(); + + $this->assertTrue($this->statsig->checkGate($user, "test_public")); + $this->statsig->flushEvents(); + + $logged_user = $this->server->getLoggedEvents()[0]['user']; + + $this->assertEquals(["employeeID" => "an_employee"], $logged_user['customIDs']); + + $this->assertArrayNotHasKey('email', $logged_user); + $this->assertArrayNotHasKey('ip', $logged_user); + $this->assertArrayNotHasKey('userAgent', $logged_user); + $this->assertArrayNotHasKey('country', $logged_user); + $this->assertArrayNotHasKey('locale', $logged_user); + $this->assertArrayNotHasKey('appVersion', $logged_user); + $this->assertArrayNotHasKey('custom', $logged_user); + $this->assertArrayNotHasKey('privateAttributes', $logged_user); + } +} diff --git a/tests/TestHelpers.php b/tests/TestHelpers.php new file mode 100644 index 0000000..645d5b7 --- /dev/null +++ b/tests/TestHelpers.php @@ -0,0 +1,32 @@ +fail("Timed out waiting for callback"); + } + } + + public static function ensureEmptyDir($dir): void + { + if (is_dir($dir)) { + $files = glob($dir . '/*'); + foreach ($files as $file) { + if (is_file($file)) { + unlink($file); + } + } + rmdir($dir); + } + mkdir($dir); + } +}