diff --git a/.github/workflows/php-ci.yaml b/.github/workflows/php-ci.yaml
index 109afb1f..daefd690 100644
--- a/.github/workflows/php-ci.yaml
+++ b/.github/workflows/php-ci.yaml
@@ -14,13 +14,14 @@ jobs:
operating-system: [ubuntu-latest]
php-version: ['8.0', '8.1', '8.2']
project-dir:
- - hooks/OpenTelemetry
- hooks/DDTrace
+ - hooks/OpenTelemetry
- hooks/Validators
+ # - providers/CloudBees
- providers/Flagd
- - providers/Split
+ - providers/Flagsmith
- providers/GoFeatureFlag
- # - providers/CloudBees
+ - providers/Split
fail-fast: false
# todo exclude some matrix combinations based on php version requirements
diff --git a/.github/workflows/split_monorepo.yaml b/.github/workflows/split_monorepo.yaml
index 56230cf5..f1d4b291 100644
--- a/.github/workflows/split_monorepo.yaml
+++ b/.github/workflows/split_monorepo.yaml
@@ -24,6 +24,7 @@ jobs:
- [hook, validators, Validators]
- [provider, cloudbees, CloudBees]
- [provider, flagd, Flagd]
+ - [provider, flagsmith, Flagsmith]
- [provider, split, Split]
- [provider, go-feature-flag, GoFeatureFlag]
steps:
diff --git a/providers/Flagsmith/.gitattributes b/providers/Flagsmith/.gitattributes
new file mode 100644
index 00000000..a8073ebb
--- /dev/null
+++ b/providers/Flagsmith/.gitattributes
@@ -0,0 +1,15 @@
+/.editorconfig export-ignore
+/.gitattributes export-ignore
+/.github/ export-ignore
+/.gitignore export-ignore
+/.readthedocs.yml export-ignore
+/build/ export-ignore
+/CHANGELOG.md export-ignore
+/phpcs.xml.dist export-ignore
+/phpstan.neon.dist export-ignore
+/phpunit.xml.dist export-ignore
+/psalm-baseline.xml export-ignore
+/psalm.xml export-ignore
+/tests/ export-ignore
+
+* text=auto
diff --git a/providers/Flagsmith/.gitignore b/providers/Flagsmith/.gitignore
new file mode 100644
index 00000000..2e1a6a68
--- /dev/null
+++ b/providers/Flagsmith/.gitignore
@@ -0,0 +1,3 @@
+/build
+/composer.lock
+/vendor
diff --git a/providers/Flagsmith/CHANGELOG.md b/providers/Flagsmith/CHANGELOG.md
new file mode 100644
index 00000000..825c32f0
--- /dev/null
+++ b/providers/Flagsmith/CHANGELOG.md
@@ -0,0 +1 @@
+# Changelog
diff --git a/providers/Flagsmith/README.md b/providers/Flagsmith/README.md
new file mode 100644
index 00000000..394997f8
--- /dev/null
+++ b/providers/Flagsmith/README.md
@@ -0,0 +1,47 @@
+# OpenFeature Flagsmith Provider for PHP
+
+[](https://cloud-native.slack.com/archives/C0344AANLA1)
+[](https://packagist.org/packages/open-feature/flagsmith-provider)
+[](https://packagist.org/packages/open-feature/flagsmith-provider)
+
+[](https://packagist.org/packages/open-feature/flagsmith-provider)
+
+## Overview
+
+Flagsmith provides an all-in-one platform for developing, implementing, and managing your feature flags. This repository and package provides the client side code for interacting with it via the OpenFeature PHP SDK.
+
+This package also builds on various PSRs (PHP Standards Recommendations) such as the Logger interfaces (PSR-3) and the Basic and Extended Coding Standards (PSR-1 and PSR-12).
+
+## Installation
+
+```sh
+composer require open-feature/flagsmith-provider
+```
+
+## Usage
+
+The `FlagsmithProvider` constructor takes a configured Flagsmith client as its only argument:
+
+```php
+$flagsmith = new Flagsmith\Flagsmith('YOUR_FLAGSMITH_API_KEY');
+
+OpenFeatureAPI::setProvider(new FlagsmithProvider($flagsmith));
+```
+
+## Development
+
+### PHP Versioning
+
+This library targets PHP 8.1 and above. As long as you have a compatible version of PHP on your system, you should be able to utilize the OpenFeature SDK.
+
+This package also has a `.tool-versions` file for use with PHP version managers like `asdf`.
+
+### Installation and Dependencies
+
+Install dependencies with `composer install`.
+
+We value having as few runtime dependencies as possible. The addition of any dependencies requires careful consideration and review.
+
+### Testing
+
+Run tests with `composer run test`.
diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json
new file mode 100644
index 00000000..3d75c93c
--- /dev/null
+++ b/providers/Flagsmith/composer.json
@@ -0,0 +1,133 @@
+{
+ "name": "open-feature/flagsmith-provider",
+ "description": "The Flagsmith provider package for open-feature",
+ "license": "Apache-2.0",
+ "type": "library",
+ "keywords": [
+ "featureflags",
+ "featureflagging",
+ "openfeature",
+ "flagsmith",
+ "provider"
+ ],
+ "authors": [
+ {
+ "name": "OpenFeature PHP Maintainers",
+ "homepage": "https://github.com/orgs/open-feature/teams/php-maintainer"
+ },
+ {
+ "name": "open-feature/php-sdk-contrib Contributors",
+ "homepage": "https://github.com/open-feature/php-sdk-contrib/graphs/contributors"
+ }
+ ],
+ "require": {
+ "php": "^8.1",
+ "flagsmith/flagsmith-php-client": "^4.5.1",
+ "open-feature/sdk": "^2.0"
+ },
+ "require-dev": {
+ "ergebnis/composer-normalize": "^2.25",
+ "friendsofphp/php-cs-fixer": "^3.13",
+ "hamcrest/hamcrest-php": "^2.0",
+ "mdwheele/zalgo": "^0.3.1",
+ "mockery/mockery": "^1.5",
+ "phan/phan": "^5.4",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "~1.10.0",
+ "phpstan/phpstan-mockery": "^1.0",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "psalm/plugin-mockery": "^0.11.0",
+ "psalm/plugin-phpunit": "^0.18.0",
+ "ramsey/coding-standard": "^2.0.3",
+ "ramsey/composer-repl": "^1.4",
+ "ramsey/conventional-commits": "^1.3",
+ "roave/security-advisories": "dev-latest",
+ "spatie/phpunit-snapshot-assertions": "^4.2",
+ "vimeo/psalm": "~4.30.0"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "autoload": {
+ "psr-4": {
+ "OpenFeature\\Providers\\Flagsmith\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "OpenFeature\\Providers\\Flagsmith\\Test\\": "tests/"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "phpstan/extension-installer": true,
+ "dealerdirect/phpcodesniffer-composer-installer": true,
+ "ergebnis/composer-normalize": true,
+ "captainhook/plugin-composer": true,
+ "ramsey/composer-repl": true,
+ "php-http/discovery": false
+ },
+ "lock": false,
+ "sort-packages": true
+ },
+ "extra": {
+ "captainhook": {
+ "force-install": false
+ }
+ },
+ "scripts": {
+ "dev:analyze": [
+ "@dev:analyze:phpstan",
+ "@dev:analyze:psalm"
+ ],
+ "dev:analyze:phpstan": "@php vendor/bin/phpstan analyse --ansi --debug --memory-limit=512M",
+ "dev:analyze:psalm": "@php vendor/bin/psalm",
+ "dev:build:clean": "git clean -fX build/",
+ "dev:grpc": [
+ "@dev:grpc:init",
+ "@dev:grpc:generate",
+ "@dev:grpc:stage"
+ ],
+ "dev:grpc:generate": "export GOPATH=\"$(pwd)/schemas/vendor\" && pushd schemas && make gen-php && popd",
+ "dev:grpc:init": "git submodule update --recursive",
+ "dev:grpc:stage": "git add --force ./proto",
+ "dev:lint": [
+ "@dev:lint:syntax",
+ "@dev:lint:style"
+ ],
+ "dev:lint:fix": "@php vendor/bin/phpcbf",
+ "dev:lint:style": "@php vendor/bin/phpcs --colors",
+ "dev:lint:syntax": "@php vendor/bin/parallel-lint --colors src/ tests/",
+ "dev:test": [
+ "@dev:lint",
+ "@dev:analyze",
+ "@dev:test:unit",
+ "@dev:test:integration"
+ ],
+ "dev:test:coverage:ci": "@php vendor/bin/phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml",
+ "dev:test:coverage:html": "@php vendor/bin/phpunit --colors=always --coverage-html build/coverage/coverage-html/",
+ "dev:test:unit": "@php vendor/bin/phpunit --colors=always --testdox",
+ "dev:test:unit:debug": "@php vendor/bin/phpunit --colors=always --testdox -d xdebug.profiler_enable=on",
+ "dev:test:unit:setup": "echo 'Setup for unit tests...'",
+ "dev:test:unit:teardown": "echo 'Tore down unit tests...'",
+ "dev:test:integration:setup": "echo 'Setup for integration tests...'",
+ "dev:test:integration:teardown": "echo 'Tore down integration tests...'",
+ "test": "@dev:test"
+ },
+ "scripts-descriptions": {
+ "dev:analyze": "Runs all static analysis checks.",
+ "dev:analyze:phpstan": "Runs the PHPStan static analyzer.",
+ "dev:analyze:psalm": "Runs the Psalm static analyzer.",
+ "dev:build:clean": "Cleans the build/ directory.",
+ "dev:lint": "Runs all linting checks.",
+ "dev:lint:fix": "Auto-fixes coding standards issues, if possible.",
+ "dev:lint:style": "Checks for coding standards issues.",
+ "dev:lint:syntax": "Checks for syntax errors.",
+ "dev:test": "Runs linting, static analysis, and unit tests.",
+ "dev:test:coverage:ci": "Runs unit tests and generates CI coverage reports.",
+ "dev:test:coverage:html": "Runs unit tests and generates HTML coverage report.",
+ "dev:test:unit": "Runs unit tests.",
+ "test": "Runs linting, static analysis, and unit tests."
+ }
+}
diff --git a/providers/Flagsmith/phpcs.xml.dist b/providers/Flagsmith/phpcs.xml.dist
new file mode 100644
index 00000000..55d9d3a1
--- /dev/null
+++ b/providers/Flagsmith/phpcs.xml.dist
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ ./src
+ ./tests
+
+ */tests/fixtures/*
+ */tests/*/fixtures/*
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/providers/Flagsmith/phpstan.neon.dist b/providers/Flagsmith/phpstan.neon.dist
new file mode 100644
index 00000000..93c5b2d2
--- /dev/null
+++ b/providers/Flagsmith/phpstan.neon.dist
@@ -0,0 +1,9 @@
+parameters:
+ tmpDir: ./build/cache/phpstan
+ level: max
+ paths:
+ - ./src
+ - ./tests
+ excludePaths:
+ - */tests/fixtures/*
+ - */tests/*/fixtures/*
diff --git a/providers/Flagsmith/phpunit.xml.dist b/providers/Flagsmith/phpunit.xml.dist
new file mode 100644
index 00000000..f96ebda6
--- /dev/null
+++ b/providers/Flagsmith/phpunit.xml.dist
@@ -0,0 +1,25 @@
+
+
+
+
+
+ ./tests/Unit
+
+
+
+
+
+ ./src
+
+
+
+
+
+
+
+
diff --git a/providers/Flagsmith/psalm-baseline.xml b/providers/Flagsmith/psalm-baseline.xml
new file mode 100644
index 00000000..ceaa5778
--- /dev/null
+++ b/providers/Flagsmith/psalm-baseline.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/providers/Flagsmith/psalm.xml b/providers/Flagsmith/psalm.xml
new file mode 100644
index 00000000..c3e6c03c
--- /dev/null
+++ b/providers/Flagsmith/psalm.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php
new file mode 100644
index 00000000..7fb620bf
--- /dev/null
+++ b/providers/Flagsmith/src/FlagsmithProvider.php
@@ -0,0 +1,145 @@
+contextualFlagStore($context)->getFlag($flagKey)->getEnabled(),
+ );
+ } catch (FlagsmithThrowable $throwable) {
+ return (new ResolutionDetailsBuilder())
+ ->withValue($defaultValue)
+ ->withError(
+ new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()),
+ )->build();
+ }
+ }
+
+ public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->resolve($flagKey, $defaultValue, $context);
+ }
+
+ public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->resolve($flagKey, $defaultValue, $context);
+ }
+
+ public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->resolve($flagKey, $defaultValue, $context);
+ }
+
+ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ $builder = new ResolutionDetailsBuilder();
+
+ try {
+ $value = $this->resolve($flagKey, $defaultValue, $context)->getValue();
+
+ if ($value !== $defaultValue && is_string($value)) {
+ /** @var array|bool|DateTime|float|int|string|null $value */
+ $value = json_decode($value, true, flags: JSON_THROW_ON_ERROR);
+ }
+
+ // Valid JSON document might not be an array, so error in this case.
+ if (!is_array($value) || array_is_list($value)) {
+ throw new InvalidArgumentException("Flag [$flagKey] value must be a JSON encoded array");
+ }
+
+ $builder->withValue($value);
+ } catch (JsonException | InvalidArgumentException $exception) {
+ $builder
+ ->withValue($defaultValue)
+ ->withError(
+ new ResolutionError(ErrorCode::PARSE_ERROR(), $exception->getMessage()),
+ );
+ }
+
+ return $builder->build();
+ }
+
+ /**
+ * @param array|bool|DateTime|float|int|string|null $defaultValue
+ */
+ protected function resolve(
+ string $flagKey,
+ array | bool | DateTime | float | int | string | null $defaultValue,
+ ?EvaluationContext $context = null,
+ ): ResolutionDetails {
+ $builder = new ResolutionDetailsBuilder();
+
+ try {
+ $flag = $this->contextualFlagStore($context)->getFlag($flagKey);
+
+ if ($flag->getEnabled()) {
+ /** @var array|bool|DateTime|float|int|string|null $value */
+ $value = $flag->getValue();
+
+ $builder->withValue($value);
+ } else {
+ $builder
+ ->withValue($defaultValue)
+ ->withReason(Reason::DISABLED);
+ }
+ } catch (FlagsmithThrowable $throwable) {
+ $builder
+ ->withValue($defaultValue)
+ ->withReason(Reason::ERROR)
+ ->withError(
+ new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()),
+ );
+ }
+
+ return $builder->build();
+ }
+
+ /**
+ * @throws FlagsmithThrowable
+ */
+ private function contextualFlagStore(?EvaluationContext $context = null): Flags
+ {
+ if ($context && !is_null($identifier = $context->getTargetingKey())) {
+ return $this->flagsmith->getIdentityFlags($identifier, (object) $context->getAttributes()->toArray());
+ }
+
+ return $this->flagsmith->getEnvironmentFlags();
+ }
+}
diff --git a/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php
new file mode 100644
index 00000000..c8879607
--- /dev/null
+++ b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php
@@ -0,0 +1,21 @@
+environmentModel;
+ }
+}
diff --git a/providers/Flagsmith/tests/Fixtures/environments/boolean.json b/providers/Flagsmith/tests/Fixtures/environments/boolean.json
new file mode 100644
index 00000000..4f92a246
--- /dev/null
+++ b/providers/Flagsmith/tests/Fixtures/environments/boolean.json
@@ -0,0 +1,67 @@
+{
+ "project": {
+ "name": "Test project",
+ "organisation": {
+ "feature_analytics": false,
+ "name": "Test Org",
+ "id": 1,
+ "persist_trait_data": true,
+ "stop_serving_flags": false
+ },
+ "id": 1,
+ "hide_disabled_flags": false
+ },
+ "segment_overrides": [],
+ "id": 1,
+ "feature_states": [
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "some-value",
+ "id": 1,
+ "feature": {
+ "name": "some_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": true
+ },
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "some-value",
+ "id": 1,
+ "feature": {
+ "name": "disabled_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": false
+ }
+ ],
+ "identity_overrides": [
+ {
+ "identifier": "overridden-id",
+ "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
+ "created_date": "2019-08-27T14:53:45.698555Z",
+ "updated_at": "2023-07-14 16:12:00.000000",
+ "environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
+ "identity_features": [
+ {
+ "id": 1,
+ "feature": {
+ "id": 1,
+ "name": "some_feature",
+ "type": "STANDARD"
+ },
+ "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
+ "feature_state_value": "some-overridden-value",
+ "enabled": false,
+ "environment": 1,
+ "identity": null,
+ "feature_segment": null
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/providers/Flagsmith/tests/Fixtures/environments/float.json b/providers/Flagsmith/tests/Fixtures/environments/float.json
new file mode 100644
index 00000000..06b86fb0
--- /dev/null
+++ b/providers/Flagsmith/tests/Fixtures/environments/float.json
@@ -0,0 +1,67 @@
+{
+ "project": {
+ "name": "Test project",
+ "organisation": {
+ "feature_analytics": false,
+ "name": "Test Org",
+ "id": 1,
+ "persist_trait_data": true,
+ "stop_serving_flags": false
+ },
+ "id": 1,
+ "hide_disabled_flags": false
+ },
+ "segment_overrides": [],
+ "id": 1,
+ "feature_states": [
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": 2.345,
+ "id": 1,
+ "feature": {
+ "name": "float_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": true
+ },
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": -1.0,
+ "id": 1,
+ "feature": {
+ "name": "disabled_float_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": false
+ }
+ ],
+ "identity_overrides": [
+ {
+ "identifier": "overridden-id",
+ "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
+ "created_date": "2019-08-27T14:53:45.698555Z",
+ "updated_at": "2023-07-14 16:12:00.000000",
+ "environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
+ "identity_features": [
+ {
+ "id": 1,
+ "feature": {
+ "id": 1,
+ "name": "some_feature",
+ "type": "STANDARD"
+ },
+ "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
+ "feature_state_value": "some-overridden-value",
+ "enabled": false,
+ "environment": 1,
+ "identity": null,
+ "feature_segment": null
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/providers/Flagsmith/tests/Fixtures/environments/integer.json b/providers/Flagsmith/tests/Fixtures/environments/integer.json
new file mode 100644
index 00000000..90d22f1d
--- /dev/null
+++ b/providers/Flagsmith/tests/Fixtures/environments/integer.json
@@ -0,0 +1,67 @@
+{
+ "project": {
+ "name": "Test project",
+ "organisation": {
+ "feature_analytics": false,
+ "name": "Test Org",
+ "id": 1,
+ "persist_trait_data": true,
+ "stop_serving_flags": false
+ },
+ "id": 1,
+ "hide_disabled_flags": false
+ },
+ "segment_overrides": [],
+ "id": 1,
+ "feature_states": [
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": 2,
+ "id": 1,
+ "feature": {
+ "name": "integer_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": true
+ },
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": -1,
+ "id": 1,
+ "feature": {
+ "name": "disabled_integer_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": false
+ }
+ ],
+ "identity_overrides": [
+ {
+ "identifier": "overridden-id",
+ "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
+ "created_date": "2019-08-27T14:53:45.698555Z",
+ "updated_at": "2023-07-14 16:12:00.000000",
+ "environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
+ "identity_features": [
+ {
+ "id": 1,
+ "feature": {
+ "id": 1,
+ "name": "some_feature",
+ "type": "STANDARD"
+ },
+ "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
+ "feature_state_value": "some-overridden-value",
+ "enabled": false,
+ "environment": 1,
+ "identity": null,
+ "feature_segment": null
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/providers/Flagsmith/tests/Fixtures/environments/object.json b/providers/Flagsmith/tests/Fixtures/environments/object.json
new file mode 100644
index 00000000..7dcbb52d
--- /dev/null
+++ b/providers/Flagsmith/tests/Fixtures/environments/object.json
@@ -0,0 +1,79 @@
+{
+ "project": {
+ "name": "Test project",
+ "organisation": {
+ "feature_analytics": false,
+ "name": "Test Org",
+ "id": 1,
+ "persist_trait_data": true,
+ "stop_serving_flags": false
+ },
+ "id": 1,
+ "hide_disabled_flags": false
+ },
+ "segment_overrides": [],
+ "id": 1,
+ "feature_states": [
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "{\"key\":\"value\"}",
+ "id": 1,
+ "feature": {
+ "name": "object_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": true
+ },
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "some-value",
+ "id": 1,
+ "feature": {
+ "name": "disabled_object_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": false
+ },
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "[\"foo\",\"bar\"]",
+ "id": 1,
+ "feature": {
+ "name": "invalid_object_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": true
+ }
+ ],
+ "identity_overrides": [
+ {
+ "identifier": "overridden-id",
+ "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
+ "created_date": "2019-08-27T14:53:45.698555Z",
+ "updated_at": "2023-07-14 16:12:00.000000",
+ "environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
+ "identity_features": [
+ {
+ "id": 1,
+ "feature": {
+ "id": 1,
+ "name": "some_feature",
+ "type": "STANDARD"
+ },
+ "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
+ "feature_state_value": "some-overridden-value",
+ "enabled": false,
+ "environment": 1,
+ "identity": null,
+ "feature_segment": null
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/providers/Flagsmith/tests/Fixtures/environments/string.json b/providers/Flagsmith/tests/Fixtures/environments/string.json
new file mode 100644
index 00000000..37259d05
--- /dev/null
+++ b/providers/Flagsmith/tests/Fixtures/environments/string.json
@@ -0,0 +1,67 @@
+{
+ "project": {
+ "name": "Test project",
+ "organisation": {
+ "feature_analytics": false,
+ "name": "Test Org",
+ "id": 1,
+ "persist_trait_data": true,
+ "stop_serving_flags": false
+ },
+ "id": 1,
+ "hide_disabled_flags": false
+ },
+ "segment_overrides": [],
+ "id": 1,
+ "feature_states": [
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "flag value",
+ "id": 1,
+ "feature": {
+ "name": "string_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": true
+ },
+ {
+ "multivariate_feature_state_values": [],
+ "feature_state_value": "some-value",
+ "id": 1,
+ "feature": {
+ "name": "disabled_string_feature",
+ "type": "STANDARD",
+ "id": 1
+ },
+ "segment_id": null,
+ "enabled": false
+ }
+ ],
+ "identity_overrides": [
+ {
+ "identifier": "overridden-id",
+ "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
+ "created_date": "2019-08-27T14:53:45.698555Z",
+ "updated_at": "2023-07-14 16:12:00.000000",
+ "environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
+ "identity_features": [
+ {
+ "id": 1,
+ "feature": {
+ "id": 1,
+ "name": "some_feature",
+ "type": "STANDARD"
+ },
+ "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
+ "feature_state_value": "some-overridden-value",
+ "enabled": false,
+ "environment": 1,
+ "identity": null,
+ "feature_segment": null
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/providers/Flagsmith/tests/TestCase.php b/providers/Flagsmith/tests/TestCase.php
new file mode 100644
index 00000000..b3e28657
--- /dev/null
+++ b/providers/Flagsmith/tests/TestCase.php
@@ -0,0 +1,40 @@
+ $class
+ * @param mixed ...$arguments
+ *
+ * @return T & MockInterface
+ *
+ * @template T
+ *
+ * phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
+ */
+ public function mockery(string $class, ...$arguments)
+ {
+ /** @var T & MockInterface $mock */
+ $mock = Mockery::mock($class, ...$arguments);
+
+ return $mock;
+ }
+}
diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php
new file mode 100644
index 00000000..6c140701
--- /dev/null
+++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php
@@ -0,0 +1,244 @@
+assertInstanceOf(Provider::class, $provider);
+ }
+
+ public function testBooleanResolutionWithEnabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json');
+
+ // When
+ $resolutionDetails = $provider->resolveBooleanValue('some_feature', false);
+
+ // Then
+ $this->assertTrue($resolutionDetails->getValue());
+ }
+
+ public function testBooleanResolutionWithDisabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json');
+
+ // When
+ $resolutionDetails = $provider->resolveBooleanValue('disabled_feature', true);
+
+ // Then
+ $this->assertFalse($resolutionDetails->getValue());
+ }
+
+ public function testBooleanResolutionWithDefaultValueFromFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json');
+
+ // When
+ $resolutionDetails = $provider->resolveBooleanValue('missing_feature', false);
+
+ // Then
+ $this->assertFalse($resolutionDetails->getValue());
+ $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode());
+ }
+
+ public function testStringResolutionWithEnabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json');
+
+ // When
+ $resolutionDetails = $provider->resolveStringValue('string_feature', 'default value');
+
+ // Then
+ $this->assertEquals('flag value', $resolutionDetails->getValue());
+ }
+
+ public function testStringResolutionWithDisabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json');
+
+ // When
+ $resolutionDetails = $provider->resolveStringValue('disabled_string_feature', 'default value');
+
+ // Then
+ $this->assertEquals('default value', $resolutionDetails->getValue());
+ }
+
+ public function testStringResolutionWithMissingFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json');
+
+ // When
+ $resolutionDetails = $provider->resolveStringValue('missing_string_feature', 'default value');
+
+ // Then
+ $this->assertEquals('default value', $resolutionDetails->getValue());
+ $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode());
+ $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason());
+ }
+
+ public function testIntegerResolutionWithEnabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json');
+
+ // When
+ $resolutionDetails = $provider->resolveIntegerValue('integer_feature', 1);
+
+ // Then
+ $this->assertEquals(2, $resolutionDetails->getValue());
+ }
+
+ public function testIntegerResolutionWithDisabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json');
+
+ // When
+ $resolutionDetails = $provider->resolveIntegerValue('disabled_integer_feature', 456);
+
+ // Then
+ $this->assertEquals(456, $resolutionDetails->getValue());
+ }
+
+ public function testIntegerResolutionWithMissingFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json');
+
+ // When
+ $resolutionDetails = $provider->resolveIntegerValue('missing_integer_feature', 123);
+
+ // Then
+ $this->assertEquals(123, $resolutionDetails->getValue());
+ $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode());
+ $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason());
+ }
+
+ public function testFloatResolutionWithEnabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json');
+
+ // When
+ $resolutionDetails = $provider->resolveFloatValue('float_feature', 1.0);
+
+ // Then
+ $this->assertEquals(2.345, $resolutionDetails->getValue());
+ }
+
+ public function testFloatResolutionWithDisabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json');
+
+ // When
+ $resolutionDetails = $provider->resolveFloatValue('disabled_float_feature', 9.99);
+
+ // Then
+ $this->assertEquals(9.99, $resolutionDetails->getValue());
+ }
+
+ public function testFloatResolutionWithMissingFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json');
+
+ // When
+ $resolutionDetails = $provider->resolveFloatValue('missing_float_feature', 0.123);
+
+ // Then
+ $this->assertEquals(0.123, $resolutionDetails->getValue());
+ $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode());
+ $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason());
+ }
+
+ public function testObjectResolutionWithEnabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json');
+
+ // When
+ $resolutionDetails = $provider->resolveObjectValue('object_feature', ['a' => 'b']);
+
+ // Then
+ $this->assertEquals(['key' => 'value'], $resolutionDetails->getValue());
+ }
+
+ public function testObjectResolutionWithDisabledFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json');
+
+ // When
+ $resolutionDetails = $provider->resolveObjectValue('disabled_object_feature', ['a' => 'b']);
+
+ // Then
+ $this->assertEquals(['a' => 'b'], $resolutionDetails->getValue());
+ }
+
+ public function testObjectResolutionWithMissingFlag(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json');
+
+ // When
+ $resolutionDetails = $provider->resolveObjectValue('missing_object_feature', ['c' => 3, 'p' => 'o']);
+
+ // Then
+ $this->assertEquals(['c' => 3, 'p' => 'o'], $resolutionDetails->getValue());
+ }
+
+ public function testObjectResolutionWithEnabledFlagWithInvalidValue(): void
+ {
+ // Given
+ $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json');
+
+ // When
+ $resolutionDetails = $provider->resolveObjectValue('invalid_object_feature', ['default' => 'value']);
+
+ // Then
+ $this->assertEquals(['default' => 'value'], $resolutionDetails->getValue());
+ }
+}