diff --git a/.github/workflows/php-ci.yaml b/.github/workflows/php-ci.yaml index 5741bae3..109afb1f 100644 --- a/.github/workflows/php-ci.yaml +++ b/.github/workflows/php-ci.yaml @@ -12,8 +12,16 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-version: ['7.4', '8.0', '8.1'] - project: ['Flagd', 'Split', 'CloudBees'] + php-version: ['8.0', '8.1', '8.2'] + project-dir: + - hooks/OpenTelemetry + - hooks/DDTrace + - hooks/Validators + - providers/Flagd + - providers/Split + - providers/GoFeatureFlag + # - providers/CloudBees + fail-fast: false # todo exclude some matrix combinations based on php version requirements # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations @@ -40,56 +48,54 @@ jobs: id: composer-cache uses: actions/cache@v3 with: - path: src/${{ matrix.project }}/vendor + path: ${{ matrix.project-dir }}/vendor key: ${{ runner.os }}-${{ matrix.php-version }}-php-${{ hashFiles('**/composer.json') }} restore-keys: | ${{ runner.os }}-${{ matrix.php-version }}-php- - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: composer install --prefer-dist --no-progress --no-suggest - name: Validate Packages composer.json - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: composer validate - name: Check Style - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: vendor/bin/phpcs - name: Run Phan - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} env: PHAN_DISABLE_XDEBUG_WARN: 1 run: vendor/bin/phan - name: Run Psalm - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: vendor/bin/psalm --output-format=github --php-version=${{ matrix.php-version }} - name: Run Phpstan - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: vendor/bin/phpstan analyse --error-format=github - name: Run PHPUnit (unit tests) - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: | composer run dev:test:unit:setup - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover --testsuite unit + vendor/bin/phpunit --coverage-text --coverage-clover=coverage.unit.xml --testsuite unit composer run dev:test:unit:teardown - name: Run PHPUnit (integration tests) - working-directory: src/${{ matrix.project }} + working-directory: ${{ matrix.project-dir }} run: | composer run dev:test:integration:setup - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover --testsuite integration + vendor/bin/phpunit --coverage-text --coverage-clover=coverage.integration.xml --testsuite integration composer run dev:test:integration:teardown - ### TODO: Enable codecov support - # - name: Code Coverage - # working-directory: src/${{ matrix.project }} - # run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.php-version }} + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 ### TODO: Replicate any necessary package checks # packages: @@ -97,4 +103,4 @@ jobs: # needs: php # with: # matrix_extension: '["ast, json, grpc"]' - # install_directory: '~/.test/.packages' \ No newline at end of file + # install_directory: '~/.test/.packages' diff --git a/src/CloudBees/.github/workflows/release-please.yml b/.github/workflows/release-please.yaml similarity index 100% rename from src/CloudBees/.github/workflows/release-please.yml rename to .github/workflows/release-please.yaml diff --git a/.github/workflows/split_monorepo.yaml b/.github/workflows/split_monorepo.yaml index 2a1faefa..56230cf5 100644 --- a/.github/workflows/split_monorepo.yaml +++ b/.github/workflows/split_monorepo.yaml @@ -1,23 +1,73 @@ -name: gitsplit +name: split-monorepo on: - push: - branches: - - main - - split release: types: [published] - create: workflow_dispatch: + inputs: + tag: + description: 'Package tag' + type: string + required: true jobs: - gitsplit: + split-repositories: runs-on: ubuntu-latest + strategy: + matrix: + # Structure of the config tuple is: + # 0: Type of package (e.g. open-feature/flagd-*provider*) + # 1: Name of package (sans-org-prefix e.g. open-feature/*flagd*-provider) + # 2: Name of subdirectory (e.g. providers/*Flagd*) + config: + - [hook, dd-trace, DDTrace] + - [hook, otel, OpenTelemetry] + - [hook, validators, Validators] + - [provider, cloudbees, CloudBees] + - [provider, flagd, Flagd] + - [provider, split, Split] + - [provider, go-feature-flag, GoFeatureFlag] steps: + - name: Detect run requirement + id: shouldRun + run: | + input_ref="${{ github.event.release.tag_name }}" + if [ -z "$input_ref" ]; then + input_ref="${{ inputs.tag }}" + fi + result=0 + if [[ "$input_ref" == open-feature/${{ matrix.config[1] }}-${{ matrix.config[0] }}-* ]]; then + result=1 + fi + echo "::set-output name=result::${result}" - name: checkout - run: git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" "$GITHUB_WORKSPACE" && cd "$GITHUB_WORKSPACE" && git checkout $GITHUB_SHA - - name: Split repositories - uses: docker://jderusse/gitsplit:latest + if: ${{ steps.shouldRun.outputs.result == '1' }} + run: | + git clone "$GITHUB_SERVER_URL"/"$GITHUB_REPOSITORY" "$GITHUB_WORKSPACE" + cd "$GITHUB_WORKSPACE" + git checkout "$GITHUB_SHA" + - name: Replace string in GitHub Actions + if: ${{ steps.shouldRun.outputs.result == '1' }} + id: targetRef + run: | + input_ref="${{ github.event.release.tag_name }}" + if [ -z "$input_ref" ]; then + input_ref="${{ inputs.tag }}" + fi + if [ -n "$input_ref" ]; then + input_ref="refs/tags/$input_ref" + target_ref="$(echo -n "$input_ref"|sed 's#open-feature/${{ matrix.config[1] }}-${{ matrix.config[0] }}-##')" + fi + echo "::set-output name=result::${target_ref}" + - name: Filter and push package ${{ matrix.config[1] }}-${{ matrix.config[0] }} + if: ${{ steps.shouldRun.outputs.result == '1' }} + uses: tcarrio/git-filter-repo-docker-action@v1.5.0 with: - args: gitsplit - env: - GH_TOKEN: ${{ secrets.GITSPLIT_TOKEN }} \ No newline at end of file + privateKey: ${{ secrets.SSH_PRIVATE_KEY }} + targetOrg: open-feature-php + targetRepo: ${{ matrix.config[1] }}-${{ matrix.config[0] }} + targetBranch: ${{ steps.targetRef.outputs.result }} + tagFilter: ^open-feature/${{ matrix.config[1] }}- + filterArguments: | + --subdirectory-filter "${{matrix.config[0] }}s/${{ matrix.config[2] }}/" \ + --tag-rename "open-feature/${{ matrix.config[1] }}-${{ matrix.config[0] }}-:" \ + --force diff --git a/.gitignore b/.gitignore index 4fb5c2ae..d3bb74fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ /captainhook.config.json -/composer.lock +composer.lock /phpcs.xml /phpunit.xml /vendor/ /build/ -/proto/ \ No newline at end of file +/proto/ + +/.devenv* + +.idea/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 0e03a2f9..8573d5aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "schemas"] - path = src/Flagd/schemas + path = providers/Flagd/schemas url = https://github.com/tcarrio/schemas/ diff --git a/.gitsplit.yml b/.gitsplit.yml deleted file mode 100644 index 607974d4..00000000 --- a/.gitsplit.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Path to a cache directory Used to speed up the split over time by reusing git's objects -cache_url: "/cache/gitsplit" - -# Path to the repository to split (default = current path) -project_url: "https://github.com/open-feature/php-sdk-contrib.git" - -# List of splits. -splits: - - prefix: "src/Flagd" - target: "https://$%7BGH_TOKEN%7D@github.com/open-feature-php/flagd-provider.git" - - prefix: "src/Split" - target: "https://$%7BGH_TOKEN%7D@github.com/open-feature-php/split-provider.git" - - prefix: "src/CloudBees" - target: "https://$%7BGH_TOKEN%7D@github.com/open-feature-php/cloudbees-provider.git" - -# List of references to split (defined as regexp) -origins: - - ^main$ - - ^test$ - - ^split$ - - ^v\d+\.\d+\.\d+$ - - ^\d+\.\d+\.\d+$ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 120000 index 00000000..a72a7aff --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1 @@ +/nix/store/kjn5c8afbhm6xjdaz5z4z2ldwpknw0z9-pre-commit-config.json \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..b02e2c18 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,9 @@ +{ + "hooks/DDTrace": "1.0.0", + "hooks/OpenTelemetry": "1.0.0", + "hooks/Validators": "1.0.0", + "providers/CloudBees": "1.1.0", + "providers/Flagd": "1.0.0", + "providers/Split": "1.0.0", + "providers/GoFeatureFlag": "1.1.0" +} \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 8d0b8372..260a2ddb 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -php 7.4.33 -# php 8.0.24 -# php 8.1.11 \ No newline at end of file +php 8.0.24 +# php 8.1.11 +# php 8.2.1 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..ad0f3c91 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence +# +# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-php/workgroup.yaml +# +* @open-feature/sdk-php-maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2332eb6f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing + +## Development + +### System Requirements + +PHP 8+ is required. + +### Compilation target(s) + +We target compatibility with PHP versions 8.0, 8.1, and 8.2. + +### Project Structure + +The repository is made up of two primary directories for development: `hooks` and `providers`. These each contain packages offering OpenFeature Hooks and Providers respectively. All development is done within those packages. + +There is not yet a process for generating a hook or provider package, but [an issue is tracking this](https://github.com/open-feature/php-sdk-contrib/issues/37). + +> **🛈 All of the following instructions are from the context of the package directory being developed.** + +### 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 dev:test`. + +All packages should implement `composer` scripts for unit and integration tests. It is fine to provide no-ops for these scripts. + +### Unit tests + +Run unit tests with `composer dev:test:unit`. + +### Integration tests + +Run unit tests with `composer dev:test:unit`. + +### Packaging + +This package is directly available via Packagist and can be installed with `composer require open-feature/sdk`. Packagist utilizes Git and tags for releases, and this process is automated through GitHub. + +## Pull Request + +All contributions to the OpenFeature project are welcome via GitHub pull requests. + +To create a new PR, you will need to first fork the GitHub repository and clone upstream. + +```bash +git clone https://github.com/open-feature/php-sdk-contrib.git openfeature-php-sdk-contrib +``` + +Navigate to the repository folder + +```bash +cd openfeature-php-sdk-contrib +``` + +Add your fork as an origin + +```bash +git remote add fork https://github.com/YOUR_GITHUB_USERNAME/openfeature-php-sdk-contrib.git +``` + +Makes sure your development environment is all setup by building and testing + +```bash +composer install +composer dev:test +``` + +To start working on a new feature or bugfix, create a new branch and start working on it. + +```bash +git checkout -b feat/NAME_OF_FEATURE +# Make your changes +git commit +git push fork feat/NAME_OF_FEATURE +``` + +Open a pull request against the main php-sdk repository. + +### How to Receive Comments + +- If the PR is not ready for review, please mark it as + [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +- Make sure all required CI checks are clear. +- Submit small, focused PRs addressing a single concern/issue. +- Make sure the PR title reflects the contribution. +- Write a summary that helps understand the change. +- Include usage examples in the summary, where applicable. + +### How to Get PRs Merged + +A PR is considered to be **ready to merge** when: + +- Major feedback is resolved. +- Urgent fix can take exception as long as it has been actively communicated. + +Any Maintainer can merge the PR once it is **ready to merge**. Note, that some +PRs may not be merged immediately if the repo is in the process of a release and +the maintainers decided to defer the PR to the next release train. + +If a PR has been stuck (e.g. there are lots of debates and people couldn't agree +on each other), the owner should try to get people aligned by: + +- Consolidating the perspectives and putting a summary in the PR. It is + recommended to add a link into the PR description, which points to a comment + with a summary in the PR conversation. +- Tagging domain experts (by looking at the change history) in the PR asking + for suggestion. +- Reaching out to more people on the [CNCF OpenFeature Slack channel](https://cloud-native.slack.com/archives/C0344AANLA1). +- Stepping back to see if it makes sense to narrow down the scope of the PR or + split it up. +- If none of the above worked and the PR has been stuck for more than 2 weeks, + the owner should bring it to the OpenFeatures [meeting](README.md#contributing). + +## Versioning and releasing + +As described in the [README](./README.md), this project uses release-please, and semantic versioning. +Breaking changes should be identified by using a semantic PR title. + +## Dependencies + +Keep dependencies to a minimum, especially non-dev dependencies. + +The PHP SDK can be a non-dev dependency, as `composer` does not allow multiple versions of packages to be resolved during a `composer install`. diff --git a/README.md b/README.md index 6e744c7e..f80b3b56 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ # OpenFeature PHP SDK Contrib Library [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -![PHP 7.4+](https://img.shields.io/badge/php->=7.4-blue.svg) +[![codecov](https://codecov.io/gh/open-feature/php-sdk-contrib/branch/main/graph/badge.svg?token=3DC5XOEHMY)](https://codecov.io/gh/open-feature/php-sdk-contrib) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) ![License](https://img.shields.io/github/license/open-feature/php-sdk-contrib) ![Experimental](https://img.shields.io/badge/Status-Experimental-yellow) ## Overview -The `php-contrib-sdk` repository is a monorepository containing various providers and hooks for OpenFeature's PHP SDK. Packages include: +The `php-contrib-sdk` repository is a monorepository containing various providers, hooks, and other integrations for OpenFeature's PHP SDK. Packages include: -- [Flagd](./src/Flagd/README.md) -- [Split](./src/Split/README.md) -- [CloudBees](./src/CloudBees/README.md) +- Providers + - [Flagd](./providers/Flagd/README.md) + - [Split](./providers/Split/README.md) + - [CloudBees](./providers/CloudBees/README.md) + - [GO Feature Flag](./providers/GoFeatureFlag/README.md) +- Hooks + - [OpenTelemetry](./hooks/OpenTelemetry/README.md) + - [Datadog](./hooks/DDTrace/README.md) + - [Validators](./hooks/Validators/README.md) ### Status @@ -21,9 +28,7 @@ This repository is marked as **experimental** since the repository structure its ### PHP Versioning -This library targets PHP version 7.4 and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK. - -⚠️ **PHP 7.4 is EOL and support will be discontinued in these libraries soon.** +This library targets PHP version 8.0 and newer. As long as you have any 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`. diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 00000000..a79b9f04 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,152 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1675875772, + "narHash": "sha256-sYXHPZ4tsjdG+UXK0mYnABhiS/RuzHiV9uGOU9YakwE=", + "owner": "cachix", + "repo": "devenv", + "rev": "eac5eb12eb42765f5f252972dc876d1f96b03dfe", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1660459072, + "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1675758091, + "narHash": "sha256-7gFSQbSVAFUHtGCNHPF7mPc5CcqDk9M2+inlVPZSneg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "747927516efcb5e31ba03b7ff32f61f6d47e7d87", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1673800717, + "narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1671271357, + "narHash": "sha256-xRJdLbWK4v2SewmSStYrcLa0YGJpleufl44A19XSW8k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "40f79f003b6377bd2f4ed4027dde1f8f922995dd", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1675688762, + "narHash": "sha256-oit/SxMk0B380ASuztBGQLe8TttO1GJiXF8aZY9AYEc=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "ab608394886fb04b8a5df3cb0bab2598400e3634", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 00000000..8a9cc2e5 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,52 @@ +{ pkgs, ... }: + +{ + + # https://devenv.sh/packages/ + packages = [ pkgs.git ]; + + # https://devenv.sh/languages/ + languages.nix.enable = true; + languages.php.enable = true; + languages.php.package = pkgs.php80; + + # https://devenv.sh/basics/ + env.PROJECT_NAME = "openfeature-php-sdk"; + + # https://devenv.sh/scripts/ + scripts.hello.exec = "echo $ Started devenv shell in $PROJECT_NAME"; + + enterShell = '' + hello + echo + git --version + php --version + echo + + # optimization step -- files and directories that match entries + # in the .gitignore will still be traversed, and the .devenv + # directory contains over 5000 files and 121MB. + if ! grep -E "excludesfile.+\.gitignore" .git/config &>/dev/null + then + git config --local core.excludesfile .gitignore + fi + ''; + + ## https://devenv.sh/pre-commit-hooks/ + pre-commit.hooks = { + # # general formatting + # prettier.enable = true; + # github actions + actionlint.enable = true; + # nix + deadnix.enable = true; + nixfmt.enable = true; + # php + # phpcbf.enable = true; + # # ensure Markdown code is executable + # mdsh.enable = true; + }; + + # https://devenv.sh/processes/ + # processes.ping.exec = "ping example.com"; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 00000000..292df95b --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,5 @@ +inputs: + nixpkgs: + url: github:NixOS/nixpkgs/nixpkgs-unstable + pre-commit-hooks: + url: github:cachix/pre-commit-hooks.nix \ No newline at end of file diff --git a/hooks/.gitkeep b/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/CloudBees/.gitignore b/hooks/DDTrace/.gitignore similarity index 100% rename from src/CloudBees/.gitignore rename to hooks/DDTrace/.gitignore diff --git a/hooks/DDTrace/CHANGELOG.md b/hooks/DDTrace/CHANGELOG.md new file mode 100644 index 00000000..e589e582 --- /dev/null +++ b/hooks/DDTrace/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/dd-trace-hook-v1.0.0...open-feature/dd-trace-hook-1.0.0) (2024-09-04) + + +### ⚠ BREAKING CHANGES + +* fix release-please config ([#38](https://github.com/open-feature/php-sdk-contrib/issues/38)) +* fix release-please config + +### Features + +* dd-trace hook ([#26](https://github.com/open-feature/php-sdk-contrib/issues/26)) ([d2b1a04](https://github.com/open-feature/php-sdk-contrib/commit/d2b1a0440bbb0d1fa557b3aefd32eee6267f2823)) +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) + + +### Miscellaneous Chores + +* fix release-please config ([0210952](https://github.com/open-feature/php-sdk-contrib/commit/0210952af1d6774744c633507a9bec73f3cf7251)) +* fix release-please config ([#38](https://github.com/open-feature/php-sdk-contrib/issues/38)) ([8ee9fe3](https://github.com/open-feature/php-sdk-contrib/commit/8ee9fe37584ad6754272ad3ac016902e6ebd48d8)) + +## [0.2.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/dd-trace-hook-0.2.0...open-feature/dd-trace-hook-0.2.0) (2024-08-05) + + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) + +## [0.2.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/dd-trace-hook-0.2.0...open-feature/dd-trace-hook-0.2.0) (2023-02-12) + + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) + +## 0.2.0 (2023-01-09) + + +### ⚠ BREAKING CHANGES + +* fix release-please config ([#38](https://github.com/open-feature/php-sdk-contrib/issues/38)) +* fix release-please config + +### Features + +* dd-trace hook ([#26](https://github.com/open-feature/php-sdk-contrib/issues/26)) ([d2b1a04](https://github.com/open-feature/php-sdk-contrib/commit/d2b1a0440bbb0d1fa557b3aefd32eee6267f2823)) + + +### Miscellaneous Chores + +* fix release-please config ([0210952](https://github.com/open-feature/php-sdk-contrib/commit/0210952af1d6774744c633507a9bec73f3cf7251)) +* fix release-please config ([#38](https://github.com/open-feature/php-sdk-contrib/issues/38)) ([8ee9fe3](https://github.com/open-feature/php-sdk-contrib/commit/8ee9fe37584ad6754272ad3ac016902e6ebd48d8)) diff --git a/hooks/DDTrace/README.md b/hooks/DDTrace/README.md new file mode 100644 index 00000000..6f8fd1fd --- /dev/null +++ b/hooks/DDTrace/README.md @@ -0,0 +1,62 @@ +# OpenFeature DDTrace Hook + +[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Latest Stable Version](http://poser.pugx.org/open-feature/dd-trace-hook/v)](https://packagist.org/packages/open-feature/dd-trace-hook) +[![Total Downloads](http://poser.pugx.org/open-feature/dd-trace-hook/downloads)](https://packagist.org/packages/open-feature/dd-trace-hook) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) +[![License](http://poser.pugx.org/open-feature/dd-trace-hook/license)](https://packagist.org/packages/open-feature/dd-trace-hook) + +## Overview + +`dd-trace` is the Datadog tracing library for PHP. It is built on the OpenTracing specification. + +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). + +### Design + +OpenTracing is now an archived project of the CNCF, with suggestions to move towards OpenTelemetry. Feel free to check out our [OpenTelemetry hook for OpenFeature](../OpenTelemetry/README.md) as well. OpenTelemetry defines a semantic convention for feature flagging which is utilized in this hook to report flag evaluations, which is the basis for the log events being performed in this library for `dd-trace`. + +### Autoloading + +This package supports Composer autoloading. Thus, simply installing the package is all you need in order to immediately get started with Datadog's DDTrace for OpenFeature! Examples are provided that showcase the simple setup as well. Check out the **Usage** section for more info. + +## Installation + +```sh +composer require open-feature/dd-trace-hook +``` + +## Usage + +The `DDTraceHook` should be registered to the OpenFeatureAPI globally for use across all evaluations. + +It makes use of the `dd-trace` packages `Globals` utility for current span retrieval, thus has +no dependency on configuration or injection of tracers. + +```php +use OpenFeature\Hooks\DDTrace\DDTraceHook; + +DDTraceHook::register(); +``` + +For more information on DDTrace, check out [their documentation](https://docs.datadoghq.com/tracing/trace_collection/dd_libraries/php?tab=containers). + +For more examples, see the [examples](./examples/). + +## Development + +### PHP Versioning + +This library targets PHP version 8.0 and newer. As long as you have any 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`. `composer install` will update the `composer.lock` with the most recent compatible versions. + +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/hooks/DDTrace/composer.json b/hooks/DDTrace/composer.json new file mode 100644 index 00000000..dd6eae71 --- /dev/null +++ b/hooks/DDTrace/composer.json @@ -0,0 +1,139 @@ +{ + "name": "open-feature/dd-trace-hook", + "description": "The Datadog dd-trace hook package for OpenFeature", + "license": "Apache-2.0", + "type": "library", + "keywords": [ + "featureflags", + "featureflagging", + "openfeature", + "dd-trace", + "ddtrace", + "datadog", + "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", + "open-feature/sdk": "^2.0" + }, + "require-dev": { + "datadog/dd-trace": "^0.99.0", + "ergebnis/composer-normalize": "^2.25", + "friendsofphp/php-cs-fixer": "^3.13", + "hamcrest/hamcrest-php": "^2.0", + "mdwheele/zalgo": "^0.3.1", + "mikey179/vfsstream": "v1.6.11", + "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.9.1", + "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\\Hooks\\DDTrace\\": "src" + }, + "files": [ + "src/_autoload.php" + ] + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Hooks\\DDTrace\\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 + }, + "sort-packages": true + }, + "extra": { + "captainhook": { + "force-install": false + } + }, + "scripts": { + "dev:analyze": [ + "@dev:analyze:phpstan", + "@dev:analyze:psalm" + ], + "dev:analyze:phpstan": "phpstan analyse --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "psalm", + "dev:build:clean": "git clean -fX build/", + "dev:lint": [ + "@dev:lint:syntax", + "@dev:lint:style" + ], + "dev:lint:fix": "phpcbf", + "dev:lint:style": "phpcs --colors", + "dev:lint:syntax": "parallel-lint --colors src/ tests/", + "dev:test": [ + "@dev:lint", + "@dev:analyze", + "@dev:test:unit", + "@dev:test:integration" + ], + "dev:test:coverage:ci": "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": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": [ + "@dev:test:unit:setup", + "phpunit --colors=always --testdox --testsuite=unit", + "@dev:test:unit:teardown" + ], + "dev:test:unit:debug": "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 for unit tests...'", + "dev:test:integration": [ + "@dev:test:integration:setup", + "phpunit --colors=always --testdox --testsuite=integration", + "@dev:test:integration:teardown" + ], + "dev:test:integration:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "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/src/CloudBees/examples/.gitignore b/hooks/DDTrace/examples/.gitignore similarity index 100% rename from src/CloudBees/examples/.gitignore rename to hooks/DDTrace/examples/.gitignore diff --git a/hooks/DDTrace/examples/AutoloadDDTrace/README.md b/hooks/DDTrace/examples/AutoloadDDTrace/README.md new file mode 100644 index 00000000..f981a28d --- /dev/null +++ b/hooks/DDTrace/examples/AutoloadDDTrace/README.md @@ -0,0 +1,5 @@ +# OpenFeature DDTrace Hook example + +This example provides an example of bootstrapping and using the DDTrace hook for OpenFeature. + +It showcases the auto-instrumentation functionality of OpenFeature and DDTrace, neither of which requiring imperative interactions with DDTrace to bootstrap, configure, and utilize it. diff --git a/hooks/DDTrace/examples/AutoloadDDTrace/composer.json b/hooks/DDTrace/examples/AutoloadDDTrace/composer.json new file mode 100644 index 00000000..e90f37e2 --- /dev/null +++ b/hooks/DDTrace/examples/AutoloadDDTrace/composer.json @@ -0,0 +1,16 @@ +{ + "name": "open-feature/dd-trace-manual-instrumentation-example", + "description": "An example of using the DDTrace hook for OpenFeature with manual instrumentation", + "type": "project", + "license": "Apache-2.0", + "authors": [ + { + "name": "Tom Carrio", + "email": "tom@carrio.dev" + } + ], + "require": { + "open-feature/sdk": "^1.2.0", + "datadog/dd-trace": "^0.82.0" + } +} diff --git a/hooks/DDTrace/examples/AutoloadDDTrace/src/main.php b/hooks/DDTrace/examples/AutoloadDDTrace/src/main.php new file mode 100644 index 00000000..1a8ffa0d --- /dev/null +++ b/hooks/DDTrace/examples/AutoloadDDTrace/src/main.php @@ -0,0 +1,16 @@ +getClient('dev.openfeature.contrib.php.demo', '1.0.0'); + +$version = $client->getStringValue('dev.openfeature.contrib.php.version-value', 'unknown'); + +echo 'Version is ' . $version; \ No newline at end of file diff --git a/src/CloudBees/phpcs.xml.dist b/hooks/DDTrace/phpcs.xml.dist similarity index 87% rename from src/CloudBees/phpcs.xml.dist rename to hooks/DDTrace/phpcs.xml.dist index 5ba48465..55d9d3a1 100644 --- a/src/CloudBees/phpcs.xml.dist +++ b/hooks/DDTrace/phpcs.xml.dist @@ -20,9 +20,6 @@ - - - diff --git a/src/CloudBees/phpstan.neon.dist b/hooks/DDTrace/phpstan.neon.dist similarity index 100% rename from src/CloudBees/phpstan.neon.dist rename to hooks/DDTrace/phpstan.neon.dist diff --git a/src/CloudBees/phpunit.xml.dist b/hooks/DDTrace/phpunit.xml.dist similarity index 100% rename from src/CloudBees/phpunit.xml.dist rename to hooks/DDTrace/phpunit.xml.dist diff --git a/src/CloudBees/psalm-baseline.xml b/hooks/DDTrace/psalm-baseline.xml similarity index 100% rename from src/CloudBees/psalm-baseline.xml rename to hooks/DDTrace/psalm-baseline.xml diff --git a/src/CloudBees/psalm.xml b/hooks/DDTrace/psalm.xml similarity index 100% rename from src/CloudBees/psalm.xml rename to hooks/DDTrace/psalm.xml diff --git a/hooks/DDTrace/src/DDTraceHook.php b/hooks/DDTrace/src/DDTraceHook.php new file mode 100644 index 00000000..e1e43f43 --- /dev/null +++ b/hooks/DDTrace/src/DDTraceHook.php @@ -0,0 +1,132 @@ +addHooks(self::$instance); + self::$registeredHook = true; + } + + private static ?DDTraceHook $instance = null; + private static bool $registeredHook = false; + + public function before(HookContext $context, HookHints $hints): ?EvaluationContext + { + return null; + } + + public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void + { + $span = self::getCurrentSpan(); + if (!$span) { + return; + } + + $span->log([ + Tag::LOG_MESSAGE => [ + self::FLAG_KEY => $context->getFlagKey(), + self::FLAG_PROVIDER_NAME => OpenFeatureAPI::getInstance()->getProvider()->getMetadata()->getName(), + self::FLAG_VARIANT => $details->getVariant(), + ], + ], new DateTimeImmutable('now')); + } + + public function error(HookContext $context, Throwable $error, HookHints $hints): void + { + $span = self::getCurrentSpan(); + if (!$span) { + return; + } + + $span->log([ + Tag::LOG_ERROR_OBJECT => [ + self::FLAG_KEY => $context->getFlagKey(), + self::FLAG_PROVIDER_NAME => OpenFeatureAPI::getInstance()->getProvider()->getMetadata()->getName(), + ], + ], new DateTimeImmutable('now')); + } + + public function finally(HookContext $context, HookHints $hints): void + { + // no-op + } + + public function supportsFlagValueType(string $flagValueType): bool + { + return true; + } + + /** + * Hooks can be cleared by other means so we can't simply memoize whether a registration has occurred + * + * However if no registration has yet happened then we can absolutely determine that the hook will + * not be registered yet. + */ + private static function isRegisteredInHooks(): bool + { + foreach (OpenFeatureAPI::getInstance()->getHooks() as $hook) { + if ($hook instanceof DDTraceHook) { + return true; + } + } + + return false; + } + + private static function getCurrentSpan(): ?Span + { + if (!class_exists(GlobalTracer::class)) { + return null; + } + + return GlobalTracer::get()->getActiveSpan(); + } +} diff --git a/hooks/DDTrace/src/_autoload.php b/hooks/DDTrace/src/_autoload.php new file mode 100644 index 00000000..8d397662 --- /dev/null +++ b/hooks/DDTrace/src/_autoload.php @@ -0,0 +1,8 @@ + $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/hooks/DDTrace/tests/integration/DDTraceHookTest.php b/hooks/DDTrace/tests/integration/DDTraceHookTest.php new file mode 100644 index 00000000..b5fe9e04 --- /dev/null +++ b/hooks/DDTrace/tests/integration/DDTraceHookTest.php @@ -0,0 +1,47 @@ +clearHooks(); + + // When + $this->simulateAutoload(); + + // Then + + $this->assertCount(1, $api->getHooks()); + $this->assertInstanceOf(Hook::class, $api->getHooks()[0]); + } + + public function testCanBeRegistered(): void + { + // Given + $api = OpenFeatureAPI::getInstance(); + $api->clearHooks(); + + // When + DDTraceHook::register(); + + // Then + $this->assertCount(1, $api->getHooks()); + $this->assertInstanceOf(Hook::class, $api->getHooks()[0]); + } + + private function simulateAutoload(): void + { + require __DIR__ . '/../../src/_autoload.php'; + } +} diff --git a/hooks/DDTrace/tests/unit/DDTraceHookTest.php b/hooks/DDTrace/tests/unit/DDTraceHookTest.php new file mode 100644 index 00000000..f335328b --- /dev/null +++ b/hooks/DDTrace/tests/unit/DDTraceHookTest.php @@ -0,0 +1,26 @@ +assertNotEmpty($api->getHooks()); + $this->assertInstanceOf(Hook::class, $api->getHooks()[0]); + } +} diff --git a/src/Flagd/.gitignore b/hooks/OpenTelemetry/.gitignore similarity index 100% rename from src/Flagd/.gitignore rename to hooks/OpenTelemetry/.gitignore diff --git a/hooks/OpenTelemetry/CHANGELOG.md b/hooks/OpenTelemetry/CHANGELOG.md new file mode 100644 index 00000000..46150196 --- /dev/null +++ b/hooks/OpenTelemetry/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/otel-hook-v1.0.0...open-feature/otel-hook-1.0.0) (2024-09-04) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) + +## 0.2.0 (2023-01-09) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) diff --git a/hooks/OpenTelemetry/README.md b/hooks/OpenTelemetry/README.md new file mode 100644 index 00000000..fb732de4 --- /dev/null +++ b/hooks/OpenTelemetry/README.md @@ -0,0 +1,62 @@ +# OpenFeature OpenTelemetry Hook + +[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Latest Stable Version](http://poser.pugx.org/open-feature/otel-hook/v)](https://packagist.org/packages/open-feature/otel-hook) +[![Total Downloads](http://poser.pugx.org/open-feature/otel-hook/downloads)](https://packagist.org/packages/open-feature/otel-hook) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) +[![License](http://poser.pugx.org/open-feature/otel-hook/license)](https://packagist.org/packages/open-feature/otel-hook) + +## Overview + +OpenTelemetry is an open specification for distributed tracing, metrics, and logging. It defines a semantic convention for feature flagging which is utilized in this hook to report flag evaluations. + +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). + +### Autoloading + +This package supports Composer autoloading. Thus, simply installing the package is all you need in order to immediately get started with OpenTracing for OpenFeature! Examples are provided that showcase the simple setup as well. Check out the Usage section for more info. + +### OpenTelemetry Package Status + +The OpenTelemetry package for PHP is still in beta, so there could be changes required. However, it exposes global primitives for span retrieval that should not require any configuration upfront for the provider to just work. + +## Installation + +```sh +composer require open-feature/otel-hook +``` + +## Usage + +The `OpenTelemetryHook` should be registered to the OpenFeatureAPI globally for use across all evaluations. + +It makes use of the `open-telemetry/api` packages `Globals` utility for current span retrieval, thus has +no dependency on configuration or injection of tracers. + +```php +use OpenFeature\Hooks\OpenTelemetry\OpenTelemetryHook; + +OpenTelemetryHook::register(); +``` + +For more information on OpenTelemetry, check out [their documentation](https://opentelemetry.io/docs/instrumentation/php/). + +For more examples, see the [examples](./examples/). + +## Development + +### PHP Versioning + +This library targets PHP version 8.0 and newer. As long as you have any 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`. `composer install` will update the `composer.lock` with the most recent compatible versions. + +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/hooks/OpenTelemetry/composer.json b/hooks/OpenTelemetry/composer.json new file mode 100644 index 00000000..ba4b9e8a --- /dev/null +++ b/hooks/OpenTelemetry/composer.json @@ -0,0 +1,138 @@ +{ + "name": "open-feature/otel-hook", + "description": "The OpenTelemetry hook package for OpenFeature", + "license": "Apache-2.0", + "type": "library", + "keywords": [ + "featureflags", + "featureflagging", + "openfeature", + "otel", + "opentelemetry", + "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", + "open-feature/sdk": "^2.0", + "open-telemetry/api": "^0.0.17" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.25", + "friendsofphp/php-cs-fixer": "^3.13", + "hamcrest/hamcrest-php": "^2.0", + "mdwheele/zalgo": "^0.3.1", + "mikey179/vfsstream": "v1.6.11", + "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.9.1", + "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\\Hooks\\OpenTelemetry\\": "src" + }, + "files": [ + "src/_autoload.php" + ] + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Hooks\\OpenTelemetry\\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 + }, + "sort-packages": true + }, + "extra": { + "captainhook": { + "force-install": false + } + }, + "scripts": { + "dev:analyze": [ + "@dev:analyze:phpstan", + "@dev:analyze:psalm" + ], + "dev:analyze:phpstan": "phpstan analyse --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "psalm", + "dev:build:clean": "git clean -fX build/", + "dev:lint": [ + "@dev:lint:syntax", + "@dev:lint:style" + ], + "dev:lint:fix": "phpcbf", + "dev:lint:style": "phpcs --colors", + "dev:lint:syntax": "parallel-lint --colors src/ tests/", + "dev:test": [ + "@dev:lint", + "@dev:analyze", + "@dev:test:unit", + "@dev:test:integration" + ], + "dev:test:coverage:ci": "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": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": [ + "@dev:test:unit:setup", + "phpunit --colors=always --testdox --testsuite=unit", + "@dev:test:unit:teardown" + ], + "dev:test:unit:debug": "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 for unit tests...'", + "dev:test:integration": [ + "@dev:test:integration:setup", + "phpunit --colors=always --testdox --testsuite=integration", + "@dev:test:integration:teardown" + ], + "dev:test:integration:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "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/src/Flagd/examples/.gitignore b/hooks/OpenTelemetry/examples/.gitignore similarity index 100% rename from src/Flagd/examples/.gitignore rename to hooks/OpenTelemetry/examples/.gitignore diff --git a/hooks/OpenTelemetry/examples/AutoloadOTelSDK/README.md b/hooks/OpenTelemetry/examples/AutoloadOTelSDK/README.md new file mode 100644 index 00000000..6162d7f2 --- /dev/null +++ b/hooks/OpenTelemetry/examples/AutoloadOTelSDK/README.md @@ -0,0 +1,5 @@ +# OpenFeature OpenTelemetry Hook example + +This example provides an example of bootstrapping and using the OpenTelemetry hook for OpenFeature. + +It showcases the auto-instrumentation functionality of OpenFeature and OpenTelemetry, neither of which requiring imperative interactions with OTel to bootstrap, configure, and utilize it. diff --git a/hooks/OpenTelemetry/examples/AutoloadOTelSDK/composer.json b/hooks/OpenTelemetry/examples/AutoloadOTelSDK/composer.json new file mode 100644 index 00000000..f2116e92 --- /dev/null +++ b/hooks/OpenTelemetry/examples/AutoloadOTelSDK/composer.json @@ -0,0 +1,16 @@ +{ + "name": "open-feature/otel-manual-instrumentation-example", + "description": "An example of using the OpenTelemetry hook for OpenFeature with manual instrumentation", + "type": "project", + "license": "Apache-2.0", + "authors": [ + { + "name": "Tom Carrio", + "email": "tom@carrio.dev" + } + ], + "require": { + "open-telemetry/api": "0.0.17", + "open-feature/sdk": "^1.2.0" + } +} diff --git a/hooks/OpenTelemetry/examples/AutoloadOTelSDK/src/main.php b/hooks/OpenTelemetry/examples/AutoloadOTelSDK/src/main.php new file mode 100644 index 00000000..a15aa1a3 --- /dev/null +++ b/hooks/OpenTelemetry/examples/AutoloadOTelSDK/src/main.php @@ -0,0 +1,25 @@ +getClient('dev.openfeature.contrib.php.demo', '1.0.0'); + +$version = $client->getStringValue('dev.openfeature.contrib.php.version-value', 'unknown'); + +echo 'Version is ' . $version; \ No newline at end of file diff --git a/hooks/OpenTelemetry/examples/OTelManualInstrumentation/README.md b/hooks/OpenTelemetry/examples/OTelManualInstrumentation/README.md new file mode 100644 index 00000000..b4cfaad7 --- /dev/null +++ b/hooks/OpenTelemetry/examples/OTelManualInstrumentation/README.md @@ -0,0 +1,5 @@ +# OpenFeature OpenTelemetry Hook example + +This example provides an example of bootstrapping and using the OpenTelemetry hook for OpenFeature. + +It showcases how you can manually configure the OTel SDK for metrics, tracing, and more. This is then utilized under the hood by OpenFeature within its hook lifecycles to report the feature flag semantic events via the tracer. \ No newline at end of file diff --git a/hooks/OpenTelemetry/examples/OTelManualInstrumentation/composer.json b/hooks/OpenTelemetry/examples/OTelManualInstrumentation/composer.json new file mode 100644 index 00000000..b1121bbb --- /dev/null +++ b/hooks/OpenTelemetry/examples/OTelManualInstrumentation/composer.json @@ -0,0 +1,22 @@ +{ + "name": "open-feature/otel-manual-instrumentation-example", + "description": "An example of using the OpenTelemetry hook for OpenFeature with manual instrumentation", + "type": "project", + "license": "Apache-2.0", + "authors": [ + { + "name": "Tom Carrio", + "email": "tom@carrio.dev" + } + ], + "require": { + "guzzlehttp/guzzle": "*", + "php-http/guzzle7-adapter": "*", + "slim/slim": "~4", + "php-di/php-di": "^6.3", + "php-di/slim-bridge": "^3.2", + "open-telemetry/api": "0.0.17", + "open-telemetry/sdk": "0.0.17", + "open-feature/sdk": "^1.2.0" + } +} diff --git a/hooks/OpenTelemetry/examples/OTelManualInstrumentation/src/main.php b/hooks/OpenTelemetry/examples/OTelManualInstrumentation/src/main.php new file mode 100644 index 00000000..59ecf457 --- /dev/null +++ b/hooks/OpenTelemetry/examples/OTelManualInstrumentation/src/main.php @@ -0,0 +1,172 @@ +getClient('dev.openfeature.contrib.php', '1.0.0'); + +// Registration of a provider could be done here. See examples from other +// providers in php-sdk-contrib under [the providers directory](https://github.com/open-feature/php-sdk-contrib/tree/main/providers) +$openFeatureApi->setProvider(new NoOpProvider()); + +// The rest of this example code is verbatim copied from OpenTelemetry as +// a demo app which utilizes OTel. We inject a few feature flag lookups +// for demo sake. +$tracerProvider = (new TracerProviderFactory())->create(); +ShutdownHandler::register([$tracerProvider, 'shutdown']); +$tracer = $tracerProvider->getTracer('io.opentelemetry.contrib.php'); + +$cb = new ContainerBuilder(); +$container = $cb->addDefinitions([ + Tracer::class => $tracer, + Client::class => function () use ($tracer) { + $stack = HandlerStack::create(); + //a guzzle middleware to wrap http calls in a span, and inject trace headers + $stack->push(function (callable $handler) use ($tracer) { + return function (RequestInterface $request, array $options) use ($handler, $tracer): PromiseInterface { + $span = $tracer + ->spanBuilder(sprintf('%s %s', $request->getMethod(), $request->getUri())) + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute('http.method', $request->getMethod()) + ->setAttribute('http.url', $request->getUri()) + ->startSpan(); + + $ctx = $span->storeInContext(Context::getCurrent()); + $carrier = []; + TraceContextPropagator::getInstance()->inject($carrier, null, $ctx); + //inject traceparent and tracestate headers + foreach ($carrier as $name => $value) { + $request = $request->withAddedHeader($name, $value); + } + + $promise = $handler($request, $options); + $promise->then(function (Response $response) use ($span) { + $span->setAttribute('http.status_code', $response->getStatusCode()) + ->setAttribute('http.response_content_length', $response->getHeaderLine('Content-Length') ?: $response->getBody()->getSize()) + ->setStatus($response->getStatusCode() < 500 ? StatusCode::STATUS_OK : StatusCode::STATUS_ERROR) + ->end(); + + return $response; + }, function (\Throwable $t) use ($span) { + $span->recordException($t)->setStatus(StatusCode::STATUS_ERROR)->end(); + + throw $t; + }); + + return $promise; + }; + }); + + return new Client(['handler' => $stack, 'http_errors' => false]); + }, +])->build(); +$app = Bridge::create($container); + +//middleware starts root span based on route pattern, sets status from http code +$app->add(function (Request $request, RequestHandler $handler) use ($tracer) { + $parent = TraceContextPropagator::getInstance()->extract($request->getHeaders()); + $routeContext = RouteContext::fromRequest($request); + $route = $routeContext->getRoute(); + $root = $tracer->spanBuilder($route->getPattern()) + ->setStartTimestamp((int) ($request->getServerParams()['REQUEST_TIME_FLOAT'] * 1e9)) + ->setParent($parent) + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + $scope = $root->activate(); + + try { + $response = $handler->handle($request); + $root->setStatus($response->getStatusCode() < 500 ? StatusCode::STATUS_OK : StatusCode::STATUS_ERROR); + } finally { + $root->end(); + $scope->detach(); + } + + return $response; +}); +$app->addRoutingMiddleware(); +$errorMiddleware = $app->addErrorMiddleware(true, true, true); + +//route for service-one +$app->get('/users/{name}', function ($name, Client $client, Response $response) use ($flagsClient) { + $protocol = $flagsClient->getBooleanValue('dev.openfeature.contrib.php.secure-http-enabled', false, null, null) + ? 'https' + : 'http'; + + $promises = [ + 'two' => $client->getAsync($protocol . '://service-two:8000/two/' . $name), + 'three' => $client->getAsync($protocol . '://service-three:8000/three'), + 'other' => $client->getAsync($protocol . '://httpbin.org/get?foo=bar'), + ]; + $responses = Utils::unwrap($promises); + foreach ($responses as $res) { + $response->getBody()->write($res->getBody()->getContents()); + } + + return $response; +}); + +//route for service-two +$app->get('/two/{name}', function (Response $response, $name) use ($tracer) { + $span = $tracer + ->spanBuilder('get-user') + ->setAttribute('db.system', 'mysql') + ->setAttribute('db.name', 'users') + ->setAttribute('db.user', 'some_user') + ->setAttribute('db.statement', 'select * from users where username = :1') + ->startSpan(); + usleep((int) (0.3 * 1e6)); + $span->setStatus(StatusCode::STATUS_OK)->end(); + $response->getBody()->write(\json_encode(['some' => 'data', 'user' => $name])); + + return $response->withAddedHeader('Content-Type', 'application/json'); +}); + +//route for service-three +$app->get('/three', function (Response $response) use ($flagsClient) { + $waitTime = $flagsClient->getFloatValue('dev.openfeature.contrib.php.three-wait-time', 1e6, null, null); + + usleep((int) (0.2 * $waitTime)); + $response->getBody()->write(\json_encode(['error' => 'foo'])); + + return $response->withStatus(500)->withAddedHeader('Content-Type', 'application/json'); +}); + +$app->run(); +$tracerProvider->shutdown(); diff --git a/src/Split/phpcs.xml.dist b/hooks/OpenTelemetry/phpcs.xml.dist similarity index 87% rename from src/Split/phpcs.xml.dist rename to hooks/OpenTelemetry/phpcs.xml.dist index 5ba48465..55d9d3a1 100644 --- a/src/Split/phpcs.xml.dist +++ b/hooks/OpenTelemetry/phpcs.xml.dist @@ -20,9 +20,6 @@ - - - diff --git a/src/Split/phpstan.neon.dist b/hooks/OpenTelemetry/phpstan.neon.dist similarity index 100% rename from src/Split/phpstan.neon.dist rename to hooks/OpenTelemetry/phpstan.neon.dist diff --git a/src/Split/phpunit.xml.dist b/hooks/OpenTelemetry/phpunit.xml.dist similarity index 100% rename from src/Split/phpunit.xml.dist rename to hooks/OpenTelemetry/phpunit.xml.dist diff --git a/src/Flagd/psalm-baseline.xml b/hooks/OpenTelemetry/psalm-baseline.xml similarity index 100% rename from src/Flagd/psalm-baseline.xml rename to hooks/OpenTelemetry/psalm-baseline.xml diff --git a/src/Split/psalm.xml b/hooks/OpenTelemetry/psalm.xml similarity index 100% rename from src/Split/psalm.xml rename to hooks/OpenTelemetry/psalm.xml diff --git a/hooks/OpenTelemetry/src/OpenTelemetryHook.php b/hooks/OpenTelemetry/src/OpenTelemetryHook.php new file mode 100644 index 00000000..790a6c55 --- /dev/null +++ b/hooks/OpenTelemetry/src/OpenTelemetryHook.php @@ -0,0 +1,108 @@ +addHooks(self::$instance); + self::$registeredHook = true; + } + + private static ?OpenTelemetryHook $instance = null; + private static bool $registeredHook = false; + + public function before(HookContext $context, HookHints $hints): ?EvaluationContext + { + return null; + } + + public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void + { + $span = Span::getCurrent(); + + $span->addEvent(self::EVENT_NAME, [ + self::FLAG_KEY => $context->getFlagKey(), + self::FLAG_PROVIDER_NAME => OpenFeatureAPI::getInstance()->getProvider()->getMetadata()->getName(), + self::FLAG_VARIANT => $details->getVariant(), + ]); + } + + public function error(HookContext $context, Throwable $error, HookHints $hints): void + { + $span = Span::getCurrent(); + + $span->recordException($error, [ + self::FLAG_KEY => $context->getFlagKey(), + self::FLAG_PROVIDER_NAME => OpenFeatureAPI::getInstance()->getProvider()->getMetadata()->getName(), + ]); + } + + public function finally(HookContext $context, HookHints $hints): void + { + // no-op + } + + public function supportsFlagValueType(string $flagValueType): bool + { + return true; + } + + /** + * Hooks can be cleared by other means so we can't simply memoize whether a registration has occurred + * + * However if no registration has yet happened then we can absolutely determine that the hook will + * not be registered yet. + */ + private static function isRegisteredInHooks(): bool + { + foreach (OpenFeatureAPI::getInstance()->getHooks() as $hook) { + if ($hook instanceof OpenTelemetryHook) { + return true; + } + } + + return false; + } +} diff --git a/hooks/OpenTelemetry/src/TracerHelper.php b/hooks/OpenTelemetry/src/TracerHelper.php new file mode 100644 index 00000000..795ea2e9 --- /dev/null +++ b/hooks/OpenTelemetry/src/TracerHelper.php @@ -0,0 +1,38 @@ +has(TracerInterface::class)) { + /** @var TracerInterface|null $maybeTracer */ + $maybeTracer = $container->get(TracerInterface::class); + + if ($maybeTracer instanceof TracerInterface) { + return $maybeTracer; + } + } + } + + return Globals::tracerProvider()->getTracer('open-feature/otel-hook', '0.0.1'); + } +} diff --git a/hooks/OpenTelemetry/src/_autoload.php b/hooks/OpenTelemetry/src/_autoload.php new file mode 100644 index 00000000..ac8b3881 --- /dev/null +++ b/hooks/OpenTelemetry/src/_autoload.php @@ -0,0 +1,8 @@ + $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/hooks/OpenTelemetry/tests/integration/OpenTelemetryHookTest.php b/hooks/OpenTelemetry/tests/integration/OpenTelemetryHookTest.php new file mode 100644 index 00000000..fb3a3a9c --- /dev/null +++ b/hooks/OpenTelemetry/tests/integration/OpenTelemetryHookTest.php @@ -0,0 +1,47 @@ +clearHooks(); + + // When + $this->simulateAutoload(); + + // Then + + $this->assertCount(1, $api->getHooks()); + $this->assertInstanceOf(Hook::class, $api->getHooks()[0]); + } + + public function testCanBeRegistered(): void + { + // Given + $api = OpenFeatureAPI::getInstance(); + $api->clearHooks(); + + // When + OpenTelemetryHook::register(); + + // Then + $this->assertCount(1, $api->getHooks()); + $this->assertInstanceOf(Hook::class, $api->getHooks()[0]); + } + + private function simulateAutoload(): void + { + require __DIR__ . '/../../src/_autoload.php'; + } +} diff --git a/hooks/OpenTelemetry/tests/unit/OpenTelemetryHookTest.php b/hooks/OpenTelemetry/tests/unit/OpenTelemetryHookTest.php new file mode 100644 index 00000000..c793d9d4 --- /dev/null +++ b/hooks/OpenTelemetry/tests/unit/OpenTelemetryHookTest.php @@ -0,0 +1,26 @@ +assertNotEmpty($api->getHooks()); + $this->assertInstanceOf(Hook::class, $api->getHooks()[0]); + } +} diff --git a/src/Split/.gitignore b/hooks/Validators/.gitignore similarity index 100% rename from src/Split/.gitignore rename to hooks/Validators/.gitignore diff --git a/hooks/Validators/CHANGELOG.md b/hooks/Validators/CHANGELOG.md new file mode 100644 index 00000000..ddb94333 --- /dev/null +++ b/hooks/Validators/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/validators-hook-v1.0.0...open-feature/validators-hook-1.0.0) (2024-09-04) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) +* **validators-hook:** created new package with validation offering ([#30](https://github.com/open-feature/php-sdk-contrib/issues/30)) ([a2501e6](https://github.com/open-feature/php-sdk-contrib/commit/a2501e6440e8f25ce3231fffd225f5cf13ab5fe4)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) + +## [0.2.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/validators-hook-0.2.0...open-feature/validators-hook-0.2.0) (2023-02-12) + + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) + +## 0.2.0 (2023-01-09) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* **validators-hook:** created new package with validation offering ([#30](https://github.com/open-feature/php-sdk-contrib/issues/30)) ([a2501e6](https://github.com/open-feature/php-sdk-contrib/commit/a2501e6440e8f25ce3231fffd225f5cf13ab5fe4)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) diff --git a/hooks/Validators/README.md b/hooks/Validators/README.md new file mode 100644 index 00000000..135879fd --- /dev/null +++ b/hooks/Validators/README.md @@ -0,0 +1,57 @@ +# OpenFeature Validator Hooks + +[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Latest Stable Version](http://poser.pugx.org/open-feature/validators-hook/v)](https://packagist.org/packages/open-feature/validators-hook) +[![Total Downloads](http://poser.pugx.org/open-feature/validators-hook/downloads)](https://packagist.org/packages/open-feature/validators-hook) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) +[![License](http://poser.pugx.org/open-feature/validators-hook/license)](https://packagist.org/packages/open-feature/validators-hook) + +## Overview + +Validator Hook constructs that provide means to execute validation against resolved feature flag values. + +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/validators-hook +``` + +## Usage + +The following validator hook constructs are available, but more are being worked on over time: + +- `RegexpValidatorHook` + + +```php +use OpenFeature\Hooks\Validators\RegexpValidatorHook; + +$alphanumericValidator = new RegexpValidatorHook('/^[A-Za-z0-9]+$/'); +$hexadecimalValidator = new RegexpValidatorHook('/^[0-9a-f]+$/'); +$asciiValidator = new RegexpValidatorHook('/^[ -~]$/'); + +// this specific invocation will use this validator +$client->resolveStringValue('test-flag', 'deadbeef', null, new EvaluationOptions([$hexadecimalValidator])); +``` + +For more examples, see the [examples](./examples/). + +## Development + +### PHP Versioning + +This library targets PHP version 8.0 and newer. As long as you have any 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`. `composer install` will update the `composer.lock` with the most recent compatible versions. + +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/hooks/Validators/composer.json b/hooks/Validators/composer.json new file mode 100644 index 00000000..9ac24a05 --- /dev/null +++ b/hooks/Validators/composer.json @@ -0,0 +1,133 @@ +{ + "name": "open-feature/validators-hook", + "description": "A validator hooks package for OpenFeature", + "license": "Apache-2.0", + "type": "library", + "keywords": [ + "featureflags", + "featureflagging", + "openfeature", + "validator", + "hook" + ], + "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", + "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", + "mikey179/vfsstream": "v1.6.11", + "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.9.1", + "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\\Hooks\\Validators\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Hooks\\Validators\\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 + }, + "sort-packages": true + }, + "extra": { + "captainhook": { + "force-install": false + } + }, + "scripts": { + "dev:analyze": [ + "@dev:analyze:phpstan", + "@dev:analyze:psalm" + ], + "dev:analyze:phpstan": "phpstan analyse --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "psalm", + "dev:build:clean": "git clean -fX build/", + "dev:lint": [ + "@dev:lint:syntax", + "@dev:lint:style" + ], + "dev:lint:fix": "phpcbf", + "dev:lint:style": "phpcs --colors", + "dev:lint:syntax": "parallel-lint --colors src/ tests/", + "dev:test": [ + "@dev:lint", + "@dev:analyze", + "@dev:test:unit", + "@dev:test:integration" + ], + "dev:test:coverage:ci": "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": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": [ + "@dev:test:unit:setup", + "phpunit --colors=always --testdox --testsuite=unit", + "@dev:test:unit:teardown" + ], + "dev:test:unit:debug": "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 for unit tests...'", + "dev:test:integration": [ + "@dev:test:integration:setup", + "phpunit --colors=always --testdox --testsuite=integration", + "@dev:test:integration:teardown" + ], + "dev:test:integration:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "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/src/Split/examples/.gitignore b/hooks/Validators/examples/.gitignore similarity index 100% rename from src/Split/examples/.gitignore rename to hooks/Validators/examples/.gitignore diff --git a/hooks/Validators/examples/ExampleRegexpValidators/README.md b/hooks/Validators/examples/ExampleRegexpValidators/README.md new file mode 100644 index 00000000..50d8fcc5 --- /dev/null +++ b/hooks/Validators/examples/ExampleRegexpValidators/README.md @@ -0,0 +1,3 @@ +# OpenFeature Validators Hook example + +This example provides an example of using the validators hooks for OpenFeature. diff --git a/hooks/Validators/examples/ExampleRegexpValidators/composer.json b/hooks/Validators/examples/ExampleRegexpValidators/composer.json new file mode 100644 index 00000000..12f72d99 --- /dev/null +++ b/hooks/Validators/examples/ExampleRegexpValidators/composer.json @@ -0,0 +1,27 @@ +{ + "name": "open-feature/validators-hook-example", + "description": "An example of using the validator hooks for OpenFeature", + "type": "project", + "license": "Apache-2.0", + "authors": [ + { + "name": "Tom Carrio", + "email": "tom@carrio.dev" + } + ], + "require": { + "open-feature/sdk": "^1.2.0", + "open-feature/validators-hook": "dev-main" + }, + "repositories": [ + { + "type": "path", + "url": "../../", + "options": { + "versions": { + "open-feature/validators-hook": "dev-main" + } + } + } + ] +} diff --git a/hooks/Validators/examples/ExampleRegexpValidators/src/main.php b/hooks/Validators/examples/ExampleRegexpValidators/src/main.php new file mode 100644 index 00000000..16627b34 --- /dev/null +++ b/hooks/Validators/examples/ExampleRegexpValidators/src/main.php @@ -0,0 +1,31 @@ +getClient('split-example', '1.0'); + +// create some example hook validators + +$alphanumericValidator = new RegexpValidatorHook('/^[A-Za-z0-9]+$/'); +$hexadecimalValidator = new RegexpValidatorHook('/^[0-9a-f]+$/'); +$asciiValidator = new RegexpValidatorHook('/^[ -~]$/'); + +$client->setHooks([ + $alphanumericValidator, + $hexadecimalValidator, + $asciiValidator +]); + +$flagValue = $client->getBooleanDetails('dev.openfeature.example_flag', true, null, null); diff --git a/src/Flagd/phpcs.xml.dist b/hooks/Validators/phpcs.xml.dist similarity index 87% rename from src/Flagd/phpcs.xml.dist rename to hooks/Validators/phpcs.xml.dist index 5ba48465..55d9d3a1 100644 --- a/src/Flagd/phpcs.xml.dist +++ b/hooks/Validators/phpcs.xml.dist @@ -20,9 +20,6 @@ - - - diff --git a/hooks/Validators/phpstan.neon.dist b/hooks/Validators/phpstan.neon.dist new file mode 100644 index 00000000..93c5b2d2 --- /dev/null +++ b/hooks/Validators/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/hooks/Validators/phpunit.xml.dist b/hooks/Validators/phpunit.xml.dist new file mode 100644 index 00000000..9d4740e1 --- /dev/null +++ b/hooks/Validators/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + ./tests/unit + + + ./tests/integration + + + + + + ./src + + + + + + + + diff --git a/src/Split/psalm-baseline.xml b/hooks/Validators/psalm-baseline.xml similarity index 100% rename from src/Split/psalm-baseline.xml rename to hooks/Validators/psalm-baseline.xml diff --git a/src/Flagd/psalm.xml b/hooks/Validators/psalm.xml similarity index 84% rename from src/Flagd/psalm.xml rename to hooks/Validators/psalm.xml index f1c9c37a..c3e6c03c 100644 --- a/src/Flagd/psalm.xml +++ b/hooks/Validators/psalm.xml @@ -11,8 +11,6 @@ - - diff --git a/hooks/Validators/src/Exceptions/ValidationException.php b/hooks/Validators/src/Exceptions/ValidationException.php new file mode 100644 index 00000000..285236c7 --- /dev/null +++ b/hooks/Validators/src/Exceptions/ValidationException.php @@ -0,0 +1,18 @@ +regexp = self::validateRegexp($regexp); + } + + public function before(HookContext $context, HookHints $hints): ?EvaluationContext + { + return null; + } + + public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void + { + /** @var string $resolvedValue */ + $resolvedValue = $details->getValue(); + + if ($this->testResolvedValue($resolvedValue)) { + return; + } + + throw new ValidationException(); + } + + public function error(HookContext $context, Throwable $error, HookHints $hints): void + { + // no-op + } + + public function finally(HookContext $context, HookHints $hints): void + { + // no-op + } + + public function supportsFlagValueType(string $flagValueType): bool + { + return $flagValueType === FlagValueType::STRING; + } + + private function testResolvedValue(string $resolvedValue): bool + { + return preg_match($this->regexp, $resolvedValue) === 1; + } + + private static function validateRegexp(string $regexp): string + { + if (self::isValidRegexp($regexp)) { + return $regexp; + } + + throw new InvalidRegularExpressionException($regexp); + } + + private static function isValidRegexp(string $regexp): bool + { + return is_int(@preg_match($regexp, '')); + } +} diff --git a/hooks/Validators/tests/TestCase.php b/hooks/Validators/tests/TestCase.php new file mode 100644 index 00000000..584c5e69 --- /dev/null +++ b/hooks/Validators/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/hooks/Validators/tests/integration/RegexpValidatorHookTest.php b/hooks/Validators/tests/integration/RegexpValidatorHookTest.php new file mode 100644 index 00000000..d174818d --- /dev/null +++ b/hooks/Validators/tests/integration/RegexpValidatorHookTest.php @@ -0,0 +1,19 @@ +assertInstanceOf(Hook::class, $hook); + } +} diff --git a/hooks/Validators/tests/unit/RegexpValidatorHookTest.php b/hooks/Validators/tests/unit/RegexpValidatorHookTest.php new file mode 100644 index 00000000..a1caa6de --- /dev/null +++ b/hooks/Validators/tests/unit/RegexpValidatorHookTest.php @@ -0,0 +1,111 @@ +assertInstanceOf(Hook::class, $hook); + } + + public function testCannotCreateInvalidRegexpForHook(): void + { + $this->expectException(InvalidRegularExpressionException::class); + + new RegexpValidatorHook('/[\\]/'); + } + + public function testAlphanumericRegexpHookPasses(): void + { + $hook = new RegexpValidatorHook('/^[A-Za-z0-9]+$/'); + + $this->assertInstanceOf(Hook::class, $hook); + + $this->executeHook($hook, 'Abc123'); + } + + public function testAlphanumericRegexpHookFails(): void + { + $hook = new RegexpValidatorHook('/^[A-Za-z0-9]+$/'); + + $this->assertInstanceOf(Hook::class, $hook); + + $this->expectException(ValidationException::class); + + $this->executeHook($hook, 'This, a sentence, has other invalid characters.'); + } + + public function testHexadecimalRegexpHookPasses(): void + { + $hook = new RegexpValidatorHook('/^[0-9a-f]+$/'); + + $this->assertInstanceOf(Hook::class, $hook); + + $this->executeHook($hook, 'deadbeef007'); + } + + public function testHexadecimalRegexpHookFails(): void + { + $hook = new RegexpValidatorHook('/^[0-9a-f]+$/'); + + $this->assertInstanceOf(Hook::class, $hook); + + $this->expectException(ValidationException::class); + + $this->executeHook($hook, '0123456789abcdefg'); + } + + public function testAsciiRegexpHookPasses(): void + { + $hook = new RegexpValidatorHook('/^[ -~]+$/'); + + $this->assertInstanceOf(Hook::class, $hook); + + $this->executeHook($hook, 'Only ASCII characters get used here: See?'); + } + + public function testAsciiRegexpHookFails(): void + { + $hook = new RegexpValidatorHook('/^[ -~]+$/'); + + $this->assertInstanceOf(Hook::class, $hook); + + $this->expectException(ValidationException::class); + + $this->executeHook($hook, '死'); + } + + private function executeHook(Hook $hook, string $resolvedValue): void + { + $ctx = HookContextFactory::from( + 'any-key', + FlagValueType::STRING, + 'default-value', + null, + new Metadata('client'), + new Metadata('provider'), + ); + + $details = ResolutionDetailsFactory::fromSuccess($resolvedValue); + + $nullHints = new HookHints(); + + $hook->after($ctx, $details, $nullHints); + } +} diff --git a/providers/.gitkeep b/providers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/providers/CloudBees/.gitignore b/providers/CloudBees/.gitignore new file mode 100644 index 00000000..e1efd914 --- /dev/null +++ b/providers/CloudBees/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/build \ No newline at end of file diff --git a/providers/CloudBees/CHANGELOG.md b/providers/CloudBees/CHANGELOG.md new file mode 100644 index 00000000..a613ef92 --- /dev/null +++ b/providers/CloudBees/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +## [1.1.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/cloudbees-provider-1.0.0...open-feature/cloudbees-provider-1.1.0) (2025-01-24) + + +### Features + +* upgrade rollout/rox to 6.0.1 ([#119](https://github.com/open-feature/php-sdk-contrib/issues/119)) ([e3aef1a](https://github.com/open-feature/php-sdk-contrib/commit/e3aef1ad277d8fe90d3f4ab6ddb3e509dafb45f8)) + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/cloudbees-provider-v1.0.0...open-feature/cloudbees-provider-1.0.0) (2024-09-04) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Bug Fixes + +* declaration for provider name ([#105](https://github.com/open-feature/php-sdk-contrib/issues/105)) ([42919fd](https://github.com/open-feature/php-sdk-contrib/commit/42919fdb8a2d3992ac529ddd7e90d6b99b340732)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) + +## 0.3.0 (2023-01-09) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) diff --git a/src/CloudBees/README.md b/providers/CloudBees/README.md similarity index 94% rename from src/CloudBees/README.md rename to providers/CloudBees/README.md index 806937bb..1cc42522 100644 --- a/src/CloudBees/README.md +++ b/providers/CloudBees/README.md @@ -3,7 +3,7 @@ [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Latest Stable Version](http://poser.pugx.org/open-feature/cloudbees-provider/v)](https://packagist.org/packages/open-feature/cloudbees-provider) [![Total Downloads](http://poser.pugx.org/open-feature/cloudbees-provider/downloads)](https://packagist.org/packages/open-feature/cloudbees-provider) -![PHP 7.4+](https://img.shields.io/badge/php->=7.4-blue.svg) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) [![License](http://poser.pugx.org/open-feature/cloudbees-provider/license)](https://packagist.org/packages/open-feature/cloudbees-provider) ## Overview @@ -18,8 +18,8 @@ There is [an open issue](https://github.com/rollout/rox-php/issues/37) with obje ## Installation -``` -$ composer require open-feature/cloudbees-provider // installs the latest version +```sh +composer require open-feature/cloudbees-provider ``` ## Usage @@ -52,7 +52,7 @@ CloudBeesProvider::shutdown(); ### PHP Versioning -This library targets PHP version 7.4 and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK. +This library targets PHP version 8.0 and newer. As long as you have any 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`. @@ -72,4 +72,4 @@ The integration test suite utilizes a locally available mock server for Rollout The docker image is published under `rollout/roxy`. -For more information on Roxy, see [the documentation](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/debugging/microservices-automated-testing-and-local-development#_running_roxy). \ No newline at end of file +For more information on Roxy, see [the documentation](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/debugging/microservices-automated-testing-and-local-development#_running_roxy). diff --git a/src/CloudBees/composer.json b/providers/CloudBees/composer.json similarity index 96% rename from src/CloudBees/composer.json rename to providers/CloudBees/composer.json index e855b74f..c4946acc 100644 --- a/src/CloudBees/composer.json +++ b/providers/CloudBees/composer.json @@ -23,9 +23,9 @@ } ], "require": { - "php": "^7.4 || ^8", - "open-feature/sdk": "^1.0.0", - "rollout/rox": "^5.0" + "php": "^8", + "open-feature/sdk": "^2.0", + "rollout/rox": "^6.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.25", @@ -37,10 +37,10 @@ "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "~1.9.0", + "phpstan/phpstan": "~1.10.0", "phpstan/phpstan-mockery": "^1.0", "phpstan/phpstan-phpunit": "^1.1", - "psalm/plugin-mockery": "^0.9.1", + "psalm/plugin-mockery": "^0.11.0", "psalm/plugin-phpunit": "^0.18.0", "ramsey/coding-standard": "^2.0.3", "ramsey/composer-repl": "^1.4", diff --git a/providers/CloudBees/examples/.gitignore b/providers/CloudBees/examples/.gitignore new file mode 100644 index 00000000..149cf08d --- /dev/null +++ b/providers/CloudBees/examples/.gitignore @@ -0,0 +1,2 @@ +/*/vendor +/*/composer.lock \ No newline at end of file diff --git a/src/CloudBees/examples/CloudBees/README.md b/providers/CloudBees/examples/CloudBees/README.md similarity index 100% rename from src/CloudBees/examples/CloudBees/README.md rename to providers/CloudBees/examples/CloudBees/README.md diff --git a/src/CloudBees/examples/CloudBees/composer.json b/providers/CloudBees/examples/CloudBees/composer.json similarity index 93% rename from src/CloudBees/examples/CloudBees/composer.json rename to providers/CloudBees/examples/CloudBees/composer.json index 9d2e626d..7ff5ca9e 100644 --- a/src/CloudBees/examples/CloudBees/composer.json +++ b/providers/CloudBees/examples/CloudBees/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "open-feature/sdk": "^0.0.5", + "open-feature/sdk": "^1.2.0", "open-feature/cloudbees-provider": "^0.0.1" } } diff --git a/src/CloudBees/examples/CloudBees/src/main.php b/providers/CloudBees/examples/CloudBees/src/main.php similarity index 100% rename from src/CloudBees/examples/CloudBees/src/main.php rename to providers/CloudBees/examples/CloudBees/src/main.php diff --git a/providers/CloudBees/phpcs.xml.dist b/providers/CloudBees/phpcs.xml.dist new file mode 100644 index 00000000..55d9d3a1 --- /dev/null +++ b/providers/CloudBees/phpcs.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + ./src + ./tests + + */tests/fixtures/* + */tests/*/fixtures/* + + + + + + + + + + + + + diff --git a/providers/CloudBees/phpstan.neon.dist b/providers/CloudBees/phpstan.neon.dist new file mode 100644 index 00000000..93c5b2d2 --- /dev/null +++ b/providers/CloudBees/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/CloudBees/phpunit.xml.dist b/providers/CloudBees/phpunit.xml.dist new file mode 100644 index 00000000..9d4740e1 --- /dev/null +++ b/providers/CloudBees/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + ./tests/unit + + + ./tests/integration + + + + + + ./src + + + + + + + + diff --git a/providers/CloudBees/psalm-baseline.xml b/providers/CloudBees/psalm-baseline.xml new file mode 100644 index 00000000..ceaa5778 --- /dev/null +++ b/providers/CloudBees/psalm-baseline.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/providers/CloudBees/psalm.xml b/providers/CloudBees/psalm.xml new file mode 100644 index 00000000..c3e6c03c --- /dev/null +++ b/providers/CloudBees/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/CloudBees/scripts/scaffold-integration-tests.py b/providers/CloudBees/scripts/scaffold-integration-tests.py similarity index 100% rename from src/CloudBees/scripts/scaffold-integration-tests.py rename to providers/CloudBees/scripts/scaffold-integration-tests.py diff --git a/src/CloudBees/scripts/seed-data.json b/providers/CloudBees/scripts/seed-data.json similarity index 100% rename from src/CloudBees/scripts/seed-data.json rename to providers/CloudBees/scripts/seed-data.json diff --git a/src/CloudBees/src/CloudBeesProvider.php b/providers/CloudBees/src/CloudBeesProvider.php similarity index 89% rename from src/CloudBees/src/CloudBeesProvider.php rename to providers/CloudBees/src/CloudBeesProvider.php index 392a8c7c..62f42226 100644 --- a/src/CloudBees/src/CloudBeesProvider.php +++ b/providers/CloudBees/src/CloudBeesProvider.php @@ -26,7 +26,7 @@ class CloudBeesProvider extends AbstractProvider implements Provider { - protected const NAME = 'CloudBeesProvider'; + protected static string $NAME = 'CloudBeesProvider'; private static ?CloudBeesProvider $instance = null; @@ -96,7 +96,7 @@ public function resolveFloatValue(string $flagKey, float $defaultValue, ?Evaluat /** * @param mixed[] $defaultValue */ - public function resolveObjectValue(string $flagKey, $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { return $this->resolve( $defaultValue, @@ -108,22 +108,26 @@ public function resolveObjectValue(string $flagKey, $defaultValue, ?EvaluationCo /** * @param bool|string|int|float|mixed[] $defaultValue */ - private function resolve($defaultValue, callable $fn, ?callable $transformer = null): ResolutionDetails + private function resolve(mixed $defaultValue, callable $fn, ?callable $transformer = null): ResolutionDetails { if (is_null($transformer)) { $transformer = new IdentityTransformer(); } try { - /** @var bool|string|int|float $value */ + /** + * @var bool|string|int|float $value +*/ $value = call_user_func($fn); - /** @var bool|string|int|float|mixed[] $transformed */ + /** + * @var bool|string|int|float|mixed[] $transformed +*/ $transformed = call_user_func($transformer, $value); return (new ResolutionDetailsBuilder()) - ->withValue($transformed) - ->build(); + ->withValue($transformed) + ->build(); } catch (Throwable $err) { $detailsBuilder = new ResolutionDetailsBuilder(); diff --git a/src/CloudBees/src/context/ContextAdapter.php b/providers/CloudBees/src/context/ContextAdapter.php similarity index 100% rename from src/CloudBees/src/context/ContextAdapter.php rename to providers/CloudBees/src/context/ContextAdapter.php diff --git a/src/CloudBees/src/errors/InvalidJsonTypeException.php b/providers/CloudBees/src/errors/InvalidJsonTypeException.php similarity index 100% rename from src/CloudBees/src/errors/InvalidJsonTypeException.php rename to providers/CloudBees/src/errors/InvalidJsonTypeException.php diff --git a/src/CloudBees/src/errors/JsonParseException.php b/providers/CloudBees/src/errors/JsonParseException.php similarity index 100% rename from src/CloudBees/src/errors/JsonParseException.php rename to providers/CloudBees/src/errors/JsonParseException.php diff --git a/src/CloudBees/src/transformers/IdentityTransformer.php b/providers/CloudBees/src/transformers/IdentityTransformer.php similarity index 85% rename from src/CloudBees/src/transformers/IdentityTransformer.php rename to providers/CloudBees/src/transformers/IdentityTransformer.php index 430c305e..071453f4 100644 --- a/src/CloudBees/src/transformers/IdentityTransformer.php +++ b/providers/CloudBees/src/transformers/IdentityTransformer.php @@ -11,7 +11,7 @@ class IdentityTransformer * * @return bool|string|int|float|mixed[] */ - public function __invoke($x) + public function __invoke(mixed $x): mixed { return $x; } diff --git a/src/CloudBees/src/transformers/JsonTransformer.php b/providers/CloudBees/src/transformers/JsonTransformer.php similarity index 100% rename from src/CloudBees/src/transformers/JsonTransformer.php rename to providers/CloudBees/src/transformers/JsonTransformer.php diff --git a/src/CloudBees/tests/TestCase.php b/providers/CloudBees/tests/TestCase.php similarity index 100% rename from src/CloudBees/tests/TestCase.php rename to providers/CloudBees/tests/TestCase.php diff --git a/src/CloudBees/tests/integration/CloudBeesProviderTest.php b/providers/CloudBees/tests/integration/CloudBeesProviderTest.php similarity index 97% rename from src/CloudBees/tests/integration/CloudBeesProviderTest.php rename to providers/CloudBees/tests/integration/CloudBeesProviderTest.php index 588d2d4f..5c4cf4f6 100644 --- a/src/CloudBees/tests/integration/CloudBeesProviderTest.php +++ b/providers/CloudBees/tests/integration/CloudBeesProviderTest.php @@ -42,6 +42,7 @@ public function testCanBeInstantiated(): void $this->assertNotNull($instance); $this->assertInstanceOf(CloudBeesProvider::class, $instance); $this->assertInstanceOf(Provider::class, $instance); + $this->assertEquals('CloudBeesProvider', $instance->getMetadata()->getName()); } public function testCanResolveBool(): void diff --git a/src/CloudBees/tests/unit/JsonTransformerTest.php b/providers/CloudBees/tests/unit/JsonTransformerTest.php similarity index 100% rename from src/CloudBees/tests/unit/JsonTransformerTest.php rename to providers/CloudBees/tests/unit/JsonTransformerTest.php diff --git a/providers/Flagd/.gitignore b/providers/Flagd/.gitignore new file mode 100644 index 00000000..e1efd914 --- /dev/null +++ b/providers/Flagd/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/build \ No newline at end of file diff --git a/providers/Flagd/CHANGELOG.md b/providers/Flagd/CHANGELOG.md new file mode 100644 index 00000000..18bbdc3a --- /dev/null +++ b/providers/Flagd/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/flagd-provider-v1.0.0...open-feature/flagd-provider-1.0.0) (2024-09-04) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) +* **flagd:** support newer psr/log ([bf6f449](https://github.com/open-feature/php-sdk-contrib/commit/bf6f449ea790c0021698d33ebb59fab576e23341)) +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Bug Fixes + +* declaration for provider name ([#105](https://github.com/open-feature/php-sdk-contrib/issues/105)) ([42919fd](https://github.com/open-feature/php-sdk-contrib/commit/42919fdb8a2d3992ac529ddd7e90d6b99b340732)) +* **deps:** update dependency psr/http-message to v2 ([#72](https://github.com/open-feature/php-sdk-contrib/issues/72)) ([bb18c04](https://github.com/open-feature/php-sdk-contrib/commit/bb18c04af7280c71c013d5a2e11903506b815f8b)) +* flagd documentation ([#83](https://github.com/open-feature/php-sdk-contrib/issues/83)) ([684367c](https://github.com/open-feature/php-sdk-contrib/commit/684367cd14b6b55cded5accdc5b92eb00986404c)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) + +## [0.4.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/flagd-provider-0.4.0...open-feature/flagd-provider-0.4.0) (2023-02-12) + + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) + +## 0.4.0 (2023-01-09) + + +### ⚠ BREAKING CHANGES + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) diff --git a/src/Flagd/README.md b/providers/Flagd/README.md similarity index 69% rename from src/Flagd/README.md rename to providers/Flagd/README.md index 65196b04..d5d09f75 100644 --- a/src/Flagd/README.md +++ b/providers/Flagd/README.md @@ -3,7 +3,7 @@ [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Latest Stable Version](http://poser.pugx.org/open-feature/flagd-provider/v)](https://packagist.org/packages/open-feature/flagd-provider) [![Total Downloads](http://poser.pugx.org/open-feature/flagd-provider/downloads)](https://packagist.org/packages/open-feature/flagd-provider) -![PHP 7.4+](https://img.shields.io/badge/php->=7.4-blue.svg) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) [![License](http://poser.pugx.org/open-feature/flagd-provider/license)](https://packagist.org/packages/open-feature/flagd-provider) ## Overview @@ -14,8 +14,8 @@ This package also builds on various PSRs (PHP Standards Recommendations) such as ## Installation -``` -$ composer require open-feature/flagd-provider // installs the latest version +```sh +composer require open-feature/flagd-provider ``` ## Usage @@ -37,7 +37,7 @@ OpenFeatureAPI::setProvider(new FlagdProvider([ 'host' => 'localhost', 'port' => 8013, 'secure' => true, - 'http' => [ + 'httpConfig' => [ 'client' => $client, 'requestFactory' => $requestFactory, 'streamFactory' => $streamFactory, @@ -45,20 +45,24 @@ OpenFeatureAPI::setProvider(new FlagdProvider([ ])); ``` -- **protocol**: "http" | "grpc" _(defaults to http)_ +- **protocol**: "http" _(defaults to http)_ - **host**: string _(defaults to "localhost")_ - **port**: number _(defaults to 8013)_ - **secure**: true | false _(defaults to false)_ -- **http**: An array or `HttpConfig` object, providing implementations for PSR interfaces +- **httpConfig**: An array or `HttpConfig` object, providing implementations for PSR interfaces - **client**: a `ClientInterface` implementation - **requestFactory**: a `RequestFactoryInterface` implementation - **streamFactory**: a `StreamFactoryInterface` implementation +### gRPC vs HTTP + +The Flagd server is gRPC but offers gRPC Web endpoints that can be accessed over HTTP. The latter is used by the current implementation of the Flagd provider, with future development planned to implement a gRPC native provider option. There are certain flexibilities around HTTP with PHP available, whereas gRPC is an opinionated code-generation strategy, but they are both useful and gRPC native may provide better performance over certain sync/async scenarios. An additional goal will be to provide benchmarking of the Flagd provider's protocol for various scenarios so this decision can be made more easily by consumers of the provider. + ## Development ### PHP Versioning -This library targets PHP version 7.4 and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK. +This library targets PHP version and newer. As long as you have any 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`. @@ -70,4 +74,4 @@ We value having as few runtime dependencies as possible. The addition of any dep ### Testing -Run tests with `composer run test`. \ No newline at end of file +Run tests with `composer run test`. diff --git a/src/Flagd/composer.json b/providers/Flagd/composer.json similarity index 96% rename from src/Flagd/composer.json rename to providers/Flagd/composer.json index 68ab618f..59b95c82 100644 --- a/src/Flagd/composer.json +++ b/providers/Flagd/composer.json @@ -21,13 +21,13 @@ } ], "require": { - "php": "^7.4 || ^8", - "open-feature/sdk": "^1.1.0", + "php": "^8", + "open-feature/sdk": "^2.0", "php-http/httplug": "^2.3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "psr/log": "^1.1" + "psr/http-message": "^2.0", + "psr/log": "^2.0 || ^3.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.25", @@ -39,10 +39,10 @@ "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "~1.9.0", + "phpstan/phpstan": "~1.10.0", "phpstan/phpstan-mockery": "^1.0", "phpstan/phpstan-phpunit": "^1.1", - "psalm/plugin-mockery": "^0.9.1", + "psalm/plugin-mockery": "^0.11.0", "psalm/plugin-phpunit": "^0.18.0", "ramsey/coding-standard": "^2.0.3", "ramsey/composer-repl": "^1.4", diff --git a/providers/Flagd/examples/.gitignore b/providers/Flagd/examples/.gitignore new file mode 100644 index 00000000..149cf08d --- /dev/null +++ b/providers/Flagd/examples/.gitignore @@ -0,0 +1,2 @@ +/*/vendor +/*/composer.lock \ No newline at end of file diff --git a/providers/Flagd/examples/Http/README.md b/providers/Flagd/examples/Http/README.md new file mode 100644 index 00000000..de296d72 --- /dev/null +++ b/providers/Flagd/examples/Http/README.md @@ -0,0 +1 @@ +# OpenFeature flagd HTTP example \ No newline at end of file diff --git a/src/Flagd/examples/Http/composer.json b/providers/Flagd/examples/Http/composer.json similarity index 92% rename from src/Flagd/examples/Http/composer.json rename to providers/Flagd/examples/Http/composer.json index 767fed55..317e70d5 100644 --- a/src/Flagd/examples/Http/composer.json +++ b/providers/Flagd/examples/Http/composer.json @@ -15,6 +15,6 @@ } ], "require": { - "open-feature/sdk": "^0.0.5" + "open-feature/sdk": "^1.2.0" } } diff --git a/src/Flagd/examples/Http/src/main.php b/providers/Flagd/examples/Http/src/main.php similarity index 100% rename from src/Flagd/examples/Http/src/main.php rename to providers/Flagd/examples/Http/src/main.php diff --git a/providers/Flagd/phpcs.xml.dist b/providers/Flagd/phpcs.xml.dist new file mode 100644 index 00000000..55d9d3a1 --- /dev/null +++ b/providers/Flagd/phpcs.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + ./src + ./tests + + */tests/fixtures/* + */tests/*/fixtures/* + + + + + + + + + + + + + diff --git a/src/Flagd/phpstan.neon.dist b/providers/Flagd/phpstan.neon.dist similarity index 100% rename from src/Flagd/phpstan.neon.dist rename to providers/Flagd/phpstan.neon.dist diff --git a/src/Flagd/phpunit.xml.dist b/providers/Flagd/phpunit.xml.dist similarity index 100% rename from src/Flagd/phpunit.xml.dist rename to providers/Flagd/phpunit.xml.dist diff --git a/src/Flagd/proto/README.md b/providers/Flagd/proto/README.md similarity index 100% rename from src/Flagd/proto/README.md rename to providers/Flagd/proto/README.md diff --git a/src/Flagd/proto/php/GPBMetadata/Schema/V1/Schema.php b/providers/Flagd/proto/php/GPBMetadata/Schema/V1/Schema.php similarity index 100% rename from src/Flagd/proto/php/GPBMetadata/Schema/V1/Schema.php rename to providers/Flagd/proto/php/GPBMetadata/Schema/V1/Schema.php diff --git a/src/Flagd/proto/php/Schema/V1/EventStreamResponse.php b/providers/Flagd/proto/php/Schema/V1/EventStreamResponse.php similarity index 100% rename from src/Flagd/proto/php/Schema/V1/EventStreamResponse.php rename to providers/Flagd/proto/php/Schema/V1/EventStreamResponse.php diff --git a/src/Flagd/proto/php/Schema/V1/ResolveBooleanRequest.php b/providers/Flagd/proto/php/Schema/V1/ResolveBooleanRequest.php similarity index 87% rename from src/Flagd/proto/php/Schema/V1/ResolveBooleanRequest.php rename to providers/Flagd/proto/php/Schema/V1/ResolveBooleanRequest.php index 70bea8da..9ed905e8 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveBooleanRequest.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveBooleanRequest.php @@ -22,7 +22,7 @@ class ResolveBooleanRequest extends \Google\Protobuf\Internal\Message */ protected $flag_key = ''; /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; */ @@ -37,7 +37,7 @@ class ResolveBooleanRequest extends \Google\Protobuf\Internal\Message * @type string $flag_key * Flag key of the requested flag. * @type \Google\Protobuf\Struct $context - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * } */ public function __construct($data = NULL) { @@ -72,7 +72,7 @@ public function setFlagKey($var) } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @return \Google\Protobuf\Struct|null @@ -93,7 +93,7 @@ public function clearContext() } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @param \Google\Protobuf\Struct $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveBooleanResponse.php b/providers/Flagd/proto/php/Schema/V1/ResolveBooleanResponse.php similarity index 88% rename from src/Flagd/proto/php/Schema/V1/ResolveBooleanResponse.php rename to providers/Flagd/proto/php/Schema/V1/ResolveBooleanResponse.php index acee0ed2..a2edf802 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveBooleanResponse.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveBooleanResponse.php @@ -22,7 +22,7 @@ class ResolveBooleanResponse extends \Google\Protobuf\Internal\Message */ protected $value = false; /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; */ @@ -43,7 +43,7 @@ class ResolveBooleanResponse extends \Google\Protobuf\Internal\Message * @type bool $value * The response value of the boolean flag evaluation, will be unset in the case of error. * @type string $reason - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * @type string $variant * The variant name of the returned flag value. * } @@ -80,7 +80,7 @@ public function setValue($var) } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @return string @@ -91,7 +91,7 @@ public function getReason() } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @param string $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveFloatRequest.php b/providers/Flagd/proto/php/Schema/V1/ResolveFloatRequest.php similarity index 87% rename from src/Flagd/proto/php/Schema/V1/ResolveFloatRequest.php rename to providers/Flagd/proto/php/Schema/V1/ResolveFloatRequest.php index 87194e02..6555908e 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveFloatRequest.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveFloatRequest.php @@ -22,7 +22,7 @@ class ResolveFloatRequest extends \Google\Protobuf\Internal\Message */ protected $flag_key = ''; /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; */ @@ -37,7 +37,7 @@ class ResolveFloatRequest extends \Google\Protobuf\Internal\Message * @type string $flag_key * Flag key of the requested flag. * @type \Google\Protobuf\Struct $context - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * } */ public function __construct($data = NULL) { @@ -72,7 +72,7 @@ public function setFlagKey($var) } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @return \Google\Protobuf\Struct|null @@ -93,7 +93,7 @@ public function clearContext() } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @param \Google\Protobuf\Struct $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveFloatResponse.php b/providers/Flagd/proto/php/Schema/V1/ResolveFloatResponse.php similarity index 88% rename from src/Flagd/proto/php/Schema/V1/ResolveFloatResponse.php rename to providers/Flagd/proto/php/Schema/V1/ResolveFloatResponse.php index 1b96a349..3cc01526 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveFloatResponse.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveFloatResponse.php @@ -22,7 +22,7 @@ class ResolveFloatResponse extends \Google\Protobuf\Internal\Message */ protected $value = 0.0; /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; */ @@ -43,7 +43,7 @@ class ResolveFloatResponse extends \Google\Protobuf\Internal\Message * @type float $value * The response value of the float flag evaluation, will be empty in the case of error. * @type string $reason - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * @type string $variant * The variant name of the returned flag value. * } @@ -80,7 +80,7 @@ public function setValue($var) } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @return string @@ -91,7 +91,7 @@ public function getReason() } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @param string $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveIntRequest.php b/providers/Flagd/proto/php/Schema/V1/ResolveIntRequest.php similarity index 87% rename from src/Flagd/proto/php/Schema/V1/ResolveIntRequest.php rename to providers/Flagd/proto/php/Schema/V1/ResolveIntRequest.php index 71b11647..ce8d2b9b 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveIntRequest.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveIntRequest.php @@ -22,7 +22,7 @@ class ResolveIntRequest extends \Google\Protobuf\Internal\Message */ protected $flag_key = ''; /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; */ @@ -37,7 +37,7 @@ class ResolveIntRequest extends \Google\Protobuf\Internal\Message * @type string $flag_key * Flag key of the requested flag. * @type \Google\Protobuf\Struct $context - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * } */ public function __construct($data = NULL) { @@ -72,7 +72,7 @@ public function setFlagKey($var) } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @return \Google\Protobuf\Struct|null @@ -93,7 +93,7 @@ public function clearContext() } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @param \Google\Protobuf\Struct $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveIntResponse.php b/providers/Flagd/proto/php/Schema/V1/ResolveIntResponse.php similarity index 88% rename from src/Flagd/proto/php/Schema/V1/ResolveIntResponse.php rename to providers/Flagd/proto/php/Schema/V1/ResolveIntResponse.php index 556b6c35..5e774eff 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveIntResponse.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveIntResponse.php @@ -22,7 +22,7 @@ class ResolveIntResponse extends \Google\Protobuf\Internal\Message */ protected $value = 0; /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; */ @@ -43,7 +43,7 @@ class ResolveIntResponse extends \Google\Protobuf\Internal\Message * @type int|string $value * The response value of the int flag evaluation, will be unset in the case of error. * @type string $reason - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * @type string $variant * The variant name of the returned flag value. * } @@ -80,7 +80,7 @@ public function setValue($var) } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @return string @@ -91,7 +91,7 @@ public function getReason() } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @param string $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveObjectRequest.php b/providers/Flagd/proto/php/Schema/V1/ResolveObjectRequest.php similarity index 87% rename from src/Flagd/proto/php/Schema/V1/ResolveObjectRequest.php rename to providers/Flagd/proto/php/Schema/V1/ResolveObjectRequest.php index d2428b20..8b858214 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveObjectRequest.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveObjectRequest.php @@ -22,7 +22,7 @@ class ResolveObjectRequest extends \Google\Protobuf\Internal\Message */ protected $flag_key = ''; /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; */ @@ -37,7 +37,7 @@ class ResolveObjectRequest extends \Google\Protobuf\Internal\Message * @type string $flag_key * Flag key of the requested flag. * @type \Google\Protobuf\Struct $context - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * } */ public function __construct($data = NULL) { @@ -72,7 +72,7 @@ public function setFlagKey($var) } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @return \Google\Protobuf\Struct|null @@ -93,7 +93,7 @@ public function clearContext() } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @param \Google\Protobuf\Struct $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveObjectResponse.php b/providers/Flagd/proto/php/Schema/V1/ResolveObjectResponse.php similarity index 90% rename from src/Flagd/proto/php/Schema/V1/ResolveObjectResponse.php rename to providers/Flagd/proto/php/Schema/V1/ResolveObjectResponse.php index 6f38a5c9..04804eb4 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveObjectResponse.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveObjectResponse.php @@ -23,7 +23,7 @@ class ResolveObjectResponse extends \Google\Protobuf\Internal\Message */ protected $value = null; /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; */ @@ -45,7 +45,7 @@ class ResolveObjectResponse extends \Google\Protobuf\Internal\Message * The response value of the object flag evaluation, will be unset in the case of error. * NOTE: This structure will need to be decoded from google/protobuf/struct.proto before it is returned to the SDK * @type string $reason - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * @type string $variant * The variant name of the returned flag value. * } @@ -94,7 +94,7 @@ public function setValue($var) } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @return string @@ -105,7 +105,7 @@ public function getReason() } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @param string $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveStringRequest.php b/providers/Flagd/proto/php/Schema/V1/ResolveStringRequest.php similarity index 87% rename from src/Flagd/proto/php/Schema/V1/ResolveStringRequest.php rename to providers/Flagd/proto/php/Schema/V1/ResolveStringRequest.php index 7cde269d..d3c394b1 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveStringRequest.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveStringRequest.php @@ -22,7 +22,7 @@ class ResolveStringRequest extends \Google\Protobuf\Internal\Message */ protected $flag_key = ''; /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; */ @@ -37,7 +37,7 @@ class ResolveStringRequest extends \Google\Protobuf\Internal\Message * @type string $flag_key * Flag key of the requested flag. * @type \Google\Protobuf\Struct $context - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * } */ public function __construct($data = NULL) { @@ -72,7 +72,7 @@ public function setFlagKey($var) } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @return \Google\Protobuf\Struct|null @@ -93,7 +93,7 @@ public function clearContext() } /** - * Object structure describing the EvaluationContext used in the flag evaluation, see https://docs.openfeature.dev/docs/reference/concepts/evaluation-context + * Object structure describing the EvaluationContext used in the flag evaluation, see https://openfeature.dev/docs/reference/concepts/evaluation-context * * Generated from protobuf field .google.protobuf.Struct context = 2 [json_name = "context"]; * @param \Google\Protobuf\Struct $var diff --git a/src/Flagd/proto/php/Schema/V1/ResolveStringResponse.php b/providers/Flagd/proto/php/Schema/V1/ResolveStringResponse.php similarity index 88% rename from src/Flagd/proto/php/Schema/V1/ResolveStringResponse.php rename to providers/Flagd/proto/php/Schema/V1/ResolveStringResponse.php index bd256357..b72a5ce9 100644 --- a/src/Flagd/proto/php/Schema/V1/ResolveStringResponse.php +++ b/providers/Flagd/proto/php/Schema/V1/ResolveStringResponse.php @@ -22,7 +22,7 @@ class ResolveStringResponse extends \Google\Protobuf\Internal\Message */ protected $value = ''; /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; */ @@ -43,7 +43,7 @@ class ResolveStringResponse extends \Google\Protobuf\Internal\Message * @type string $value * The response value of the string flag evaluation, will be unset in the case of error. * @type string $reason - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * @type string $variant * The variant name of the returned flag value. * } @@ -80,7 +80,7 @@ public function setValue($var) } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @return string @@ -91,7 +91,7 @@ public function getReason() } /** - * The reason for the given return value, see https://docs.openfeature.dev/docs/specification/types#resolution-details + * The reason for the given return value, see https://openfeature.dev/docs/specification/types#resolution-details * * Generated from protobuf field string reason = 2 [json_name = "reason"]; * @param string $var diff --git a/src/Flagd/proto/php/Schema/V1/ServiceClient.php b/providers/Flagd/proto/php/Schema/V1/ServiceClient.php similarity index 100% rename from src/Flagd/proto/php/Schema/V1/ServiceClient.php rename to providers/Flagd/proto/php/Schema/V1/ServiceClient.php diff --git a/providers/Flagd/psalm-baseline.xml b/providers/Flagd/psalm-baseline.xml new file mode 100644 index 00000000..ceaa5778 --- /dev/null +++ b/providers/Flagd/psalm-baseline.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/providers/Flagd/psalm.xml b/providers/Flagd/psalm.xml new file mode 100644 index 00000000..c3e6c03c --- /dev/null +++ b/providers/Flagd/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/Flagd/schemas b/providers/Flagd/schemas similarity index 100% rename from src/Flagd/schemas rename to providers/Flagd/schemas diff --git a/src/Flagd/src/FlagdProvider.php b/providers/Flagd/src/FlagdProvider.php similarity index 85% rename from src/Flagd/src/FlagdProvider.php rename to providers/Flagd/src/FlagdProvider.php index 99450b1d..cb72bc04 100644 --- a/src/Flagd/src/FlagdProvider.php +++ b/providers/Flagd/src/FlagdProvider.php @@ -16,16 +16,13 @@ class FlagdProvider extends AbstractProvider implements Provider { - protected const NAME = 'FlagdProvider'; + protected static string $NAME = 'FlagdProvider'; private IConfig $config; private ServiceInterface $service; - /** - * @param mixed|IConfig|mixed[] $config - */ - public function __construct($config = null) + public function __construct(mixed $config = null) { $this->config = Validator::validate($config); @@ -52,10 +49,7 @@ public function resolveFloatValue(string $flagKey, float $defaultValue, ?Evaluat return $this->service->resolveValue($flagKey, FlagValueType::FLOAT, $defaultValue, $context); } - /** - * @param mixed[] $defaultValue - */ - public function resolveObjectValue(string $flagKey, $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { return $this->service->resolveValue($flagKey, FlagValueType::OBJECT, $defaultValue, $context); } diff --git a/src/Flagd/src/common/EvaluationContextArrayFactory.php b/providers/Flagd/src/common/EvaluationContextArrayFactory.php similarity index 100% rename from src/Flagd/src/common/EvaluationContextArrayFactory.php rename to providers/Flagd/src/common/EvaluationContextArrayFactory.php diff --git a/src/Flagd/src/common/ResponseCodeErrorCodeMap.php b/providers/Flagd/src/common/ResponseCodeErrorCodeMap.php similarity index 100% rename from src/Flagd/src/common/ResponseCodeErrorCodeMap.php rename to providers/Flagd/src/common/ResponseCodeErrorCodeMap.php diff --git a/src/Flagd/src/common/SafeArrayAccessor.php b/providers/Flagd/src/common/SafeArrayAccessor.php similarity index 100% rename from src/Flagd/src/common/SafeArrayAccessor.php rename to providers/Flagd/src/common/SafeArrayAccessor.php diff --git a/src/Flagd/src/config/Config.php b/providers/Flagd/src/config/Config.php similarity index 97% rename from src/Flagd/src/config/Config.php rename to providers/Flagd/src/config/Config.php index 2638e4d8..639a6ba3 100644 --- a/src/Flagd/src/config/Config.php +++ b/providers/Flagd/src/config/Config.php @@ -12,7 +12,7 @@ class Config implements IConfig private bool $secure; private ?IHttpConfig $httpConfig; - public function __construct(?string $host = null, ?int $port = null, ?string $protocol = null, ?bool $secure = null, ?IHttpConfig $httpConfig) + public function __construct(?string $host = null, ?int $port = null, ?string $protocol = null, ?bool $secure = null, ?IHttpConfig $httpConfig = null) { $this->host = $host ?? Defaults::DEFAULT_HOST; $this->port = $port ?? Defaults::DEFAULT_PORT; diff --git a/src/Flagd/src/config/ConfigFactory.php b/providers/Flagd/src/config/ConfigFactory.php similarity index 92% rename from src/Flagd/src/config/ConfigFactory.php rename to providers/Flagd/src/config/ConfigFactory.php index 5baee5f3..674a9349 100644 --- a/src/Flagd/src/config/ConfigFactory.php +++ b/providers/Flagd/src/config/ConfigFactory.php @@ -6,7 +6,7 @@ class ConfigFactory { - public static function fromOptions(?string $host = null, ?int $port = null, ?string $protocol = null, ?bool $secure = null, ?IHttpConfig $httpConfig): IConfig + public static function fromOptions(?string $host = null, ?int $port = null, ?string $protocol = null, ?bool $secure = null, ?IHttpConfig $httpConfig = null): IConfig { return Validator::validate(new Config($host, $port, $protocol, $secure, $httpConfig)); } diff --git a/src/Flagd/src/config/Defaults.php b/providers/Flagd/src/config/Defaults.php similarity index 100% rename from src/Flagd/src/config/Defaults.php rename to providers/Flagd/src/config/Defaults.php diff --git a/src/Flagd/src/config/HttpConfig.php b/providers/Flagd/src/config/HttpConfig.php similarity index 100% rename from src/Flagd/src/config/HttpConfig.php rename to providers/Flagd/src/config/HttpConfig.php diff --git a/src/Flagd/src/config/IConfig.php b/providers/Flagd/src/config/IConfig.php similarity index 100% rename from src/Flagd/src/config/IConfig.php rename to providers/Flagd/src/config/IConfig.php diff --git a/src/Flagd/src/config/IHttpConfig.php b/providers/Flagd/src/config/IHttpConfig.php similarity index 100% rename from src/Flagd/src/config/IHttpConfig.php rename to providers/Flagd/src/config/IHttpConfig.php diff --git a/src/Flagd/src/config/Protocols.php b/providers/Flagd/src/config/Protocols.php similarity index 100% rename from src/Flagd/src/config/Protocols.php rename to providers/Flagd/src/config/Protocols.php diff --git a/src/Flagd/src/config/Validator.php b/providers/Flagd/src/config/Validator.php similarity index 80% rename from src/Flagd/src/config/Validator.php rename to providers/Flagd/src/config/Validator.php index 36ecb10c..2302eb66 100644 --- a/src/Flagd/src/config/Validator.php +++ b/providers/Flagd/src/config/Validator.php @@ -22,10 +22,7 @@ class Validator private const VALID_PORT_RANGE = [1, 65535]; private const VALID_PROTOCOLS = ['grpc', 'http']; - /** - * @param mixed $config - */ - public static function validate($config = null): IConfig + public static function validate(mixed $config = null): IConfig { if ($config instanceof IConfig) { return self::validateConfig($config); @@ -63,10 +60,7 @@ private static function validateConfig(IConfig $config): IConfig return new Config($host, $port, $protocol, $secure, $httpConfig); } - /** - * @param mixed $secure - */ - private static function validateSecure($secure): bool + private static function validateSecure(mixed $secure): bool { if (is_bool($secure)) { return $secure; @@ -75,10 +69,7 @@ private static function validateSecure($secure): bool return Defaults::DEFAULT_SECURE; } - /** - * @param mixed $host - */ - private static function validateHost($host): string + private static function validateHost(mixed $host): string { if (is_string($host) && preg_match(self::VALID_HOST_REGEXP, $host)) { return $host; @@ -87,10 +78,7 @@ private static function validateHost($host): string return Defaults::DEFAULT_HOST; } - /** - * @param mixed $port - */ - private static function validatePort($port): int + private static function validatePort(mixed $port): int { [$minPort, $maxPort] = self::VALID_PORT_RANGE; @@ -101,10 +89,7 @@ private static function validatePort($port): int return Defaults::DEFAULT_PORT; } - /** - * @param mixed $protocol - */ - private static function validateProtocol($protocol): string + private static function validateProtocol(mixed $protocol): string { if (is_string($protocol) && in_array($protocol, self::VALID_PROTOCOLS)) { return $protocol; @@ -113,10 +98,7 @@ private static function validateProtocol($protocol): string return Defaults::DEFAULT_PROTOCOL; } - /** - * @param mixed $httpConfig - */ - private static function validateHttpConfig($httpConfig): ?IHttpConfig + private static function validateHttpConfig(mixed $httpConfig): ?IHttpConfig { if (is_null($httpConfig)) { return null; @@ -131,9 +113,9 @@ private static function validateHttpConfig($httpConfig): ?IHttpConfig $streamFactory = $httpConfig['streamFactory']; if ( - $client instanceof ClientInterface && - $requestFactory instanceof RequestFactoryInterface && - $streamFactory instanceof StreamFactoryInterface + $client instanceof ClientInterface + && $requestFactory instanceof RequestFactoryInterface + && $streamFactory instanceof StreamFactoryInterface ) { return new HttpConfig($client, $requestFactory, $streamFactory); } diff --git a/src/Flagd/src/errors/InvalidConfigException.php b/providers/Flagd/src/errors/InvalidConfigException.php similarity index 100% rename from src/Flagd/src/errors/InvalidConfigException.php rename to providers/Flagd/src/errors/InvalidConfigException.php diff --git a/src/Flagd/src/errors/InvalidTypeException.php b/providers/Flagd/src/errors/InvalidTypeException.php similarity index 100% rename from src/Flagd/src/errors/InvalidTypeException.php rename to providers/Flagd/src/errors/InvalidTypeException.php diff --git a/src/Flagd/src/errors/RequestBuildException.php b/providers/Flagd/src/errors/RequestBuildException.php similarity index 100% rename from src/Flagd/src/errors/RequestBuildException.php rename to providers/Flagd/src/errors/RequestBuildException.php diff --git a/src/Flagd/src/http/FlagdResponseResolutionDetailsAdapter.php b/providers/Flagd/src/http/FlagdResponseResolutionDetailsAdapter.php similarity index 89% rename from src/Flagd/src/http/FlagdResponseResolutionDetailsAdapter.php rename to providers/Flagd/src/http/FlagdResponseResolutionDetailsAdapter.php index 55589ffe..c4f6e406 100644 --- a/src/Flagd/src/http/FlagdResponseResolutionDetailsAdapter.php +++ b/providers/Flagd/src/http/FlagdResponseResolutionDetailsAdapter.php @@ -16,7 +16,7 @@ class FlagdResponseResolutionDetailsAdapter /** * @param mixed[]|bool|DateTime|float|int|string|null $defaultValue */ - public static function forTypeMismatch($defaultValue): ResolutionDetails + public static function forTypeMismatch(mixed $defaultValue): ResolutionDetails { return (new ResolutionDetailsBuilder()) ->withValue($defaultValue) @@ -28,11 +28,13 @@ public static function forTypeMismatch($defaultValue): ResolutionDetails * @param string[] $response * @param mixed[]|bool|DateTime|float|int|string|null $defaultValue */ - public static function forError(array $response, $defaultValue): ResolutionDetails + public static function forError(array $response, mixed $defaultValue): ResolutionDetails { $responseCode = $response['code']; if ($responseCode && ResponseCodeErrorCodeMap::has($responseCode)) { - /** @var ErrorCode $responseErrorCode */ + /** + * @var ErrorCode $responseErrorCode + */ $responseErrorCode = ResponseCodeErrorCodeMap::get($responseCode); $resolutionError = new ResolutionError( diff --git a/src/Flagd/src/http/FlagdResponseValidator.php b/providers/Flagd/src/http/FlagdResponseValidator.php similarity index 100% rename from src/Flagd/src/http/FlagdResponseValidator.php rename to providers/Flagd/src/http/FlagdResponseValidator.php diff --git a/src/Flagd/src/http/GrpcWebEndpoint.php b/providers/Flagd/src/http/GrpcWebEndpoint.php similarity index 100% rename from src/Flagd/src/http/GrpcWebEndpoint.php rename to providers/Flagd/src/http/GrpcWebEndpoint.php diff --git a/src/Flagd/src/http/HttpService.php b/providers/Flagd/src/http/HttpService.php similarity index 100% rename from src/Flagd/src/http/HttpService.php rename to providers/Flagd/src/http/HttpService.php diff --git a/src/Flagd/src/http/Method.php b/providers/Flagd/src/http/Method.php similarity index 100% rename from src/Flagd/src/http/Method.php rename to providers/Flagd/src/http/Method.php diff --git a/src/Flagd/src/service/ServiceFactory.php b/providers/Flagd/src/service/ServiceFactory.php similarity index 72% rename from src/Flagd/src/service/ServiceFactory.php rename to providers/Flagd/src/service/ServiceFactory.php index 8a8277ab..3917886d 100644 --- a/src/Flagd/src/service/ServiceFactory.php +++ b/providers/Flagd/src/service/ServiceFactory.php @@ -5,7 +5,6 @@ namespace OpenFeature\Providers\Flagd\service; use OpenFeature\Providers\Flagd\config\IConfig; -use OpenFeature\Providers\Flagd\grpc\GrpcService; use OpenFeature\Providers\Flagd\http\HttpService; class ServiceFactory @@ -13,9 +12,6 @@ class ServiceFactory public static function fromConfig(IConfig $config): ServiceInterface { switch ($config->getProtocol()) { - case 'grpc': - return GrpcService::fromConfig($config); - case 'http': default: return HttpService::fromConfig($config); } diff --git a/src/Flagd/src/service/ServiceInterface.php b/providers/Flagd/src/service/ServiceInterface.php similarity index 83% rename from src/Flagd/src/service/ServiceInterface.php rename to providers/Flagd/src/service/ServiceInterface.php index fff746c6..c8950c83 100644 --- a/src/Flagd/src/service/ServiceInterface.php +++ b/providers/Flagd/src/service/ServiceInterface.php @@ -13,5 +13,5 @@ interface ServiceInterface /** * @param mixed[]|bool|DateTime|float|int|string|null $defaultValue */ - public function resolveValue(string $flagKey, string $flagType, $defaultValue, ?EvaluationContext $context): ResolutionDetails; + public function resolveValue(string $flagKey, string $flagType, mixed $defaultValue, ?EvaluationContext $context): ResolutionDetails; } diff --git a/src/Flagd/tests/TestCase.php b/providers/Flagd/tests/TestCase.php similarity index 100% rename from src/Flagd/tests/TestCase.php rename to providers/Flagd/tests/TestCase.php diff --git a/src/Flagd/tests/unit/FlagdProviderTest.php b/providers/Flagd/tests/unit/FlagdProviderTest.php similarity index 95% rename from src/Flagd/tests/unit/FlagdProviderTest.php rename to providers/Flagd/tests/unit/FlagdProviderTest.php index 25b70c87..2eca3114 100644 --- a/src/Flagd/tests/unit/FlagdProviderTest.php +++ b/providers/Flagd/tests/unit/FlagdProviderTest.php @@ -35,6 +35,7 @@ public function testCanBeInstantiated(): void // Then $this->assertNotNull($instance); $this->assertInstanceOf(Provider::class, $instance); + $this->assertEquals('FlagdProvider', $instance->getMetadata()->getName()); } public function testCanInstantiateHttpWithConfigObject(): void @@ -57,7 +58,7 @@ public function testCanInstantiateHttpWithConfigObject(): void $mockStreamFactory->shouldReceive('createStream')->andReturn($mockStream); $mockResponse = $this->mockery(ResponseInterface::class); - $mockResponse->shouldReceive('getBody')->andReturn("{ + $mockResponse->shouldReceive('getBody->__toString')->andReturn("{ \"value\":\"{$expectedValue}\", \"variant\":\"{$expectedVariant}\", \"reason\":\"{$expectedReason}\" @@ -111,7 +112,7 @@ public function testCanInstantiateHttpWithConfigArray(): void $mockStreamFactory->shouldReceive('createStream')->andReturn($mockStream); $mockResponse = $this->mockery(ResponseInterface::class); - $mockResponse->shouldReceive('getBody')->andReturn("{ + $mockResponse->shouldReceive('getBody->__toString')->andReturn("{ \"value\":\"{$expectedValue}\", \"variant\":\"{$expectedVariant}\", \"reason\":\"{$expectedReason}\" diff --git a/providers/GoFeatureFlag/.gitignore b/providers/GoFeatureFlag/.gitignore new file mode 100644 index 00000000..5c09088b --- /dev/null +++ b/providers/GoFeatureFlag/.gitignore @@ -0,0 +1,5 @@ +/composer.lock +/vendor +/build + +.php-cs-fixer.cache \ No newline at end of file diff --git a/providers/GoFeatureFlag/CHANGELOG.md b/providers/GoFeatureFlag/CHANGELOG.md new file mode 100644 index 00000000..53ba5145 --- /dev/null +++ b/providers/GoFeatureFlag/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +## [1.1.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/go-feature-flag-provider-1.0.0...open-feature/go-feature-flag-provider-1.1.0) (2025-01-29) + + +### Features + +* **go-feature-flag:** Support exporter metadata ([#120](https://github.com/open-feature/php-sdk-contrib/issues/120)) ([5dfcfbb](https://github.com/open-feature/php-sdk-contrib/commit/5dfcfbb9f16555585ab8bbe6ede1bdf1d177ea76)) + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/go-feature-flag-provider-v1.0.0...open-feature/go-feature-flag-provider-1.0.0) (2024-09-04) + + +### Features + +* new GO Feature Flag PHP provider ([#106](https://github.com/open-feature/php-sdk-contrib/issues/106)) ([4ba0033](https://github.com/open-feature/php-sdk-contrib/commit/4ba0033dd08a2f9f2ab02ebd49b7b6e03d11eb79)) + +## 0.1.0 (2024-09-03) + + +### Features + +* new GO Feature Flag PHP provider ([#106](https://github.com/open-feature/php-sdk-contrib/issues/106)) ([4ba0033](https://github.com/open-feature/php-sdk-contrib/commit/4ba0033dd08a2f9f2ab02ebd49b7b6e03d11eb79)) diff --git a/providers/GoFeatureFlag/README.md b/providers/GoFeatureFlag/README.md new file mode 100644 index 00000000..c81a1706 --- /dev/null +++ b/providers/GoFeatureFlag/README.md @@ -0,0 +1,144 @@ +

+ go-feature-flag logo + +

+ +# GO Feature Flag - OpenFeature PHP provider +

+ + + Packagist Version + Documentation + Issues + Join us on slack +

+ +This repository contains the official PHP OpenFeature provider for accessing your feature flags with [GO Feature Flag](https://gofeatureflag.org). + +In conjunction with the [OpenFeature SDK](https://openfeature.dev/docs/reference/concepts/provider) you will be able +to evaluate your feature flags in your Ruby applications. + +For documentation related to flags management in GO Feature Flag, +refer to the [GO Feature Flag documentation website](https://gofeatureflag.org/docs). + +### Functionalities: +- Manage the integration of the OpenFeature PHP SDK and GO Feature Flag relay-proxy. + +## Dependency Setup + +### Composer + +```shell +composer require open-feature/go-feature-flag-provider +``` +## Getting started + +### Initialize the provider + +The `GoFeatureFlagProvider` takes a config object as parameter to be initialized. + +The constructor of the config object has the following options: + +| **Option** | **Description** | +|-----------------|------------------------------------------------------------------------------------------------------------------| +| `endpoint` | **(mandatory)** The URL to access to the relay-proxy.
*(example: `https://relay.proxy.gofeatureflag.org/`)* | +| `apiKey` | The token used to call the relay proxy. | +| `customHeaders` | Any headers you want to add to call the relay-proxy. | +| `httpclient` | The HTTP Client to use (if you want to use a custom one). _It has to be a `PSR-7` compliant implementation._ | + +The only required option to create a `GoFeatureFlagProvider` is the URL _(`endpoint`)_ to your GO Feature Flag relay-proxy instance. + +```php +use OpenFeature\Providers\GoFeatureFlag\config\Config; +use OpenFeature\Providers\GoFeatureFlag\GoFeatureFlagProvider; +use OpenFeature\implementation\flags\MutableEvaluationContext; +use OpenFeature\implementation\flags\Attributes; +use OpenFeature\OpenFeatureAPI; + +$config = new Config('http://gofeatureflag.org/', 'my-api-key'); +$provider = new GoFeatureFlagProvider($config); + +$api = OpenFeatureAPI::getInstance(); +$api->setProvider($provider); +$client = $api->getClient(); +$evaluationContext = new MutableEvaluationContext( + "214b796a-807b-4697-b3a3-42de0ec10a37", + new Attributes(["email" => 'contact@gofeatureflag.org']) + ); + +$value = $client->getBooleanDetails('integer_key', false, $evaluationContext); +if ($value) { + echo "The flag is enabled"; +} else { + echo "The flag is disabled"; +} +``` + +The evaluation context is the way for the client to specify contextual data that GO Feature Flag uses to evaluate the feature flags, it allows to define rules on the flag. + +The `targeting_key` is mandatory for GO Feature Flag to evaluate the feature flag, it could be the id of a user, a session ID or anything you find relevant to use as identifier during the evaluation. + + +### Evaluate a feature flag +The client is used to retrieve values for the current `EvaluationContext`. +For example, retrieving a boolean value for the flag **"my-flag"**: + +```php +$value = $client->getBooleanDetails('integer_key', false, $evaluationContext); +if ($value) { + echo "The flag is enabled"; +} else { + echo "The flag is disabled"; +} +``` + +GO Feature Flag supports different all OpenFeature supported types of feature flags, it means that you can use all the accessor directly +```php +// Bool +$client->getBooleanDetails('my-flag-key', false, new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); +$client->getBooleanValue('my-flag-key', false, new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); + +// String +$client->getStringDetails('my-flag-key', "default", new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); +$client->getStringValue('my-flag-key', "default", new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); + +// Integer +$client->getIntegerDetails('my-flag-key', 1, new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); +$client->getIntegerValue('my-flag-key', 1, new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); + +// Float +$client->getFloatDetails('my-flag-key', 1.1, new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); +$client->getFloatValue('my-flag-key', 1.1, new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); + +// Object +$client->getObjectDetails('my-flag-key', ["default" => true], new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); +$client->getObjectValue('my-flag-key', ["default" => true], new MutableEvaluationContext("214b796a-807b-4697-b3a3-42de0ec10a37")); +``` + +## Features status + +| Status | Feature | Description | +|-------|-----------------|----------------------------------------------------------------------------| +| ✅ | Flag evaluation | It is possible to evaluate all the type of flags | +| ❌ | Caching | Mechanism is in place to refresh the cache in case of configuration change | +| ❌ | Event Streaming | Not supported by the SDK | +| ❌ | Logging | Not supported by the SDK | +| ❌ | Flag Metadata | Not supported by the SDK | + + +**Implemented**: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ + +## Contributing +This project welcomes contributions from the community. +If you're interested in contributing, see the [contributors' guide](https://github.com/thomaspoignant/go-feature-flag/blob/main/CONTRIBUTING.md) for some helpful tips. + +### PHP Versioning +This library targets PHP version 8.0 and newer. As long as you have any 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`, it will update the `composer.lock` with the most recent compatible versions. + +We value having as few runtime dependencies as possible. The addition of any dependencies requires careful consideration and review. + diff --git a/providers/GoFeatureFlag/composer.json b/providers/GoFeatureFlag/composer.json new file mode 100644 index 00000000..3cf4f307 --- /dev/null +++ b/providers/GoFeatureFlag/composer.json @@ -0,0 +1,123 @@ +{ + "name": "open-feature/go-feature-flag-provider", + "description": "The GO Feature Flag provider package for open-feature", + "license": "Apache-2.0", + "type": "library", + "keywords": [ + "featureflags", + "featureflagging", + "openfeature", + "gofeatureflag", + "provider" + ], + "authors": [ + { + "name": "Thomas Poignant", + "homepage": "https://github.com/thomaspoignant/go-feature-flag" + } + ], + "require": { + "php": "^8", + "guzzlehttp/guzzle": "^7.9", + "open-feature/sdk": "^2.0", + "psr/http-message": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "mockery/mockery": "^1.6", + "spatie/phpunit-snapshot-assertions": "^4.2", + "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\\GoFeatureFlag\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Providers\\GoFeatureFlag\\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 + }, + "sort-packages": true + }, + "scripts": { + "dev:analyze": [ + "@dev:analyze:phpstan", + "@dev:analyze:psalm" + ], + "dev:analyze:phpstan": "phpstan --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "psalm", + "dev:build:clean": "git clean -fX build/", + "dev:lint": [ + "@dev:lint:syntax", + "@dev:lint:style" + ], + "dev:lint:fix": "phpcbf", + "dev:lint:style": "phpcs --colors", + "dev:lint:syntax": "parallel-lint --colors src/ tests/", + "dev:test": [ + "@dev:lint", + "@dev:analyze", + "@dev:test:unit", + "@dev:test:integration" + ], + "dev:test:coverage:ci": "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": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": [ + "@dev:test:unit:setup", + "phpunit --colors=always --testdox --testsuite=unit", + "@dev:test:unit:teardown" + ], + "dev:test:unit:debug": "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 for unit tests...'", + "dev:test:integration": [ + "@dev:test:integration:setup", + "phpunit --colors=always --testdox --testsuite=integration", + "@dev:test:integration:teardown" + ], + "dev:test:integration:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "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/GoFeatureFlag/phpcs.xml.dist b/providers/GoFeatureFlag/phpcs.xml.dist new file mode 100644 index 00000000..55d9d3a1 --- /dev/null +++ b/providers/GoFeatureFlag/phpcs.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + ./src + ./tests + + */tests/fixtures/* + */tests/*/fixtures/* + + + + + + + + + + + + + diff --git a/providers/GoFeatureFlag/phpstan.neon.dist b/providers/GoFeatureFlag/phpstan.neon.dist new file mode 100644 index 00000000..000a4863 --- /dev/null +++ b/providers/GoFeatureFlag/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + tmpDir: ./build/cache/phpstan + level: max + paths: + - ./src + excludePaths: + - */tests/fixtures/* + - */tests/*/fixtures/* diff --git a/providers/GoFeatureFlag/phpunit.xml.dist b/providers/GoFeatureFlag/phpunit.xml.dist new file mode 100644 index 00000000..ecad4cce --- /dev/null +++ b/providers/GoFeatureFlag/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./tests/unit + + + + + + ./src + + + + + + + + diff --git a/providers/GoFeatureFlag/psalm-baseline.xml b/providers/GoFeatureFlag/psalm-baseline.xml new file mode 100644 index 00000000..ceaa5778 --- /dev/null +++ b/providers/GoFeatureFlag/psalm-baseline.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/providers/GoFeatureFlag/psalm.xml b/providers/GoFeatureFlag/psalm.xml new file mode 100644 index 00000000..c3e6c03c --- /dev/null +++ b/providers/GoFeatureFlag/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/providers/GoFeatureFlag/src/GoFeatureFlagProvider.php b/providers/GoFeatureFlag/src/GoFeatureFlagProvider.php new file mode 100644 index 00000000..e483119a --- /dev/null +++ b/providers/GoFeatureFlag/src/GoFeatureFlagProvider.php @@ -0,0 +1,154 @@ +getCustomHeaders())) { + $config->addCustomHeader('Content-Type', 'application/json'); + } + $this->ofrepApi = new OfrepApi($config); + } + + public function getMetadata(): Metadata + { + return new Metadata(static::$NAME); + } + + public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->evaluate($flagKey, $defaultValue, ['boolean'], $context); + } + + /** + * @param array|array|bool|DateTime|float|int|string|null $defaultValue + * @param array $allowedClasses + */ + private function evaluate(string $flagKey, array | string | bool | DateTime | float | int | null $defaultValue, array $allowedClasses, ?EvaluationContext $evaluationContext = null): ResolutionDetails + { + try { + Validator::validateFlagKey($flagKey); + + if ($evaluationContext === null) { + throw new InvalidContextException('Evaluation context is null'); + } + if ($evaluationContext->getTargetingKey() === null || $evaluationContext->getTargetingKey() === '') { + throw new InvalidContextException('Missing targetingKey in evaluation context'); + } + + $apiResp = $this->ofrepApi->evaluate($flagKey, $evaluationContext); + + if ($apiResp instanceof OfrepApiErrorResponse) { + $err = new ResolutionError( + $apiResp->getErrorCode(), + $apiResp->getErrorDetails(), + ); + + return (new ResolutionDetailsBuilder()) + ->withValue($defaultValue) + ->withError($err) + ->withReason($apiResp->getReason()) + ->build(); + } + + if (!$this->isValidType($apiResp->getValue(), $allowedClasses)) { + return (new ResolutionDetailsBuilder()) + ->withReason(Reason::ERROR) + ->withError(new ResolutionError( + ErrorCode::TYPE_MISMATCH(), + "Invalid type for $flagKey, got " . gettype($apiResp->getValue()) . ' expected ' . implode(', ', $allowedClasses), + )) + ->withValue($defaultValue) + ->build(); + } + + return (new ResolutionDetailsBuilder()) + ->withValue($apiResp->getValue()) + ->withReason($apiResp->getReason()) + ->withVariant($apiResp->getVariant()) + ->build(); + } catch (BaseOfrepException $e) { + $err = new ResolutionError($e->getErrorCode(), $e->getMessage()); + + return (new ResolutionDetailsBuilder()) + ->withValue($defaultValue) + ->withError($err) + ->withReason(Reason::ERROR) + ->build(); + } catch (Throwable $e) { + return (new ResolutionDetailsBuilder()) + ->withValue($defaultValue) + ->withError(new ResolutionError(ErrorCode::GENERAL(), 'An error occurred while evaluating the flag: ' . $e->getMessage())) + ->withReason(Reason::ERROR) + ->build(); + } + } + + /** + * @param array $allowedClasses + */ + private function isValidType(mixed $value, array $allowedClasses): bool + { + foreach ($allowedClasses as $class) { + if ($value instanceof $class || gettype($value) === $class) { + return true; + } + } + + return false; + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->evaluate($flagKey, $defaultValue, ['string'], $context); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->evaluate($flagKey, $defaultValue, ['integer'], $context); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->evaluate($flagKey, $defaultValue, ['double'], $context); + } + + public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->evaluate($flagKey, $defaultValue, ['array'], $context); + } +} diff --git a/providers/GoFeatureFlag/src/config/Config.php b/providers/GoFeatureFlag/src/config/Config.php new file mode 100644 index 00000000..8ba5851f --- /dev/null +++ b/providers/GoFeatureFlag/src/config/Config.php @@ -0,0 +1,89 @@ + + */ + private array $customHeaders = []; + + /** + * @var ClientInterface|null - The HTTP Client to use (if you want to use a custom one) + */ + private ?ClientInterface $httpclient; + + /** + * @var array exporterMetadata - is the metadata we send to the GO Feature Flag relay proxy when we report + * the evaluation data usage. + * + * ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information of this + * field will not be added to your feature events. + */ + private array $exporterMetadata = []; + + /** + * @param string $endpoint - The endpoint to your GO Feature Flag Instance + * @param string|null $apiKey - API Key to use to connect to GO Feature Flag + * @param array|null $customHeaders - Custom headers you want to send + * @param array|null $exporterMetadata - Metadata to send to the relay proxy during evaluation data collection + * @param ClientInterface|null $httpclient - The HTTP Client to use (if you want to use a custom one) + */ + public function __construct( + string $endpoint, + ?string $apiKey = '', + ?array $customHeaders = [], + ?array $exporterMetadata = [], + ?ClientInterface $httpclient = null, + ) { + $this->httpclient = $httpclient; + $this->endpoint = $endpoint; + $this->customHeaders = $customHeaders ?? []; + + // set default exporter metadata fields + $this->exporterMetadata = $exporterMetadata ?? []; + $this->exporterMetadata['openfeature'] = true; + $this->exporterMetadata['provider'] = 'php'; + + if ($apiKey !== null && $apiKey !== '') { + $this->customHeaders['Authorization'] = 'Bearer ' . $apiKey; + } + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + /** + * @return array + */ + public function getCustomHeaders(): array + { + return $this->customHeaders; + } + + public function addCustomHeader(string $key, string $value): void + { + $this->customHeaders[$key] = $value; + } + + public function getHttpClient(): ?ClientInterface + { + return $this->httpclient; + } + + /** + * @return array + */ + public function getExporterMetadata(): array + { + return $this->exporterMetadata; + } +} diff --git a/providers/GoFeatureFlag/src/controller/OfrepApi.php b/providers/GoFeatureFlag/src/controller/OfrepApi.php new file mode 100644 index 00000000..1b0f1700 --- /dev/null +++ b/providers/GoFeatureFlag/src/controller/OfrepApi.php @@ -0,0 +1,152 @@ +options = $config; + $this->client = $config->getHttpClient() ?? new Client([ + 'base_uri' => $config->getEndpoint(), + ]); + } + + /** + * @throws ParseException + * @throws FlagNotFoundException + * @throws RateLimitedException + * @throws UnauthorizedException + * @throws UnknownOfrepException + * @throws BaseOfrepException + */ + public function evaluate(string $flagKey, EvaluationContext $evaluationContext): OfrepApiSuccessResponse | OfrepApiErrorResponse + { + try { + if ($this->retryAfter !== null) { + if (time() < $this->retryAfter) { + throw new RateLimitedException(); + } else { + $this->retryAfter = null; + } + } + + $baseUri = $this->options->getEndpoint(); + $evaluateApiPath = rtrim($baseUri, '/') . "/ofrep/v1/evaluate/flags/{$flagKey}"; + $headers = array_merge( + ['Content-Type' => 'application/json'], + $this->options->getCustomHeaders(), + ); + + $fields = array_merge( + $evaluationContext->getAttributes()->toArray(), + ['targetingKey' => $evaluationContext->getTargetingKey()], + ); + + // Add exporter metadata to the context + $fields['gofeatureflag'] = ['exporterMetadata' => $this->options->getExporterMetadata()]; + + $requestBody = json_encode(['context' => $fields]); + if ($requestBody === false) { + throw new ParseException('failed to encode request body'); + } + $req = new Request('POST', $evaluateApiPath, $headers, $requestBody); + $response = $this->client->sendRequest($req); + + switch ($response->getStatusCode()) { + case 200: + return $this->parseSuccessResponse($response); + case 400: + return $this->parseErrorResponse($response); + case 401: + case 403: + throw new UnauthorizedException($response); + case 404: + throw new FlagNotFoundException($flagKey, $response); + case 429: + $this->parseRetryLaterHeader($response); + + throw new RateLimitedException($response); + default: + throw new UnknownOfrepException($response); + } + } catch (BaseOfrepException $e) { + throw $e; + } catch (GuzzleException | Throwable $e) { + echo $e; + + throw new UnknownOfrepException(null, $e); + } + } + + /** + * @throws ParseException + */ + private function parseSuccessResponse(ResponseInterface $response): OfrepApiSuccessResponse + { + /** @var array $parsed */ + $parsed = json_decode($response->getBody()->getContents(), true); + $parsed = Validator::validateSuccessApiResponse($parsed); + + return new OfrepApiSuccessResponse($parsed); + } + + /** + * @throws ParseException + */ + private function parseErrorResponse(ResponseInterface $response): OfrepApiErrorResponse + { + /** @var array $parsed */ + $parsed = json_decode($response->getBody()->getContents(), true); + $parsed = Validator::validateErrorApiResponse($parsed); + + return new OfrepApiErrorResponse($parsed); + } + + private function parseRetryLaterHeader(ResponseInterface $response): void + { + $retryAfterHeader = $response->getHeaderLine('Retry-After'); + if ($retryAfterHeader) { + if (is_numeric($retryAfterHeader)) { + // Retry-After is in seconds + $this->retryAfter = time() + (int) $retryAfterHeader; + } else { + // Retry-After is in HTTP-date format + $retryTime = strtotime($retryAfterHeader); + $this->retryAfter = $retryTime !== false ? $retryTime : null; + } + } + } +} diff --git a/providers/GoFeatureFlag/src/exception/BaseGoffException.php b/providers/GoFeatureFlag/src/exception/BaseGoffException.php new file mode 100644 index 00000000..cc2221f3 --- /dev/null +++ b/providers/GoFeatureFlag/src/exception/BaseGoffException.php @@ -0,0 +1,40 @@ +customMessage = $message; + $this->response = $response; + $this->errorCode = $errorCode; + parent::__construct($message, $code, $previous); + } + + public function getCustomMessage(): string + { + return $this->customMessage; + } + + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + public function getErrorCode(): ErrorCode + { + return $this->errorCode; + } +} diff --git a/providers/GoFeatureFlag/src/exception/BaseOfrepException.php b/providers/GoFeatureFlag/src/exception/BaseOfrepException.php new file mode 100644 index 00000000..fa33d8ae --- /dev/null +++ b/providers/GoFeatureFlag/src/exception/BaseOfrepException.php @@ -0,0 +1,40 @@ +customMessage = $message; + $this->response = $response; + $this->errorCode = $errorCode; + parent::__construct($message, $code, $previous); + } + + public function getCustomMessage(): string + { + return $this->customMessage; + } + + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + public function getErrorCode(): ErrorCode + { + return $this->errorCode; + } +} diff --git a/providers/GoFeatureFlag/src/exception/FlagNotFoundException.php b/providers/GoFeatureFlag/src/exception/FlagNotFoundException.php new file mode 100644 index 00000000..caf3b200 --- /dev/null +++ b/providers/GoFeatureFlag/src/exception/FlagNotFoundException.php @@ -0,0 +1,26 @@ +flagKey = $flagKey; + $message = "Flag with key $flagKey not found"; + $code = 1002; + parent::__construct($message, ErrorCode::FLAG_NOT_FOUND(), $response, $code); + } + + public function getFlagKey(): string + { + return $this->flagKey; + } +} diff --git a/providers/GoFeatureFlag/src/exception/InvalidConfigException.php b/providers/GoFeatureFlag/src/exception/InvalidConfigException.php new file mode 100644 index 00000000..0458c706 --- /dev/null +++ b/providers/GoFeatureFlag/src/exception/InvalidConfigException.php @@ -0,0 +1,24 @@ +customMessage = $message; + parent::__construct($message, $code, $previous); + } + + public function getCustomMessage(): string + { + return $this->customMessage; + } +} diff --git a/providers/GoFeatureFlag/src/exception/InvalidContextException.php b/providers/GoFeatureFlag/src/exception/InvalidContextException.php new file mode 100644 index 00000000..fbaf00b4 --- /dev/null +++ b/providers/GoFeatureFlag/src/exception/InvalidContextException.php @@ -0,0 +1,16 @@ + $apiData + * + * @throws ParseException + */ + public function __construct(array $apiData) + { + $this->reason = Reason::ERROR; + $this->errorCode = Mapper::errorCode(is_string($apiData['errorCode']) ? $apiData['errorCode'] : ''); + $this->errorDetails = is_string($apiData['errorDetails']) ? $apiData['errorDetails'] : ''; + } + + public function getReason(): string + { + return $this->reason; + } + + public function getErrorCode(): ErrorCode + { + return $this->errorCode; + } + + public function getErrorDetails(): string + { + return $this->errorDetails; + } +} diff --git a/providers/GoFeatureFlag/src/model/OfrepApiSuccessResponse.php b/providers/GoFeatureFlag/src/model/OfrepApiSuccessResponse.php new file mode 100644 index 00000000..7e8d6555 --- /dev/null +++ b/providers/GoFeatureFlag/src/model/OfrepApiSuccessResponse.php @@ -0,0 +1,82 @@ +|array|bool|DateTime|float|int|string|null + */ + private array | bool | DateTime | float | int | string | null $value; + private string $reason; + private string $variant; + + // TODO: Commenting Metadata here because it is not supported by the SDK yet. + // private array $metadata; + + /** + * @param array $apiData + * + * @throws ParseException + */ + public function __construct( + array $apiData, + ) { + if ( + is_null($apiData['value']) + || is_array($apiData['value']) + || is_bool($apiData['value']) + || $apiData['value'] instanceof DateTime + || is_float($apiData['value']) + || is_int($apiData['value']) + || is_string($apiData['value']) + ) { + $this->value = $apiData['value']; + } else { + throw new ParseException('Invalid type for value'); + } + + $this->variant = is_string($apiData['variant']) ? $apiData['variant'] : 'error in provider'; + $this->reason = Mapper::reason(is_string($apiData['reason']) ? $apiData['reason'] : ''); + // $this->metadata = $apiData['metadata'] ?? []; + } + + /** + * @return array|array|bool|DateTime|float|int|string|null + */ + public function getValue(): array | bool | DateTime | float | int | string | null + { + return $this->value; + } + + public function getReason(): string + { + return $this->reason; + } + + public function getVariant(): string + { + return $this->variant; + } + + // /** + // * @return array + // */ + // public function getMetadata(): array + // { + // return $this->metadata; + // } +} diff --git a/providers/GoFeatureFlag/src/util/Mapper.php b/providers/GoFeatureFlag/src/util/Mapper.php new file mode 100644 index 00000000..6d55f97d --- /dev/null +++ b/providers/GoFeatureFlag/src/util/Mapper.php @@ -0,0 +1,36 @@ + ErrorCode::PROVIDER_NOT_READY(), + 'FLAG_NOT_FOUND' => ErrorCode::FLAG_NOT_FOUND(), + 'PARSE_ERROR' => ErrorCode::PARSE_ERROR(), + 'TYPE_MISMATCH' => ErrorCode::TYPE_MISMATCH(), + 'TARGETING_KEY_MISSING' => ErrorCode::TARGETING_KEY_MISSING(), + 'INVALID_CONTEXT' => ErrorCode::INVALID_CONTEXT(), + default => ErrorCode::GENERAL() + }; + } + + public static function reason(string $reason): string + { + return match ($reason) { + 'ERROR' => Reason::ERROR, + 'DEFAULT' => Reason::DEFAULT, + 'TARGETING_MATCH' => Reason::TARGETING_MATCH, + 'SPLIT' => Reason::SPLIT, + 'DISABLED' => Reason::DISABLED, + default => Reason::UNKNOWN + }; + } +} diff --git a/providers/GoFeatureFlag/src/util/Validator.php b/providers/GoFeatureFlag/src/util/Validator.php new file mode 100644 index 00000000..37bc62d1 --- /dev/null +++ b/providers/GoFeatureFlag/src/util/Validator.php @@ -0,0 +1,132 @@ +getEndpoint()); + } + + /** + * @param string $endpoint - The endpoint to validate + * + * @throws InvalidConfigException + */ + private static function validateEndpoint(string $endpoint): void + { + if (!filter_var($endpoint, FILTER_VALIDATE_URL)) { + throw new InvalidConfigException('Invalid endpoint URL: ' . $endpoint); + } + } + + /** + * @param mixed $data - The data to validate + * + * @return array{key: string, reason: string, variant: string} + * + * @throws ParseException + */ + public static function validateSuccessApiResponse(mixed $data): array + { + if (!is_array($data)) { + throw new ParseException('invalid json object, expected associative array'); + } + + $requiredKeys = ['key', 'value', 'reason', 'variant']; + $missingKeys = array_diff($requiredKeys, array_keys($data)); + if (count($missingKeys) > 0) { + throw new ParseException( + 'missing keys in the success response: ' . implode(', ', $missingKeys), + ); + } + + if (!is_string($data['key'])) { + throw new ParseException('key is not a string'); + } + + if (!is_string($data['variant'])) { + throw new ParseException('variant is not a string'); + } + + if (!is_string($data['reason'])) { + throw new ParseException('reason is not a string'); + } + + if (key_exists('metadata', $data) && !is_array($data['metadata'])) { + throw new ParseException('metadata is not an array'); + } + + return $data; + } + + /** + * @param mixed $data - The data to validate + * + * @return array{errorCode: string} + * + * @throws ParseException + */ + public static function validateErrorApiResponse(mixed $data): array + { + if (!is_array($data)) { + throw new ParseException('invalid json object, expected associative array'); + } + + $requiredKeys = ['key', 'errorCode']; + $missingKeys = array_diff($requiredKeys, array_keys($data)); + if (count($missingKeys) > 0) { + throw new ParseException( + 'missing keys in the error response: ' . implode(', ', $missingKeys), + ); + } + + if (!is_string($data['errorCode'])) { + throw new ParseException('key is not a string', null); + } + + if (key_exists('errorDetails', $data) && !is_string($data['errorDetails'])) { + throw new ParseException('errorDetails is not a string', null); + } + + return $data; + } + + /** + * @param string $flagKey - The flag key to validate + * + * @throws InvalidConfigException + */ + public static function validateFlagKey(string $flagKey): void + { + if ($flagKey === '') { + throw new InvalidConfigException('Flag key is null or empty'); + } + } +} diff --git a/providers/GoFeatureFlag/tests/TestCase.php b/providers/GoFeatureFlag/tests/TestCase.php new file mode 100644 index 00000000..9d7a833b --- /dev/null +++ b/providers/GoFeatureFlag/tests/TestCase.php @@ -0,0 +1,39 @@ + $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/GoFeatureFlag/tests/unit/GoFeatureFlagProviderTest.php b/providers/GoFeatureFlag/tests/unit/GoFeatureFlagProviderTest.php new file mode 100644 index 00000000..2f1b0a68 --- /dev/null +++ b/providers/GoFeatureFlag/tests/unit/GoFeatureFlagProviderTest.php @@ -0,0 +1,529 @@ +expectException(InvalidConfigException::class); + new GoFeatureFlagProvider( + new Config('invalid'), + ); + } + + // Configuration validation tests + + public function testShouldNotThrowIfValidEndpoint(): void + { + $provider = new GoFeatureFlagProvider( + new Config('https://gofeatureflag.org/'), + ); + $this->assertInstanceOf(GoFeatureFlagProvider::class, $provider); + } + + public function testShouldRaiseIfEndpointIsNotHttp(): void + { + $this->expectException(InvalidConfigException::class); + $provider = new GoFeatureFlagProvider( + new Config('gofeatureflag.org'), + ); + $this->assertInstanceOf(GoFeatureFlagProvider::class, $provider); + } + + public function testEmptyEndpointShouldThrow(): void + { + $this->expectException(InvalidConfigException::class); + new GoFeatureFlagProvider( + new Config(''), + ); + } + + public function testMetadataNameIsDefined(): void + { + $config = new Config('http://localhost:1031/'); + $provider = new GoFeatureFlagProvider($config); + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + assertEquals('GO Feature Flag Provider', $api->getProviderMetadata()->getName()); + } + + // Metadata tests + + public function testShouldReturnTheValueOfTheFlagAsInt(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getIntegerDetails('integer_key', 1, $this->defaultEvaluationContext); + assertEquals(42, $got->getValue()); + assertEquals(Reason::TARGETING_MATCH, $got->getReason()); + assertEquals('default', $got->getVariant()); + assertEquals(null, $got->getError()); + assertEquals('integer_key', $got->getFlagKey()); + } + + /** + * @throws ReflectionException + */ + private function mockHttpClient(GoFeatureFlagProvider $provider, MockObject $mockClient): void + { + $providerReflection = new ReflectionClass($provider); + $ofrepApiProperty = $providerReflection->getProperty('ofrepApi'); + $ofrepApiProperty->setAccessible(true); + $ofrepApi = $ofrepApiProperty->getValue($provider); + + $ofrepApiReflection = new ReflectionClass($ofrepApi); + $clientProperty = $ofrepApiReflection->getProperty('client'); + $clientProperty->setAccessible(true); + $clientProperty->setValue($ofrepApi, $mockClient); + } + + public function testShouldReturnTheValueOfTheFlagAsFloat(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flag-key', + 'value' => 42.2, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getFloatDetails('flag-key', 1.0, $this->defaultEvaluationContext); + assertEquals(42.2, $got->getValue()); + assertEquals(Reason::TARGETING_MATCH, $got->getReason()); + assertEquals('default', $got->getVariant()); + assertEquals(null, $got->getError()); + assertEquals('flag-key', $got->getFlagKey()); + } + + public function testShouldReturnTheValueOfTheFlagAsString(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flag-key', + 'value' => 'value as string', + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getStringDetails('flag-key', 'default', $this->defaultEvaluationContext); + assertEquals('value as string', $got->getValue()); + assertEquals(Reason::TARGETING_MATCH, $got->getReason()); + assertEquals('default', $got->getVariant()); + assertEquals(null, $got->getError()); + assertEquals('flag-key', $got->getFlagKey()); + } + + public function testShouldReturnTheValueOfTheFlagAsBool(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flag-key', + 'value' => true, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('flag-key', false, $this->defaultEvaluationContext); + assertEquals(true, $got->getValue()); + assertEquals(Reason::TARGETING_MATCH, $got->getReason()); + assertEquals('default', $got->getVariant()); + assertEquals(null, $got->getError()); + assertEquals('flag-key', $got->getFlagKey()); + } + + public function testShouldReturnTheValueOfTheFlagAsObject(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flag-key', + 'value' => ['value' => 'value as object'], + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getObjectDetails('flag-key', ['default' => true], $this->defaultEvaluationContext); + assertEquals(['value' => 'value as object'], $got->getValue()); + assertEquals(Reason::TARGETING_MATCH, $got->getReason()); + assertEquals('default', $got->getVariant()); + assertEquals(null, $got->getError()); + assertEquals('flag-key', $got->getFlagKey()); + } + + public function testShouldReturnTheDefaultValueIfFlagIsNotTheRightType(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('integer_key', false, $this->defaultEvaluationContext); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::TYPE_MISMATCH(), $got->getError()->getResolutionErrorCode()); + assertEquals('Invalid type for integer_key, got integer expected boolean', $got->getError()->getResolutionErrorMessage()); + assertEquals('integer_key', $got->getFlagKey()); + } + + public function testShouldReturnTheDefaultValueOfTheFlagIfErrorSendByTheAPIHttpCode403(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(403, [], json_encode([])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('boolean_key', false, $this->defaultEvaluationContext); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::GENERAL(), $got->getError()->getResolutionErrorCode()); + assertEquals('Unauthorized access to the API', $got->getError()->getResolutionErrorMessage()); + assertEquals('boolean_key', $got->getFlagKey()); + } + + public function testShouldReturnTheDefaultValueOfTheFlagIfErrorSendByTheAPIHttpCode400(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(400, [], json_encode([ + 'key' => 'integer_key', + 'reason' => 'ERROR', + 'errorCode' => 'INVALID_CONTEXT', + 'errorDetails' => 'Error Details for invalid context', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('boolean_key', false, $this->defaultEvaluationContext); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::INVALID_CONTEXT(), $got->getError()->getResolutionErrorCode()); + assertEquals('Error Details for invalid context', $got->getError()->getResolutionErrorMessage()); + assertEquals('boolean_key', $got->getFlagKey()); + } + + public function testShouldReturnDefaultValueIfNoEvaluationContext(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('boolean_key', false); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::INVALID_CONTEXT(), $got->getError()->getResolutionErrorCode()); + assertEquals('Missing targetingKey in evaluation context', $got->getError()->getResolutionErrorMessage()); + assertEquals('boolean_key', $got->getFlagKey()); + } + + public function testShouldReturnDefaultValueIfEvaluationContextHasEmptyStringTargetingKey(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('boolean_key', false, new MutableEvaluationContext('')); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::INVALID_CONTEXT(), $got->getError()->getResolutionErrorCode()); + assertEquals('Missing targetingKey in evaluation context', $got->getError()->getResolutionErrorMessage()); + assertEquals('boolean_key', $got->getFlagKey()); + } + + public function testShouldReturnDefaultValueIfEvaluationContextHasNullTargetingKey(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('boolean_key', false, new MutableEvaluationContext(null)); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::INVALID_CONTEXT(), $got->getError()->getResolutionErrorCode()); + assertEquals('Missing targetingKey in evaluation context', $got->getError()->getResolutionErrorMessage()); + assertEquals('boolean_key', $got->getFlagKey()); + } + + public function testShouldReturnDefaultValueIfFlagKeyEmptyString(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $mockClient->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('', false, $this->defaultEvaluationContext); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::GENERAL(), $got->getError()->getResolutionErrorCode()); + assertEquals('An error occurred while evaluating the flag: Flag key is null or empty', $got->getError()->getResolutionErrorMessage()); + assertEquals('', $got->getFlagKey()); + } + + public function testReturnAnErrorAPIResponseIf500(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(500, [], json_encode([])); + + $mockClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn($mockResponse); + + $config = new Config('http://gofeatureflag.org/'); + $provider = new GoFeatureFlagProvider($config); + + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $got = $client->getBooleanDetails('boolean_flag', false, $this->defaultEvaluationContext); + assertEquals(false, $got->getValue()); + assertEquals(Reason::ERROR, $got->getReason()); + assertEquals(null, $got->getVariant()); + assertEquals(ErrorCode::GENERAL(), $got->getError()->getResolutionErrorCode()); + assertEquals('Unknown error occurred', $got->getError()->getResolutionErrorMessage()); + assertEquals('boolean_flag', $got->getFlagKey()); + } + + public function testShouldSendExporterMetadataInContext(): void + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'integer_key', + 'value' => 42, + 'reason' => 'TARGETING_MATCH', + 'variant' => 'default', + ])); + + $requestBody = ''; + $mockClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturnCallback(function ($request) use ($mockResponse, &$requestBody) { + $requestBody = $request->getBody()->getContents(); + + return $mockResponse; + }); + + $config = new Config( + 'http://gofeatureflag.org/', + null, + [], + ['key1' => 'value', 'key2' => 123, 'key3' => 123.45], + ); + + $provider = new GoFeatureFlagProvider($config); + $this->mockHttpClient($provider, $mockClient); + + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient(); + $client->getBooleanDetails('boolean_flag', false, $this->defaultEvaluationContext); + + // get the request body of the request received by the mock client + $want = ['key1' => 'value', + 'key2' => 123, + 'key3' => 123.45, + 'openfeature' => true, + 'provider' => 'php', + ]; + $got = json_decode($requestBody, true)['context']['gofeatureflag']['exporterMetadata']; + assertEquals($want, $got); + } + + protected function setUp(): void + { + parent::setUp(); + $this->defaultEvaluationContext = new MutableEvaluationContext('214b796a-807b-4697-b3a3-42de0ec10a37', new Attributes(['email' => 'contact@gofeatureflag.org'])); + } +} diff --git a/providers/GoFeatureFlag/tests/unit/controller/OfrepApiTest.php b/providers/GoFeatureFlag/tests/unit/controller/OfrepApiTest.php new file mode 100644 index 00000000..8caa13f7 --- /dev/null +++ b/providers/GoFeatureFlag/tests/unit/controller/OfrepApiTest.php @@ -0,0 +1,437 @@ +expectException(RateLimitedException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(429, [], json_encode([])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIfNotAuthorized401() + { + $this->expectException(UnauthorizedException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(401, [], json_encode([])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIfNotAuthorized403() + { + $this->expectException(UnauthorizedException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(403, [], json_encode([])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIfFlagNotFound404() + { + $this->expectException(FlagNotFoundException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(404, [], json_encode([])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIfUnknownHttpCode500() + { + $this->expectException(UnknownOfrepException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(500, [], json_encode([])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldReturnAnErrorResponseIf400() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(400, [], json_encode([ + 'key' => 'flagKey', + 'errorCode' => 'TYPE_MISMATCH', + 'errorDetails' => 'The flag value is not of the expected type', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $got = $api->evaluate('flagKey', $this->defaultEvaluationContext); + $this->assertInstanceOf(OfrepApiErrorResponse::class, $got); + $this->assertEquals(Reason::ERROR, $got->getReason()); + $this->assertEquals(ErrorCode::TYPE_MISMATCH(), $got->getErrorCode()); + $this->assertEquals('The flag value is not of the expected type', $got->getErrorDetails()); + } + + public function testShouldReturnAValidResponseIf200() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $got = $api->evaluate('flagKey', $this->defaultEvaluationContext); + $this->assertInstanceOf(OfrepApiSuccessResponse::class, $got); + $this->assertEquals(Reason::TARGETING_MATCH, $got->getReason()); + $this->assertEquals(true, $got->getValue()); + } + + public function testShouldRaiseAnErrorIf200AndJsonDoesNotContainTheRequiredKeysMissingValue() + { + $this->expectException(ParseException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIf200AndJsonDoesNotContainTheRequiredKeysMissingKey() + { + $this->expectException(ParseException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIf200AndJsonDoesNotContainTheRequiredKeysMissingReason() + { + $this->expectException(ParseException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'variant' => 'default', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIf200AndJsonDoesNotContainTheRequiredKeysMissingVariant() + { + $this->expectException(ParseException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIf400AndJsonDoesNotContainTheRequiredKeysMissingKey() + { + $this->expectException(ParseException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(400, [], json_encode([ + 'errorCode' => 'TYPE_MISMATCH', + 'errorDetails' => 'The flag value is not of the expected type', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldRaiseAnErrorIf400AndJsonDoesNotContainTheRequiredKeysMissingErrorCode() + { + $this->expectException(ParseException::class); + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(400, [], json_encode([ + 'key' => 'flagKey', + 'errorDetails' => 'The flag value is not of the expected type', + ])); + $mockClient->method('sendRequest')->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + public function testShouldNotBeAbleToCallTheApiAgainIfRateLimitedWithRetryAfterInt() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(429, ['Retry-After' => '1'], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->expects($this->exactly(1)) + ->method('sendRequest') + ->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + try { + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } catch (RateLimitedException $e) { + $this->assertInstanceOf(RateLimitedException::class, $e); + } + + try { + $api->evaluate('another-flag', $this->defaultEvaluationContext); + } catch (RateLimitedException $e) { + $this->assertInstanceOf(RateLimitedException::class, $e); + } + } + + public function testShouldBeAbleToCallTheApiAgainIfWeWaitAfterTheRetryAfterAsInt() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponseRateLimited = new Response(429, ['Retry-After' => '1'], json_encode([])); + $mockResponseSuccess = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->method('sendRequest')->will($this->onConsecutiveCalls($mockResponseRateLimited, $mockResponseSuccess)); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + try { + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } catch (RateLimitedException $e) { + $this->assertInstanceOf(RateLimitedException::class, $e); + } + + // Wait for 1.5 seconds + usleep(1500000); + + $got = $api->evaluate('another-flag', $this->defaultEvaluationContext); + $this->assertInstanceOf(OfrepApiSuccessResponse::class, $got); + } + + public function testShouldNotBeAbleToCallTheApiAgainIfRateLimitedWithRetryAfterDate() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(429, ['Retry-After' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1)], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->expects($this->exactly(1)) + ->method('sendRequest') + ->willReturn($mockResponse); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + try { + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } catch (RateLimitedException $e) { + $this->assertInstanceOf(RateLimitedException::class, $e); + } + + try { + $api->evaluate('another-flag', $this->defaultEvaluationContext); + } catch (RateLimitedException $e) { + $this->assertInstanceOf(RateLimitedException::class, $e); + } + } + + public function testShouldBeAbleToCallTheApiAgainIfWeWaitAfterTheRetryAfterAsDate() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponseRateLimited = new Response(429, ['Retry-After' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1)], json_encode([])); + $mockResponseSuccess = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + $mockClient->method('sendRequest')->will($this->onConsecutiveCalls($mockResponseRateLimited, $mockResponseSuccess)); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + try { + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } catch (RateLimitedException $e) { + $this->assertInstanceOf(RateLimitedException::class, $e); + } + + // Wait for 1.5 seconds + usleep(1500000); + + $got = $api->evaluate('another-flag', $this->defaultEvaluationContext); + $this->assertInstanceOf(OfrepApiSuccessResponse::class, $got); + } + + public function testShouldHaveAuthorizationHeaderIfApiKeyInConfig() + { + $mockClient = $this->createMock(ClientInterface::class); + $mockResponse = new Response(200, [], json_encode([ + 'key' => 'flagKey', + 'value' => true, + 'reason' => Reason::TARGETING_MATCH, + 'variant' => 'default', + ])); + + $mockClient->expects($this->once()) + ->method('sendRequest') + ->willReturnCallback(function ($req) use ($mockResponse) { + $this->assertArrayHasKey('Authorization', $req->getHeaders()); + $this->assertEquals('Bearer your-secure-api-key', $req->getHeader('Authorization')[0]); + + return $mockResponse; + }); + + $api = new OfrepApi(new Config('https://gofeatureflag.org/', apiKey: 'your-secure-api-key')); + $reflection = new ReflectionClass($api); + $property = $reflection->getProperty('client'); + $property->setAccessible(true); + $property->setValue($api, $mockClient); + + $api->evaluate('flagKey', $this->defaultEvaluationContext); + } + + protected function setUp(): void + { + parent::setUp(); + $this->defaultEvaluationContext = new MutableEvaluationContext('214b796a-807b-4697-b3a3-42de0ec10a37'); + } +} diff --git a/providers/Split/.gitignore b/providers/Split/.gitignore new file mode 100644 index 00000000..e1efd914 --- /dev/null +++ b/providers/Split/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/build \ No newline at end of file diff --git a/providers/Split/CHANGELOG.md b/providers/Split/CHANGELOG.md new file mode 100644 index 00000000..f9b4b8d2 --- /dev/null +++ b/providers/Split/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +## [1.0.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/split-provider-v1.0.0...open-feature/split-provider-1.0.0) (2024-09-04) + + +### ⚠ BREAKING CHANGES + +* reset release-please config ([#40](https://github.com/open-feature/php-sdk-contrib/issues/40)) +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Bug Fixes + +* declaration for provider name ([#105](https://github.com/open-feature/php-sdk-contrib/issues/105)) ([42919fd](https://github.com/open-feature/php-sdk-contrib/commit/42919fdb8a2d3992ac529ddd7e90d6b99b340732)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) +* reset release-please config ([#40](https://github.com/open-feature/php-sdk-contrib/issues/40)) ([b9a3b7e](https://github.com/open-feature/php-sdk-contrib/commit/b9a3b7e1017dc56ddfdd767fb8dab2d01b641699)) + +## [0.3.0](https://github.com/open-feature/php-sdk-contrib/compare/open-feature/split-provider-0.3.0...open-feature/split-provider-0.3.0) (2023-02-12) + + +### Features + +* deprecate php 7 ([#59](https://github.com/open-feature/php-sdk-contrib/issues/59)) ([d028e6d](https://github.com/open-feature/php-sdk-contrib/commit/d028e6d7741d07b7edef21b43b249fdb2d18d8f2)) + +## 0.3.0 (2023-01-09) + + +### ⚠ BREAKING CHANGES + +* reset release-please config ([#40](https://github.com/open-feature/php-sdk-contrib/issues/40)) +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) + +### Features + +* otel hook ([#22](https://github.com/open-feature/php-sdk-contrib/issues/22)) ([be3f1e9](https://github.com/open-feature/php-sdk-contrib/commit/be3f1e9ed37dee4bbce8e3701e4693c1b949c398)) +* **otel-hook:** autoload registration of otel hook ([#25](https://github.com/open-feature/php-sdk-contrib/issues/25)) ([789a95c](https://github.com/open-feature/php-sdk-contrib/commit/789a95c47bc278b333bf8b241b0e342baa27acc5)) + + +### Miscellaneous Chores + +* reset release-please ([#39](https://github.com/open-feature/php-sdk-contrib/issues/39)) ([b452080](https://github.com/open-feature/php-sdk-contrib/commit/b452080443d837c66b554b1bb1a07cadba5a152a)) +* reset release-please config ([#40](https://github.com/open-feature/php-sdk-contrib/issues/40)) ([b9a3b7e](https://github.com/open-feature/php-sdk-contrib/commit/b9a3b7e1017dc56ddfdd767fb8dab2d01b641699)) diff --git a/src/Split/README.md b/providers/Split/README.md similarity index 93% rename from src/Split/README.md rename to providers/Split/README.md index aa4faac3..edd7516b 100644 --- a/src/Split/README.md +++ b/providers/Split/README.md @@ -3,7 +3,7 @@ [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Latest Stable Version](http://poser.pugx.org/open-feature/split-provider/v)](https://packagist.org/packages/open-feature/split-provider) [![Total Downloads](http://poser.pugx.org/open-feature/split-provider/downloads)](https://packagist.org/packages/open-feature/split-provider) -![PHP 7.4+](https://img.shields.io/badge/php->=7.4-blue.svg) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) [![License](http://poser.pugx.org/open-feature/split-provider/license)](https://packagist.org/packages/open-feature/split-provider) ## Overview @@ -14,8 +14,8 @@ This package also builds on various PSRs (PHP Standards Recommendations) such as ## Installation -``` -$ composer require open-feature/split-provider // installs the latest version +```sh +composer require open-feature/split-provider ``` ## Usage @@ -83,7 +83,7 @@ if ($featureEnabled) { ### PHP Versioning -This library targets PHP version 7.4 and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK. +This library targets PHP version 8.0 and newer. As long as you have any 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`. @@ -95,4 +95,4 @@ We value having as few runtime dependencies as possible. The addition of any dep ### Testing -Run tests with `composer run test`. \ No newline at end of file +Run tests with `composer run test`. diff --git a/src/Split/composer.json b/providers/Split/composer.json similarity index 97% rename from src/Split/composer.json rename to providers/Split/composer.json index de496a9a..cdebe159 100644 --- a/src/Split/composer.json +++ b/providers/Split/composer.json @@ -22,8 +22,8 @@ } ], "require": { - "php": "^7.4 || ^8", - "open-feature/sdk": "^1.1.0", + "php": "^8", + "open-feature/sdk": "^2.0", "splitsoftware/split-sdk-php": "^7.1" }, "require-dev": { @@ -37,10 +37,10 @@ "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "~1.9.0", + "phpstan/phpstan": "~1.10.0", "phpstan/phpstan-mockery": "^1.0", "phpstan/phpstan-phpunit": "^1.1", - "psalm/plugin-mockery": "^0.9.1", + "psalm/plugin-mockery": "^0.11.0", "psalm/plugin-phpunit": "^0.18.0", "ramsey/coding-standard": "^2.0.3", "ramsey/composer-repl": "^1.4", diff --git a/providers/Split/examples/.gitignore b/providers/Split/examples/.gitignore new file mode 100644 index 00000000..149cf08d --- /dev/null +++ b/providers/Split/examples/.gitignore @@ -0,0 +1,2 @@ +/*/vendor +/*/composer.lock \ No newline at end of file diff --git a/src/Split/examples/SplitSDK/README.md b/providers/Split/examples/SplitSDK/README.md similarity index 100% rename from src/Split/examples/SplitSDK/README.md rename to providers/Split/examples/SplitSDK/README.md diff --git a/src/Split/examples/SplitSDK/composer.json b/providers/Split/examples/SplitSDK/composer.json similarity index 92% rename from src/Split/examples/SplitSDK/composer.json rename to providers/Split/examples/SplitSDK/composer.json index d5a1f317..8fdcef70 100644 --- a/src/Split/examples/SplitSDK/composer.json +++ b/providers/Split/examples/SplitSDK/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "open-feature/sdk": "^0.0.5", + "open-feature/sdk": "^1.2.0", "open-feature/split-provider": "^0.0.1" } } diff --git a/src/Split/examples/SplitSDK/src/main.php b/providers/Split/examples/SplitSDK/src/main.php similarity index 100% rename from src/Split/examples/SplitSDK/src/main.php rename to providers/Split/examples/SplitSDK/src/main.php diff --git a/providers/Split/phpcs.xml.dist b/providers/Split/phpcs.xml.dist new file mode 100644 index 00000000..55d9d3a1 --- /dev/null +++ b/providers/Split/phpcs.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + ./src + ./tests + + */tests/fixtures/* + */tests/*/fixtures/* + + + + + + + + + + + + + diff --git a/providers/Split/phpstan.neon.dist b/providers/Split/phpstan.neon.dist new file mode 100644 index 00000000..93c5b2d2 --- /dev/null +++ b/providers/Split/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/Split/phpunit.xml.dist b/providers/Split/phpunit.xml.dist new file mode 100644 index 00000000..9d4740e1 --- /dev/null +++ b/providers/Split/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + ./tests/unit + + + ./tests/integration + + + + + + ./src + + + + + + + + diff --git a/providers/Split/psalm-baseline.xml b/providers/Split/psalm-baseline.xml new file mode 100644 index 00000000..ceaa5778 --- /dev/null +++ b/providers/Split/psalm-baseline.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/providers/Split/psalm.xml b/providers/Split/psalm.xml new file mode 100644 index 00000000..c3e6c03c --- /dev/null +++ b/providers/Split/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/Split/src/SplitProvider.php b/providers/Split/src/SplitProvider.php similarity index 91% rename from src/Split/src/SplitProvider.php rename to providers/Split/src/SplitProvider.php index 9c93fc25..dee839fb 100644 --- a/src/Split/src/SplitProvider.php +++ b/providers/Split/src/SplitProvider.php @@ -31,9 +31,11 @@ class SplitProvider extends AbstractProvider implements Provider { - protected const NAME = 'SplitProvider'; + protected static string $NAME = 'SplitProvider'; - /** The Split factory will only be created one time */ + /** + * The Split factory will only be created one time + */ private static SplitFactoryInterface $factory; private ClientInterface $client; @@ -47,12 +49,11 @@ class SplitProvider extends AbstractProvider implements Provider * * @see https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK#configuration * - * @param string $apiKey The API key for Split * @param mixed[] $options The configuration options for the client * * @throws SplitFactoryCreationException */ - public function __construct(?string $apiKey = '', $options = []) + public function __construct(?string $apiKey = '', array $options = []) { if (isset(self::$factory)) { $factory = self::$factory; @@ -69,7 +70,7 @@ public function __construct(?string $apiKey = '', $options = []) $this->client = $factory->client(); } - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { Di::setLogger($logger); } @@ -104,7 +105,7 @@ public function resolveFloatValue(string $flagKey, float $defaultValue, ?Evaluat /** * @param mixed[] $defaultValue */ - public function resolveObjectValue(string $flagKey, $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { return $this->resolveValue($flagKey, FlagValueType::OBJECT, $defaultValue, $context); } @@ -112,7 +113,7 @@ public function resolveObjectValue(string $flagKey, $defaultValue, ?EvaluationCo /** * @param bool|string|int|float|mixed[] $defaultValue */ - private function resolveValue(string $flagKey, string $flagType, $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + private function resolveValue(string $flagKey, string $flagType, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { try { if (is_null($context)) { diff --git a/src/Split/src/errors/InvalidTreatmentTypeException.php b/providers/Split/src/errors/InvalidTreatmentTypeException.php similarity index 100% rename from src/Split/src/errors/InvalidTreatmentTypeException.php rename to providers/Split/src/errors/InvalidTreatmentTypeException.php diff --git a/src/Split/src/errors/SplitFactoryCreationException.php b/providers/Split/src/errors/SplitFactoryCreationException.php similarity index 100% rename from src/Split/src/errors/SplitFactoryCreationException.php rename to providers/Split/src/errors/SplitFactoryCreationException.php diff --git a/src/Split/src/errors/TargetingKeyMissingException.php b/providers/Split/src/errors/TargetingKeyMissingException.php similarity index 100% rename from src/Split/src/errors/TargetingKeyMissingException.php rename to providers/Split/src/errors/TargetingKeyMissingException.php diff --git a/src/Split/src/treatments/TreatmentParser.php b/providers/Split/src/treatments/TreatmentParser.php similarity index 100% rename from src/Split/src/treatments/TreatmentParser.php rename to providers/Split/src/treatments/TreatmentParser.php diff --git a/src/Split/src/treatments/TreatmentValidator.php b/providers/Split/src/treatments/TreatmentValidator.php similarity index 100% rename from src/Split/src/treatments/TreatmentValidator.php rename to providers/Split/src/treatments/TreatmentValidator.php diff --git a/src/Split/tests/TestCase.php b/providers/Split/tests/TestCase.php similarity index 100% rename from src/Split/tests/TestCase.php rename to providers/Split/tests/TestCase.php diff --git a/src/Split/tests/integration/SplitProviderTest.php b/providers/Split/tests/integration/SplitProviderTest.php similarity index 100% rename from src/Split/tests/integration/SplitProviderTest.php rename to providers/Split/tests/integration/SplitProviderTest.php diff --git a/src/Split/tests/integration/files/splits.yml b/providers/Split/tests/integration/files/splits.yml similarity index 100% rename from src/Split/tests/integration/files/splits.yml rename to providers/Split/tests/integration/files/splits.yml diff --git a/src/Split/tests/unit/SplitProviderTest.php b/providers/Split/tests/unit/SplitProviderTest.php similarity index 91% rename from src/Split/tests/unit/SplitProviderTest.php rename to providers/Split/tests/unit/SplitProviderTest.php index a6db81b3..8302ef71 100644 --- a/src/Split/tests/unit/SplitProviderTest.php +++ b/providers/Split/tests/unit/SplitProviderTest.php @@ -27,6 +27,7 @@ public function testCanBeInstantiated(): void // Then $this->assertNotNull($instance); $this->assertInstanceOf(Provider::class, $instance); + $this->assertEquals('SplitProvider', $instance->getMetadata()->getName()); } private function getPathToValidSplitFile(): string diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..c25cd034 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,31 @@ +{ + "bootstrap-sha": "91dbcca98f4a56ec37ce1b03bc0f39aeaba6dbd3", + "separate-pull-requests": true, + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "include-v-in-tag": false, + "packages": { + "hooks/DDTrace": { + "package-name": "open-feature/dd-trace-hook" + }, + "hooks/OpenTelemetry": { + "package-name": "open-feature/otel-hook" + }, + "hooks/Validators": { + "package-name": "open-feature/validators-hook" + }, + "providers/CloudBees": { + "package-name": "open-feature/cloudbees-provider" + }, + "providers/Flagd": { + "package-name": "open-feature/flagd-provider" + }, + "providers/Split": { + "package-name": "open-feature/split-provider" + }, + "providers/GoFeatureFlag": { + "package-name": "open-feature/go-feature-flag-provider" + } + } +} diff --git a/src/CloudBees/.release-please-manifest.json b/src/CloudBees/.release-please-manifest.json deleted file mode 100644 index 1332969b..00000000 --- a/src/CloudBees/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "0.0.1" -} \ No newline at end of file diff --git a/src/CloudBees/release-please-config.json b/src/CloudBees/release-please-config.json deleted file mode 100644 index 4593df0a..00000000 --- a/src/CloudBees/release-please-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "bootstrap-sha": "55230de0b59fc58a9bf442fdf88356d8ec3e84ac", - "packages": { - ".": { - "release-type": "php", - "prerelease": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "include-v-in-tag": false - } - } -} \ No newline at end of file diff --git a/src/Flagd/.github/workflows/release-please.yml b/src/Flagd/.github/workflows/release-please.yml deleted file mode 100644 index 93659057..00000000 --- a/src/Flagd/.github/workflows/release-please.yml +++ /dev/null @@ -1,18 +0,0 @@ -on: - push: - branches: - - main - -name: Run Release Please -jobs: - release-please: - runs-on: ubuntu-latest - - # Release-please creates a PR that tracks all changes - steps: - - uses: google-github-actions/release-please-action@v3 - id: release - with: - command: manifest - token: ${{secrets.GITHUB_TOKEN}} - default-branch: main diff --git a/src/Flagd/.release-please-manifest.json b/src/Flagd/.release-please-manifest.json deleted file mode 100644 index 1332969b..00000000 --- a/src/Flagd/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "0.0.1" -} \ No newline at end of file diff --git a/src/Flagd/examples/Grpc/README.md b/src/Flagd/examples/Grpc/README.md deleted file mode 100644 index 14077cf0..00000000 --- a/src/Flagd/examples/Grpc/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# OpenFeature flagd gRPC example - -This example provides a simple case of working with the OpenFeature API, creating a client, setting a provider, and resolving a value. - -It uses the `Flagd` provider and communicates over gRPC. \ No newline at end of file diff --git a/src/Flagd/examples/Grpc/composer.json b/src/Flagd/examples/Grpc/composer.json deleted file mode 100644 index fea77ede..00000000 --- a/src/Flagd/examples/Grpc/composer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "open-feature/flagd-grpc-example", - "description": "An example of using OpenFeature with the Flagd provider over gRPC", - "type": "project", - "license": "Apache-2.0", - "autoload": { - "psr-4": { - "OpenFeature\\Providers\\Examples\\FlagdGrpcExample\\": "src/" - } - }, - "authors": [ - { - "name": "Tom Carrio", - "email": "tom@carrio.dev" - } - ], - "require": { - "open-feature/sdk": "^0.0.5", - "monolog/monolog": "^2.8" - } -} diff --git a/src/Flagd/examples/Grpc/src/main.php b/src/Flagd/examples/Grpc/src/main.php deleted file mode 100644 index ab1118e5..00000000 --- a/src/Flagd/examples/Grpc/src/main.php +++ /dev/null @@ -1,39 +0,0 @@ -pushHandler(new StreamHandler('logs/openfeature.log', Logger::WARNING)); - -$api->setLogger($logger); - -// Configure a flagd provider -$provider = new FlagdProvider([ - 'host' => 'localhost', - 'port' => 8013, - 'secure' => false, - 'protocol' => 'grpc' -]); - -$api->setProvider($provider); - -// Retrieve an OpenFeatureClient -$client = $api->getClient('grpc-example', '1.0'); - -// Resolve a value -$flagValue = $client->getBooleanDetails('dev.openfeature.example_flag', true, null, null); - -$logger->info("Resolved the boolean value: " . ($flagValue ? 'true' : 'false')); \ No newline at end of file diff --git a/src/Flagd/examples/Http/README.md b/src/Flagd/examples/Http/README.md deleted file mode 100644 index a343e223..00000000 --- a/src/Flagd/examples/Http/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# OpenFeature flagd HTTP example - - -> ⚠️ Warning -> -> This example is based on currently unimplemented code. -> It may also never be provided, such that only gRPC is -> available, pending design decisions on the underlying -> `flagd` service - diff --git a/src/Flagd/release-please-config.json b/src/Flagd/release-please-config.json deleted file mode 100644 index 760779a7..00000000 --- a/src/Flagd/release-please-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "bootstrap-sha": "5488af04bf743a8d964b2986e8de7183a2ef9350", - "packages": { - ".": { - "release-type": "php", - "prerelease": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "include-v-in-tag": false - } - } -} \ No newline at end of file diff --git a/src/Flagd/src/grpc/GrpcService.php b/src/Flagd/src/grpc/GrpcService.php deleted file mode 100644 index 08bb5b31..00000000 --- a/src/Flagd/src/grpc/GrpcService.php +++ /dev/null @@ -1,161 +0,0 @@ -getHost(), $config->getPort()); - $secure = $config->isSecure(); - - return new GrpcService($target, $secure); - } - - private ServiceClient $client; - - private function __construct(string $hostname, bool $secure) - { - /** - * @psalm-suppress UndefinedClass - * @var ChannelCredentials $credentials - */ - $credentials = $secure ? ChannelCredentials::createSsl() : ChannelCredentials::createInsecure(); - - $this->client = new ServiceClient($hostname, [ - 'credentials' => $credentials, - ]); - } - - /** - * @param mixed[]|bool|DateTime|float|int|string|null $defaultValue - */ - public function resolveValue(string $flagKey, string $flagType, $defaultValue, ?EvaluationContext $context): ResolutionDetails - { - $methodName = $this->getMethodName($flagType); - $request = $this->getRequestInstance($flagType); - - $request->setFlagKey($flagKey); - $request->setContext($this->buildContextAsStruct($context)); - - /** @var UnaryCall $clientCall */ - $clientCall = $this->client->$methodName($request); - - /** @var mixed $maybeResponse */ - /** @var mixed $status */ - [$maybeResponse, $status] = $clientCall->wait(); - - if (!$this->isSuccessStatus($status)) { - $this->throwForStatus($status); - } - - if (!ResponseValidator::isResponse($maybeResponse)) { - throw new ResolutionError(ErrorCode::PARSE_ERROR(), 'The response type could not be parsed'); - } - - /** @var ResolveBooleanResponse|ResolveFloatResponse|ResolveIntResponse|ResolveObjectResponse|ResolveStringResponse $response */ - $response = $maybeResponse; - - if (!ResponseValidator::isCorrectType($response, $flagType)) { - throw new ResolutionError(ErrorCode::TYPE_MISMATCH(), 'The resolution type is incorrect'); - } - - return ResponseResolutionDetailsAdapter::fromResponse($response); - } - - private function getMethodName(string $flagType): string - { - switch ($flagType) { - case FlagValueType::BOOLEAN: - return 'resolveBoolean'; - case FlagValueType::FLOAT: - return 'resolveFloat'; - case FlagValueType::INTEGER: - return 'resolveInteger'; - case FlagValueType::OBJECT: - return 'resolveObject'; - case FlagValueType::STRING: - return 'resolveString'; - } - - throw new ResolutionError(ErrorCode::GENERAL(), 'Attempted to use invalid flag value type: ' . $flagType); - } - - /** - * @return ResolveBooleanRequest|ResolveFloatRequest|ResolveIntRequest|ResolveObjectRequest|ResolveStringRequest - */ - private function getRequestInstance(string $flagType) - { - switch ($flagType) { - case FlagValueType::BOOLEAN: - return new ResolveBooleanRequest(); - case FlagValueType::FLOAT: - return new ResolveFloatRequest(); - case FlagValueType::INTEGER: - return new ResolveIntRequest(); - case FlagValueType::OBJECT: - return new ResolveObjectRequest(); - case FlagValueType::STRING: - return new ResolveStringRequest(); - } - - throw new ResolutionError(ErrorCode::GENERAL(), 'Attempted to use invalid flag value type: ' . $flagType); - } - - /** - * @param mixed $status - */ - private function isSuccessStatus($status): bool - { - /** - * @psalm-suppress UndefinedConstant - */ - return $status === Grpc\STATUS_OK; - } - - /** - * @param mixed $status - */ - private function throwForStatus($status): void - { - switch ($status) { - default: - throw new ResolutionError(ErrorCode::GENERAL(), 'Error occurred in gRPC call'); - } - } - - private function buildContextAsStruct(?EvaluationContext $context): Struct - { - $contextArray = EvaluationContextArrayFactory::build($context); - - return new Struct($contextArray); - } -} diff --git a/src/Flagd/src/grpc/ResponseResolutionDetailsAdapter.php b/src/Flagd/src/grpc/ResponseResolutionDetailsAdapter.php deleted file mode 100644 index 541d3666..00000000 --- a/src/Flagd/src/grpc/ResponseResolutionDetailsAdapter.php +++ /dev/null @@ -1,53 +0,0 @@ -withValue($response['value']) - ->withReason($response['reason']) - ->withVariant($response['variant']) - ->build(); - } - - /** - * @param ResolveBooleanResponse|ResolveFloatResponse|ResolveIntResponse|ResolveObjectResponse|ResolveStringResponse $response - */ - public static function fromResponse($response): ResolutionDetails - { - /** @var bool|int|string|float|Struct $value */ - $value = $response->getValue(); - $reason = $response->getReason(); - $variant = $response->getVariant(); - - if ($value instanceof Struct) { - /** @var mixed[] $value */ - $value = json_decode($value->serializeToJsonString(), true); - } - - return (new ResolutionDetailsBuilder()) - ->withValue($value) - ->withReason($reason) - ->withVariant($variant) - ->build(); - } -} diff --git a/src/Flagd/src/grpc/ResponseValidator.php b/src/Flagd/src/grpc/ResponseValidator.php deleted file mode 100644 index deaa4d34..00000000 --- a/src/Flagd/src/grpc/ResponseValidator.php +++ /dev/null @@ -1,87 +0,0 @@ -getValue(); - - $actualType = self::determineType($value); - - return $expectedType !== $actualType; - } - - /** - * @param mixed $value - */ - private static function determineType($value): string - { - if (is_bool($value)) { - return FlagValueType::BOOLEAN; - } - - if (is_float($value)) { - return FlagValueType::FLOAT; - } - - if (is_int($value)) { - return FlagValueType::INTEGER; - } - - if (is_null($value) || is_array($value)) { - return FlagValueType::OBJECT; - } - - if (is_string($value)) { - return FlagValueType::STRING; - } - - throw new FlagValueTypeError('Unknown'); - } -} diff --git a/src/Split/.github/workflows/release-please.yml b/src/Split/.github/workflows/release-please.yml deleted file mode 100644 index 93659057..00000000 --- a/src/Split/.github/workflows/release-please.yml +++ /dev/null @@ -1,18 +0,0 @@ -on: - push: - branches: - - main - -name: Run Release Please -jobs: - release-please: - runs-on: ubuntu-latest - - # Release-please creates a PR that tracks all changes - steps: - - uses: google-github-actions/release-please-action@v3 - id: release - with: - command: manifest - token: ${{secrets.GITHUB_TOKEN}} - default-branch: main diff --git a/src/Split/.release-please-manifest.json b/src/Split/.release-please-manifest.json deleted file mode 100644 index 1332969b..00000000 --- a/src/Split/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "0.0.1" -} \ No newline at end of file diff --git a/src/Split/release-please-config.json b/src/Split/release-please-config.json deleted file mode 100644 index 5e4bef5f..00000000 --- a/src/Split/release-please-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "bootstrap-sha": "a694af0fb48146f386330b446274cc22a432e66d", - "packages": { - ".": { - "release-type": "php", - "prerelease": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "include-v-in-tag": false - } - } -} \ No newline at end of file