├── .coveralls.yml
├── .devcontainer
└── devcontainer.json
├── .github
├── ISSUE_TEMPLATE
│ ├── BUG-REPORT.yml
│ ├── ENHANCEMENT.yml
│ ├── FEATURE-REQUEST.md
│ └── config.yml
├── pull_request_template.md
└── workflows
│ ├── integration_test.yml
│ ├── php.yml
│ └── ticket_reference_check.yml
├── .gitignore
├── CHANGELOG.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bug-bash
├── Decide.php
├── DecideAll.php
├── DecideForKeys.php
├── ForcedDecision.php
├── OptiConfig.php
├── TrackEvent.php
└── _bug-bash-autoload.php
├── composer.json
├── composer.lock
├── phpcs.xml
├── phpunit.xml
├── phpunit_bootstrap.php
├── src
└── Optimizely
│ ├── Bucketer.php
│ ├── Config
│ ├── DatafileProjectConfig.php
│ └── ProjectConfigInterface.php
│ ├── Decide
│ ├── OptimizelyDecideOption.php
│ ├── OptimizelyDecision.php
│ └── OptimizelyDecisionMessage.php
│ ├── DecisionService
│ ├── DecisionService.php
│ └── FeatureDecision.php
│ ├── Entity
│ ├── Attribute.php
│ ├── Audience.php
│ ├── Event.php
│ ├── Experiment.php
│ ├── FeatureFlag.php
│ ├── FeatureVariable.php
│ ├── Group.php
│ ├── Rollout.php
│ ├── TrafficAllocation.php
│ ├── VariableUsage.php
│ └── Variation.php
│ ├── Enums
│ ├── CommonAudienceEvaluationLogs.php
│ ├── ControlAttributes.php
│ ├── DecisionNotificationTypes.php
│ ├── ExperimentAudienceEvaluationLogs.php
│ ├── ProjectConfigManagerConstants.php
│ └── RolloutAudienceEvaluationLogs.php
│ ├── ErrorHandler
│ ├── DefaultErrorHandler.php
│ ├── ErrorHandlerInterface.php
│ └── NoOpErrorHandler.php
│ ├── Event
│ ├── Builder
│ │ ├── EventBuilder.php
│ │ └── Params.php
│ ├── Dispatcher
│ │ ├── DefaultEventDispatcher.php
│ │ └── EventDispatcherInterface.php
│ └── LogEvent.php
│ ├── Exceptions
│ ├── InvalidAttributeException.php
│ ├── InvalidAudienceException.php
│ ├── InvalidCallbackArgumentCountException.php
│ ├── InvalidDatafileVersionException.php
│ ├── InvalidEventException.php
│ ├── InvalidEventTagException.php
│ ├── InvalidExperimentException.php
│ ├── InvalidFeatureFlagException.php
│ ├── InvalidFeatureVariableException.php
│ ├── InvalidGroupException.php
│ ├── InvalidInputException.php
│ ├── InvalidNotificationTypeException.php
│ ├── InvalidRolloutException.php
│ ├── InvalidVariationException.php
│ └── OptimizelyException.php
│ ├── Logger
│ ├── DefaultLogger.php
│ ├── LoggerInterface.php
│ └── NoOpLogger.php
│ ├── Notification
│ ├── NotificationCenter.php
│ └── NotificationType.php
│ ├── Optimizely.php
│ ├── OptimizelyConfig
│ ├── OptimizelyAttribute.php
│ ├── OptimizelyAudience.php
│ ├── OptimizelyConfig.php
│ ├── OptimizelyConfigService.php
│ ├── OptimizelyEvent.php
│ ├── OptimizelyExperiment.php
│ ├── OptimizelyFeature.php
│ ├── OptimizelyVariable.php
│ └── OptimizelyVariation.php
│ ├── OptimizelyFactory.php
│ ├── OptimizelyUserContext.php
│ ├── ProjectConfigManager
│ ├── HTTPProjectConfigManager.php
│ ├── ProjectConfigManagerInterface.php
│ └── StaticProjectConfigManager.php
│ ├── UserProfile
│ ├── Decision.php
│ ├── UserProfile.php
│ ├── UserProfileServiceInterface.php
│ └── UserProfileUtils.php
│ └── Utils
│ ├── ConditionTreeEvaluator.php
│ ├── ConfigParser.php
│ ├── CustomAttributeConditionEvaluator.php
│ ├── Errors.php
│ ├── EventTagUtils.php
│ ├── GeneratorUtils.php
│ ├── SemVersionConditionEvaluator.php
│ ├── Validator.php
│ ├── VariableTypeUtils.php
│ └── schema.json
└── tests
├── BucketerTest.php
├── ConfigTests
└── DatafileProjectConfigTest.php
├── DecisionServiceTests
└── DecisionServiceTest.php
├── ErrorHandlerTests
├── DefaultErrorHandlerTest.php
└── NoOpErrorHandlerTest.php
├── EventTests
├── DefaultEventDispatcherTest.php
├── EventBuilderTest.php
└── LogEventTest.php
├── LoggerTests
├── DefaultLoggerTest.php
└── NoOpLoggerTest.php
├── NotificationTests
└── NotificationCenterTest.php
├── OptimizelyConfigTests
├── OptimizelyConfigServiceTest.php
└── OptimizelyEntitiesTest.php
├── OptimizelyFactoryTest.php
├── OptimizelyTest.php
├── OptimizelyUserContextTest.php
├── ProjectConfigManagerTests
├── HTTPProjectConfigManagerTest.php
└── StaticProjectConfigManagerTest.php
├── UserProfileTests
├── UserProfileTest.php
└── UserProfileUtilsTest.php
└── UtilsTests
├── ConditionTreeEvaluatorTest.php
├── CustomAttributeConditionEvaluatorLoggingTest.php
├── CustomAttributeConditionEvaluatorTest.php
├── EventTagUtilsTest.php
├── ValidatorLoggingTest.php
├── ValidatorTest.php
└── VariableTypeUtilsTest.php
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: php-coveralls
2 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PHP SDK",
3 |
4 | "remoteEnv": {
5 | "SDK_ROOT": "/workspaces/php-sdk",
6 | "XDEBUG_CONFIG": "log_level=0",
7 | },
8 |
9 | "image": "mcr.microsoft.com/devcontainers/php:0-8.2",
10 |
11 | "postStartCommand": "composer install",
12 |
13 | "forwardPorts": [
14 | 8080
15 | ],
16 | "customizations": {
17 | "vscode": {
18 | "extensions": [
19 | "bmewburn.vscode-intelephense-client",
20 | "xdebug.php-debug",
21 | "DEVSENSE.composer-php-vscode",
22 | "xdebug.php-pack",
23 | "recca0120.vscode-phpunit",
24 | "eamodio.gitlens"
25 | ]
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG-REPORT.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug
2 | description: File a bug/issue
3 | title: "[BUG]
"
4 | labels: ["bug", "needs-triage"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Is there an existing issue for this?
9 | description: Please search to see if an issue already exists for the bug you encountered.
10 | options:
11 | - label: I have searched the existing issues
12 | required: true
13 | - type: textarea
14 | attributes:
15 | label: SDK Version
16 | description: Version of the SDK in use?
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: Current Behavior
22 | description: A concise description of what you're experiencing.
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Expected Behavior
28 | description: A concise description of what you expected to happen.
29 | validations:
30 | required: true
31 | - type: textarea
32 | attributes:
33 | label: Steps To Reproduce
34 | description: Steps to reproduce the behavior.
35 | placeholder: |
36 | 1. In this environment...
37 | 1. With this config...
38 | 1. Run '...'
39 | 1. See error...
40 | validations:
41 | required: true
42 | - type: textarea
43 | attributes:
44 | label: PHP Version
45 | description: What version of PHP are you using?
46 | validations:
47 | required: false
48 | - type: textarea
49 | attributes:
50 | label: Link
51 | description: Link to code demonstrating the problem.
52 | validations:
53 | required: false
54 | - type: textarea
55 | attributes:
56 | label: Logs
57 | description: Logs/stack traces related to the problem (⚠️do not include sensitive information).
58 | validations:
59 | required: false
60 | - type: dropdown
61 | attributes:
62 | label: Severity
63 | description: What is the severity of the problem?
64 | multiple: true
65 | options:
66 | - Blocking development
67 | - Affecting users
68 | - Minor issue
69 | validations:
70 | required: false
71 | - type: textarea
72 | attributes:
73 | label: Workaround/Solution
74 | description: Do you have any workaround or solution in mind for the problem?
75 | validations:
76 | required: false
77 | - type: textarea
78 | attributes:
79 | label: Recent Change
80 | description: Has this issue started happening after an update or experiment change?
81 | validations:
82 | required: false
83 | - type: textarea
84 | attributes:
85 | label: Conflicts
86 | description: Are there other libraries/dependencies potentially in conflict?
87 | validations:
88 | required: false
89 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml:
--------------------------------------------------------------------------------
1 | name: ✨Enhancement
2 | description: Create a new ticket for a Enhancement/Tech-initiative for the benefit of the SDK which would be considered for a minor version update.
3 | title: "[ENHANCEMENT] "
4 | labels: ["enhancement"]
5 | body:
6 | - type: textarea
7 | id: description
8 | attributes:
9 | label: Description
10 | description: Briefly describe the enhancement in a few sentences.
11 | placeholder: Short description...
12 | validations:
13 | required: true
14 | - type: textarea
15 | id: benefits
16 | attributes:
17 | label: Benefits
18 | description: How would the enhancement benefit to your product or usage?
19 | placeholder: Benefits...
20 | validations:
21 | required: true
22 | - type: textarea
23 | id: detail
24 | attributes:
25 | label: Detail
26 | description: How would you like the enhancement to work? Please provide as much detail as possible
27 | placeholder: Detailed description...
28 | validations:
29 | required: false
30 | - type: textarea
31 | id: examples
32 | attributes:
33 | label: Examples
34 | description: Are there any examples of this enhancement in other products/services? If so, please provide links or references.
35 | placeholder: Links/References...
36 | validations:
37 | required: false
38 | - type: textarea
39 | id: risks
40 | attributes:
41 | label: Risks/Downsides
42 | description: Do you think this enhancement could have any potential downsides or risks?
43 | placeholder: Risks/Downsides...
44 | validations:
45 | required: false
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md:
--------------------------------------------------------------------------------
1 |
4 | ## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/)
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 💡Feature Requests
4 | url: https://feedback.optimizely.com/
5 | about: Feedback requesting a new feature can be shared here.
6 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Summary
2 | - The "what"; a concise description of each logical change
3 | - Another change
4 |
5 | The "why", or other context.
6 |
7 | ## Test plan
8 |
9 | ## Issues
10 | - "THING-1234" or "Fixes #123"
11 |
--------------------------------------------------------------------------------
/.github/workflows/integration_test.yml:
--------------------------------------------------------------------------------
1 | name: Reusable action of running integration of production suite
2 |
3 | on:
4 | workflow_call:
5 | secrets:
6 | CI_USER_TOKEN:
7 | required: true
8 | TRAVIS_COM_TOKEN:
9 | required: true
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | with:
16 | # You should create a personal access token and store it in your repository
17 | token: ${{ secrets.CI_USER_TOKEN }}
18 | repository: 'optimizely/travisci-tools'
19 | path: 'home/runner/travisci-tools'
20 | ref: 'master'
21 | - name: set SDK Branch if PR
22 | env:
23 | HEAD_REF: ${{ github.head_ref }}
24 | if: ${{ github.event_name == 'pull_request' }}
25 | run: |
26 | echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV
27 | - name: set SDK Branch if not pull request
28 | env:
29 | REF_NAME: ${{github.ref_name}}
30 | if: ${{ github.event_name != 'pull_request' }}
31 | run: |
32 | echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV
33 | echo "TRAVIS_BRANCH=$REF_NAME}" >> $GITHUB_ENV
34 | - name: Trigger build
35 | env:
36 | SDK: php
37 | FULLSTACK_TEST_REPO: ${{ inputs.FULLSTACK_TEST_REPO }}
38 | BUILD_NUMBER: ${{ github.run_id }}
39 | TESTAPP_BRANCH: master
40 | GITHUB_TOKEN: ${{ secrets.CI_USER_TOKEN }}
41 | EVENT_TYPE: ${{ github.event_name }}
42 | GITHUB_CONTEXT: ${{ toJson(github) }}
43 | #REPO_SLUG: ${{ github.repository }}
44 | PULL_REQUEST_SLUG: ${{ github.repository }}
45 | UPSTREAM_REPO: ${{ github.repository }}
46 | PULL_REQUEST_SHA: ${{ github.event.pull_request.head.sha }}
47 | PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
48 | UPSTREAM_SHA: ${{ github.sha }}
49 | TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
50 | EVENT_MESSAGE: ${{ github.event.message }}
51 | HOME: 'home/runner'
52 | run: |
53 | echo "$GITHUB_CONTEXT"
54 | home/runner/travisci-tools/trigger-script-with-status-update.sh
55 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | linting:
11 | name: Linting
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v3
16 | - name: Set up PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: '8.2'
20 | - name: Install php code sniffer
21 | run: composer require "squizlabs/php_codesniffer=*"
22 | - name: Run linting
23 | run: composer lint
24 |
25 | source_clear:
26 | name: Source Clear Scan
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout code
30 | uses: actions/checkout@v3
31 | - name: Source clear scan
32 | env:
33 | SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }}
34 | run: curl -sSL https://download.sourceclear.com/ci.sh | bash -s – scan
35 |
36 | unit_tests:
37 | name: Unit Tests ${{ matrix.php-versions }}
38 | needs: [ linting, source_clear ]
39 | runs-on: ubuntu-latest
40 | strategy:
41 | fail-fast: false
42 | matrix:
43 | php-versions: [ '8.1', '8.2' ]
44 | steps:
45 | - name: Checkout code
46 | uses: actions/checkout@v3
47 | - name: Set up PHP v${{ matrix.php-versions }}
48 | uses: shivammathur/setup-php@v2
49 | with:
50 | php-version: ${{ matrix.php-versions }}
51 | - name: Cache Composer packages
52 | id: composer-cache
53 | uses: actions/cache@v3
54 | with:
55 | path: vendor
56 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
57 | restore-keys: |
58 | ${{ runner.os }}-php-
59 | - name: Install dependencies
60 | run: composer install
61 | - name: Run tests
62 | run: |
63 | mkdir -p ./build/logs
64 | ./vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml
65 | - name: Verify clover.xml created
66 | run: |
67 | if [ ! -f ./build/logs/clover.xml ]; then
68 | echo "clover.xml was not created"
69 | exit 1
70 | fi
71 | - name: Upload coverage results to Coveralls
72 | env:
73 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74 | run: |
75 | composer global require php-coveralls/php-coveralls
76 | php-coveralls --coverage_clover=./build/logs/clover.xml -v
77 |
78 | integration_tests:
79 | name: Integration Tests
80 | needs: [ unit_tests ]
81 | uses: optimizely/php-sdk/.github/workflows/integration_test.yml@master
82 | secrets:
83 | CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
84 | TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
85 |
--------------------------------------------------------------------------------
/.github/workflows/ticket_reference_check.yml:
--------------------------------------------------------------------------------
1 | name: Jira ticket reference check
2 |
3 | on:
4 | pull_request:
5 | types: [opened, edited, reopened, synchronize]
6 |
7 | jobs:
8 |
9 | jira_ticket_reference_check:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Check for Jira ticket reference
14 | uses: optimizely/github-action-ticket-reference-checker-public@master
15 | with:
16 | bodyRegex: 'FSSDK-(?\d+)'
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | build/
3 | vendor/
4 | composer.phar
5 | .phpunit.result.cache
6 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a comment.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in the repo.
5 | # Unless a later match takes precedence, @global-owner1 and @global-owner2
6 | # will be requested for review when someone opens a pull request.
7 | * @optimizely/fullstack-devs
8 |
9 | # Order is important; the last matching pattern takes the most precedence.
10 | # When someone opens a pull request that only modifies JS files, only @js-owner
11 | # and not the global owner(s) will be requested for a review.
12 | #*.js @js-owner
13 |
14 | # You can also use email addresses if you prefer. They'll be used to look up
15 | # users just like we do for commit author emails.
16 | #docs/* docs@example.com
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the Optimizely PHP SDK
2 |
3 | We welcome contributions and feedback! All contributors must sign our [Contributor License Agreement (CLA)](https://docs.google.com/a/optimizely.com/forms/d/e/1FAIpQLSf9cbouWptIpMgukAKZZOIAhafvjFCV8hS00XJLWQnWDFtwtA/viewform) to be eligible to contribute. Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code.
4 |
5 | ## Development process
6 |
7 | 1. Fork the repository and create your branch from master.
8 | 2. Please follow the [commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) for each commit message.
9 | 3. Make sure to add tests!
10 | 4. `git push` your changes to GitHub.
11 | 5. Open a PR from your fork into the master branch of the original repo.
12 | 6. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`.
13 | 7. Open a pull request from `YOUR_NAME/branch_name` to `master`.
14 | 8. A repository maintainer will review your pull request and, if all goes well, squash and merge it!
15 |
16 | ## Pull request acceptance criteria
17 |
18 | * **All code must have test coverage.** We use PHPUnit. Changes in functionality should have accompanying unit tests. Bug fixes should have accompanying regression tests.
19 | * Tests are located in `tests` with one file per class.
20 | * Lint your code with PHP CodeSniffer before submitting.
21 |
22 | ## Style
23 | We enforce [PSR-2](https://www.php-fig.org/psr/psr-2/) rules with some minor [deviations](phpcs.xml). Run linter by executing `composer lint` and autocorrect lint errors by executing `composer beautify`.
24 |
25 |
26 | ## License
27 |
28 | All contributions are under the CLA mentioned above. For this project, Optimizely uses the Apache 2.0 license, and so asks that by contributing your code, you agree to license your contribution under the terms of the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). Your contributions should also include the following header:
29 |
30 | ```
31 | /****************************************************************************
32 | * Copyright YEAR, Optimizely, Inc. and contributors *
33 | * *
34 | * Licensed under the Apache License, Version 2.0 (the "License"); *
35 | * you may not use this file except in compliance with the License. *
36 | * You may obtain a copy of the License at *
37 | * *
38 | * http://www.apache.org/licenses/LICENSE-2.0 *
39 | * *
40 | * Unless required by applicable law or agreed to in writing, software *
41 | * distributed under the License is distributed on an "AS IS" BASIS, *
42 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
43 | * See the License for the specific language governing permissions and *
44 | * limitations under the License. *
45 | ***************************************************************************/
46 | ```
47 |
48 | The YEAR above should be the year of the contribution. If work on the file has been done over multiple years, list each year in the section above. Example: Optimizely writes the file and releases it in 2014. No changes are made in 2015. Change made in 2016. YEAR should be “2014, 2016”.
49 |
50 | ## Contact
51 |
52 | If you have questions, please contact developers@optimizely.com.
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Optimizely PHP SDK
2 | [](https://packagist.org/packages/optimizely/optimizely-sdk)
3 | [](https://github.com/optimizely/php-sdk/actions/workflows/php.yml?query=branch%3Amaster)
4 | [](https://coveralls.io/github/optimizely/php-sdk?branch=master)
5 | [](https://packagist.org/packages/optimizely/optimizely-sdk)
6 | [](http://www.apache.org/licenses/LICENSE-2.0)
7 |
8 | This repository houses the PHP SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy).
9 |
10 | Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome).
11 |
12 | Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap.
13 |
14 | ## Get Started
15 |
16 | Refer to the [PHP SDK's developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/php-sdk) for detailed instructions on getting started with using the SDK.
17 |
18 | ### Requirements
19 |
20 | To access the Feature Management configuration in the Optimizely dashboard, please contact your Optimizely account executive.
21 |
22 | SDK version 4.0.0 requires PHP8+.
23 | SDK version 3 requires PHP5.5+ up to PHP7.
24 |
25 | ### Install the SDK
26 |
27 | The Optimizely PHP SDK can be installed through [Composer](https://getcomposer.org/). Please use the following command:
28 |
29 | ```bash
30 | php composer.phar require optimizely/optimizely-sdk
31 | ```
32 |
33 | ## Use the PHP SDK
34 |
35 | ### Initialization
36 |
37 | Create the Optimizely client, for example:
38 |
39 | ```php
40 | >);
45 | ```
46 |
47 | Or you may also use OptimizelyFactory method to create an optimizely client using your SDK key, an optional fallback datafile and an optional datafile access token. Using this method internally creates an HTTPProjectConfigManager. See [HTTPProjectConfigManager](#use-httpprojectconfigmanager) for further detail.
48 |
49 | ```php
50 | >,
57 | <>
58 | );
59 | ```
60 | To access your HTTPProjectConfigManager:
61 |
62 | ```php
63 | configManager;
69 | ```
70 |
71 | Or you can also provide an implementation of the [`ProjectConfigManagerInterface`](https://github.com/optimizely/php-sdk/blob/master/src/Optimizely/ProjectConfigManager/ProjectConfigManagerInterface.php) in the constructor:
72 |
73 | ```php
74 | >);
80 | $optimizely = new Optimizely(
81 | <>,
82 | null,
83 | null,
84 | null,
85 | false,
86 | null,
87 | $configManager
88 | );
89 | ```
90 |
91 | ### ProjectConfigManagerInterface
92 | [`ProjectConfigManagerInterface`](https://github.com/optimizely/php-sdk/blob/master/src/Optimizely/ProjectConfigManager/ProjectConfigManagerInterface.php) exposes `getConfig` method for retrieving `ProjectConfig` instance.
93 |
94 | ### HTTPProjectConfigManager
95 |
96 | [`HTTPProjectConfigManager`](https://github.com/optimizely/php-sdk/blob/master/src/Optimizely/ProjectConfigManager/HTTPProjectConfigManager.php)
97 | is an implementation of `ProjectConfigManagerInterface` interface.
98 |
99 | The `fetch` method makes a blocking HTTP GET request to the configured URL to download the
100 | project datafile and initialize an instance of the ProjectConfig.
101 |
102 | Calling `fetch` will update the internal ProjectConfig instance that will be returned by `getConfig`.
103 |
104 | ### Use HTTPProjectConfigManager
105 |
106 | ```php
107 | >);
112 | ```
113 |
114 | ### SDK key
115 | Optimizely project SDK key; required unless source URL is overridden.
116 |
117 | A notification will be triggered whenever a _new_ datafile is fetched and ProjectConfig is updated. To subscribe to these notifications, use the `$notificationCenter->addNotificationListener(NotificationType::OPTIMIZELY_CONFIG_UPDATE, $updateCallback)`.
118 |
119 | ## SDK Development
120 |
121 | ### Unit Tests
122 |
123 | You can run all unit tests with:
124 |
125 | ```bash
126 | ./vendor/bin/phpunit
127 | ```
128 |
129 | ### Contributing
130 |
131 | Please see [CONTRIBUTING](CONTRIBUTING.md).
132 |
133 | ### Other Optimizely SDKs
134 |
135 | - Agent - https://github.com/optimizely/agent
136 |
137 | - Android - https://github.com/optimizely/android-sdk
138 |
139 | - C# - https://github.com/optimizely/csharp-sdk
140 |
141 | - Flutter - https://github.com/optimizely/optimizely-flutter-sdk
142 |
143 | - Go - https://github.com/optimizely/go-sdk
144 |
145 | - Java - https://github.com/optimizely/java-sdk
146 |
147 | - JavaScript - https://github.com/optimizely/javascript-sdk
148 |
149 | - PHP - https://github.com/optimizely/php-sdk
150 |
151 | - Python - https://github.com/optimizely/python-sdk
152 |
153 | - React - https://github.com/optimizely/react-sdk
154 |
155 | - Ruby - https://github.com/optimizely/ruby-sdk
156 |
157 | - Swift - https://github.com/optimizely/swift-sdk
158 |
--------------------------------------------------------------------------------
/bug-bash/Decide.php:
--------------------------------------------------------------------------------
1 | ';
18 |
19 | // 2. Change this to your flag key
20 | const FLAG_KEY = '';
21 |
22 | // 3. Uncomment each scenario 1 by 1 modifying the contents of the method
23 | // to test additional scenarios.
24 |
25 | $test = new DecideTests();
26 | $test->verifyDecisionProperties();
27 | // $test->testWithVariationsOfDecideOptions();
28 | // $test->verifyLogsImpressionsEventsDispatched();
29 | // $test->verifyResultsPageInYourProjectShowsImpressionEvent();
30 | // $test->verifyDecisionListenerWasCalled();
31 | // $test->verifyAnInvalidFlagKeyIsHandledCorrectly();
32 |
33 | // 4. Change the current folder into the bug-bash directory
34 | // cd bug-bash/
35 |
36 | // 5. Run the following command to execute the uncommented tests above:
37 | // php Decide.php
38 |
39 | // https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-php
40 | class DecideTests
41 | {
42 | // verify decision return properties with default DecideOptions
43 | public function verifyDecisionProperties(): void
44 | {
45 | $decision = $this->userContext->decide(FLAG_KEY);
46 |
47 | $this->printDecision($decision, "Check that the following decision properties are expected for user $this->userId");
48 | }
49 |
50 | // test decide w all options: DISABLE_DECISION_EVENT, ENABLED_FLAGS_ONLY, IGNORE_USER_PROFILE_SERVICE, INCLUDE_REASONS, EXCLUDE_VARIABLES (will need to add variables)
51 | public function testWithVariationsOfDecideOptions(): void
52 | {
53 | $options = [
54 | OptimizelyDecideOption::INCLUDE_REASONS,
55 | // OptimizelyDecideOption::DISABLE_DECISION_EVENT,
56 | // OptimizelyDecideOption::ENABLED_FLAGS_ONLY, // ⬅️ Disable some of your flags
57 | // OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE,
58 | // OptimizelyDecideOption::EXCLUDE_VARIABLES,
59 | ];
60 |
61 | $decision = $this->userContext->decide(FLAG_KEY, $options);
62 |
63 | $this->printDecision($decision, 'Modify the OptimizelyDecideOptions and check the decision variables expected');
64 | }
65 |
66 | // verify in logs that impression event of this decision was dispatched
67 | public function verifyLogsImpressionsEventsDispatched(): void
68 | {
69 | // 💡️ Create a new flag with an A/B Test eg "product_version"
70 | $featureFlagKey = 'product_version';
71 | $logger = new DefaultLogger(Logger::DEBUG);
72 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
73 | $localUserContext = $localOptimizelyClient->createUserContext($this->userId);
74 |
75 | // review the DEBUG output, ensuring you see an impression log
76 | // "Dispatching impression event to URL https://logx.optimizely.com/v1/events with params..."
77 | $localUserContext->decide($featureFlagKey);
78 | }
79 |
80 | // verify on Results page that impression even was created
81 | public function verifyResultsPageInYourProjectShowsImpressionEvent(): void
82 | {
83 | print "Go to your project's results page and verify decisions events are showing (5 min delay)";
84 | }
85 |
86 | // verify that decision listener contains correct information
87 | public function verifyDecisionListenerWasCalled(): void
88 | {
89 | // Check that this was called during the...
90 | $onDecision = function ($type, $userId, $attributes, $decisionInfo) {
91 | print ">>> [$this->outputTag] OnDecision:
92 | type: $type,
93 | userId: $userId,
94 | attributes: " . print_r($attributes, true) . "
95 | decisionInfo: " . print_r($decisionInfo, true) . "\r\n";
96 | };
97 | $this->optimizelyClient->notificationCenter->addNotificationListener(
98 | NotificationType::DECISION,
99 | $onDecision
100 | );
101 |
102 | // ...decide.
103 | $this->userContext->decide(FLAG_KEY);
104 | }
105 |
106 | // verify that invalid flag key is handled correctly
107 | public function verifyAnInvalidFlagKeyIsHandledCorrectly(): void
108 | {
109 | $logger = new DefaultLogger(Logger::ERROR);
110 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
111 | $userId = 'user-' . mt_rand(10, 99);
112 | $localUserContext = $localOptimizelyClient->createUserContext($userId);
113 |
114 | // ensure you see an error -- Optimizely.ERROR: FeatureFlag Key "a_key_not_in_the_project" is not in datafile.
115 | $localUserContext->decide("a_key_not_in_the_project");
116 | }
117 |
118 | private Optimizely $optimizelyClient;
119 | private string $userId;
120 | private ?OptimizelyUserContext $userContext;
121 | private string $outputTag = "Decide";
122 |
123 | public function __construct()
124 | {
125 | $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY);
126 |
127 | $this->userId = 'user-' . mt_rand(10, 99);
128 | $attributes = ['age' => 25, 'country' => 'canada', 'abandoned_cart' => false];
129 | $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes);
130 | }
131 |
132 | private function printDecision($decision, $message): void
133 | {
134 | $enabled = $decision->getEnabled() ? "true" : "false";
135 |
136 | print ">>> [$this->outputTag] $message:
137 | enabled: $enabled,
138 | flagKey: {$decision->getFlagKey()},
139 | ruleKey: {$decision->getRuleKey()},
140 | variationKey: {$decision->getVariationKey()},
141 | variables: " . print_r($decision->getVariables(), true) . ",
142 | reasons: " . print_r($decision->getReasons(), true) . "\r\n";
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/bug-bash/DecideAll.php:
--------------------------------------------------------------------------------
1 | ';
18 |
19 | // 2. Create additional flag keys in your project (2+)
20 |
21 | // 3. Uncomment each scenario 1 by 1 modifying the contents of the method
22 | // to test additional scenarios.
23 |
24 | $test = new DecideAllTests();
25 | $test->verifyDecisionProperties();
26 | // $test->testWithVariousCombinationsOfOptions();
27 | // $test->verifyLogImpressionEventDispatched();
28 | // $test->verifyResultsPageShowsImpressionEvents();
29 | // $test->verifyDecisionListenerContainsCorrectInformation();
30 |
31 | // 4. Change the current folder into the bug-bash directory if you're not already there:
32 | // cd bug-bash/
33 |
34 | // 5. Run the following command to execute the uncommented tests above:
35 | // php DecideAll.php
36 |
37 | // https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-php
38 | class DecideAllTests
39 | {
40 | // verify decide all returns properties without specifying default options
41 | public function verifyDecisionProperties(): void
42 | {
43 | $decision = $this->userContext->decideAll();
44 |
45 | $this->printDecisions($decision, "Check that all of the decisions' multiple properties are expected for user `$this->userId`");
46 | }
47 |
48 | // test with all and variations/combinations of options
49 | public function testWithVariousCombinationsOfOptions(): void
50 | {
51 | $options = [
52 | OptimizelyDecideOption::INCLUDE_REASONS,
53 | // OptimizelyDecideOption::DISABLE_DECISION_EVENT,
54 | // OptimizelyDecideOption::ENABLED_FLAGS_ONLY, // ⬅️ Disable some of your flags
55 | // OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE,
56 | OptimizelyDecideOption::EXCLUDE_VARIABLES,
57 | ];
58 |
59 | $decisions = $this->userContext->decideAll($options);
60 |
61 | $this->printDecisions($decisions, "Check that all of your flags' decisions respected the options passed.");
62 | }
63 |
64 | // verify in logs that impression event of this decision was dispatched
65 | public function verifyLogImpressionEventDispatched(): void
66 | {
67 | // 💡️ Be sure you have >=1 of your project's flags has an EXPERIMENT type
68 | $logger = new DefaultLogger(Logger::DEBUG);
69 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
70 | $localUserContext = $localOptimizelyClient->createUserContext($this->userId);
71 |
72 | // review the DEBUG output, ensuring you see an impression log for each *EXPERIMENT* with a message like
73 | // "Dispatching impression event to URL https://logx.optimizely.com/v1/events with params..."
74 | // ⚠️ Rollout flag types should not dispatch and impression event
75 | $localUserContext->decideAll();
76 | }
77 |
78 | // verify on Results page that impression events was created
79 | public function verifyResultsPageShowsImpressionEvents(): void
80 | {
81 | print "After about 5-10 minutes, go to your project's results page and verify decisions events are showing.";
82 | }
83 |
84 | // verify that decision listener contains correct information
85 | public function verifyDecisionListenerContainsCorrectInformation(): void
86 | {
87 | // Check that this was called for each of your project flag keys
88 | $onDecision = function ($type, $userId, $attributes, $decisionInfo) {
89 | print ">>> [$this->outputTag] OnDecision:
90 | type: $type,
91 | userId: $userId,
92 | attributes: " . print_r($attributes, true) . "
93 | decisionInfo: " . print_r($decisionInfo, true) . "\r\n";
94 | };
95 | $this->optimizelyClient->notificationCenter->addNotificationListener(
96 | NotificationType::DECISION,
97 | $onDecision
98 | );
99 |
100 | $this->userContext->decideAll();
101 | }
102 |
103 | private Optimizely $optimizelyClient;
104 | private string $userId;
105 | private ?OptimizelyUserContext $userContext;
106 | private string $outputTag = "Decide All";
107 |
108 | public function __construct()
109 | {
110 | $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY);
111 |
112 | $this->userId = 'user-' . mt_rand(10, 99);
113 | $attributes = ['country' => 'nederland', 'age' => 43, 'is_return_visitor' => true];
114 | $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes);
115 | }
116 |
117 | private function printDecisions($decisions, $message): void
118 | {
119 | $count = 0;
120 | foreach ($decisions as $decision) {
121 | $enabled = $decision->getEnabled() ? "true" : "false";
122 |
123 | print ">>> [$this->outputTag #$count] $message:
124 | enabled: $enabled,
125 | flagKey: {$decision->getFlagKey()},
126 | ruleKey: {$decision->getRuleKey()},
127 | variationKey: {$decision->getVariationKey()},
128 | variables: " . print_r($decision->getVariables(), true) . ",
129 | reasons: " . print_r($decision->getReasons(), true) . "\r\n";
130 |
131 | $count++;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/bug-bash/DecideForKeys.php:
--------------------------------------------------------------------------------
1 | ';
18 |
19 | // 2. Check that you have 3+ flag keys in your project and add them here
20 | const FLAG_KEYS = ['', '', ''];
21 |
22 | // 3. Uncomment each scenario 1 by 1 modifying the contents of the method
23 | // to test additional scenarios.
24 |
25 | $test = new DecideForKeysTests();
26 | $test->verifyDecisionProperties();
27 | // $test->testWithVariationsOfDecideOptions();
28 | // $test->verifyLogsImpressionsEventsDispatched();
29 | // $test->verifyResultsPageInYourProjectShowsImpressionEvent();
30 | // $test->verifyDecisionListenerWasCalled();
31 | // $test->verifyAnInvalidFlagKeyIsHandledCorrectly();
32 |
33 | // 4. Change the current folder into the bug-bash directory if you've not already
34 | // cd bug-bash/
35 |
36 | // 5. Run the following command to execute the uncommented tests above:
37 | // php DecideForKeys.php
38 |
39 | // https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-php
40 | class DecideForKeysTests
41 | {
42 |
43 | // verify decision return properties with default DecideOptions
44 | public function verifyDecisionProperties(): void
45 | {
46 | $decision = $this->userContext->decideForKeys(FLAG_KEYS);
47 |
48 | $this->printDecisions($decision, "Check that the following decisions' properties are expected");
49 | }
50 |
51 | // test decide w all options: DISABLE_DECISION_EVENT, ENABLED_FLAGS_ONLY, IGNORE_USER_PROFILE_SERVICE, INCLUDE_REASONS, EXCLUDE_VARIABLES (will need to add variables)
52 | public function testWithVariationsOfDecideOptions(): void
53 | {
54 | $options = [
55 | OptimizelyDecideOption::INCLUDE_REASONS,
56 | // OptimizelyDecideOption::DISABLE_DECISION_EVENT,
57 | // OptimizelyDecideOption::ENABLED_FLAGS_ONLY, // ⬅️ Disable some of your flags
58 | // OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE,
59 | // OptimizelyDecideOption::EXCLUDE_VARIABLES,
60 | ];
61 |
62 | $decision = $this->userContext->decideForKeys(FLAG_KEYS, $options);
63 |
64 | $this->printDecisions($decision, "Modify the OptimizelyDecideOptions and check all the decisions' are as expected");
65 | }
66 |
67 | // verify in logs that impression event of this decision was dispatched
68 | public function verifyLogsImpressionsEventsDispatched(): void
69 | {
70 | // 💡️ Be sure that your FLAG_KEYS array above includes A/B Test eg "product_version"
71 | $logger = new DefaultLogger(Logger::DEBUG);
72 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
73 | $localUserContext = $localOptimizelyClient->createUserContext($this->userId);
74 |
75 | // review the DEBUG output, ensuring you see an impression log for each experiment type in your FLAG_KEYS
76 | // "Dispatching impression event to URL https://logx.optimizely.com/v1/events with params..."
77 | // ⚠️ Your Rollout type flags should not have impression events
78 | $localUserContext->decideForKeys(FLAG_KEYS);
79 | }
80 |
81 | // verify on Results page that impression even was created
82 | public function verifyResultsPageInYourProjectShowsImpressionEvent(): void
83 | {
84 | print "Go to your project's results page and verify decisions events are showing (5 min delay)";
85 | }
86 |
87 | // verify that decision listener contains correct information
88 | public function verifyDecisionListenerWasCalled(): void
89 | {
90 | // Check that this was called during the...
91 | $onDecision = function ($type, $userId, $attributes, $decisionInfo) {
92 | print ">>> [$this->outputTag] OnDecision:
93 | type: $type,
94 | userId: $userId,
95 | attributes: " . print_r($attributes, true) . "
96 | decisionInfo: " . print_r($decisionInfo, true) . "\r\n";
97 | };
98 | $this->optimizelyClient->notificationCenter->addNotificationListener(
99 | NotificationType::DECISION,
100 | $onDecision
101 | );
102 |
103 | // ...decide.
104 | $this->userContext->decide(FLAG_KEY);
105 | }
106 |
107 | // verify that invalid flag key is handled correctly
108 | public function verifyAnInvalidFlagKeyIsHandledCorrectly(): void
109 | {
110 | $logger = new DefaultLogger(Logger::ERROR);
111 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
112 | $userId = 'user-' . mt_rand(10, 99);
113 | $localUserContext = $localOptimizelyClient->createUserContext($userId);
114 |
115 | // ensure you see an error -- Optimizely.ERROR: FeatureFlag Key "a_key_not_in_the_project" is not in datafile.
116 | $localUserContext->decide("a_key_not_in_the_project");
117 | }
118 |
119 | private Optimizely $optimizelyClient;
120 | private string $userId;
121 | private ?OptimizelyUserContext $userContext;
122 | private string $outputTag = "Decide For Keys";
123 |
124 | public function __construct()
125 | {
126 | $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY);
127 |
128 | $this->userId = 'user-' . mt_rand(10, 99);
129 | $attributes = ['likes_yams' => true, 'cart_value' => 34.13, 'country' => 'sweden'];
130 | $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes);
131 | }
132 |
133 | private function printDecisions($decisions, $message): void
134 | {
135 | $count = 0;
136 | foreach ($decisions as $decision) {
137 | $enabled = $decision->getEnabled() ? "true" : "false";
138 |
139 | print ">>> [$this->outputTag #$count] $message:
140 | enabled: $enabled,
141 | flagKey: {$decision->getFlagKey()},
142 | ruleKey: {$decision->getRuleKey()},
143 | variationKey: {$decision->getVariationKey()},
144 | variables: " . print_r($decision->getVariables(), true) . ",
145 | reasons: " . print_r($decision->getReasons(), true) . "\r\n";
146 |
147 | $count++;
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/bug-bash/OptiConfig.php:
--------------------------------------------------------------------------------
1 | ";
17 | // $optimizelyClient = new Optimizely($sdkKey);
18 | $optimizelyClient = new Optimizely(null, null, null, null, false, null, null, null, $sdkKey);
19 | $user = $optimizelyClient->createUserContext('user123', ['attribute1' => 'hello']);
20 | $decision = $user->decide('flag1', [OptimizelyDecideOption::INCLUDE_REASONS]);
21 |
22 | $reasons = $decision->getReasons();
23 | echo "[OptimizelyConfig] reasons: " . json_encode($reasons) . PHP_EOL;
24 | echo "[OptimizelyConfig - flag key]: " . $decision->getFlagKey() . PHP_EOL;
25 | echo "[OptimizelyConfig - rule key]: " . $decision->getFlagKey() . PHP_EOL;
26 | echo "[OptimizelyConfig - enabled]: " . $decision->getEnabled() . PHP_EOL;
27 | echo "[OptimizelyConfig - variation key]: " . $decision->getVariationKey() . PHP_EOL;
28 | $variables = $decision->getVariables();
29 | echo "[OptimizelyConfig - variables]: " . json_encode($variables) . PHP_EOL;
30 | echo PHP_EOL;
31 |
32 | $user->trackEvent('myevent');
33 |
34 | echo "===========================" . PHP_EOL;
35 | echo " OPTIMIZELY CONFIG V2 " . PHP_EOL;
36 | echo "===========================" . PHP_EOL . PHP_EOL;
37 |
38 | $config = $optimizelyClient->getOptimizelyConfig();
39 | // get the revision
40 | echo "[OptimizelyConfig] revision:" . $config->getRevision() . PHP_EOL;
41 |
42 | // get the SDK key
43 | echo "[OptimizelyConfig] SDKKey:" . $config->getSdkKey() . PHP_EOL;
44 |
45 | // get the environment key
46 | echo "[OptimizelyConfig] environmentKey:" . $config->getEnvironmentKey() . PHP_EOL;
47 |
48 | // all attributes
49 | echo "[OptimizelyConfig] attributes:" . PHP_EOL;
50 | $attributes = $config->getAttributes();
51 | foreach($attributes as $attribute)
52 | {
53 | echo "[OptimizelyAttribute] -- (id, key) = ((" . $attribute->getId(). "), (". $attribute->getKey() . "))" . PHP_EOL;
54 | }
55 |
56 | // all audiences
57 | echo "[OptimizelyConfig] audiences:" . PHP_EOL;
58 | $audiences = $config->getAudiences();
59 | foreach($audiences as $audience)
60 | {
61 | echo "[OptimizelyAudience] -- (id, key, conditions) = ((" . $audience->getId(). "), (". $audience->getName() . "), (". $audience->getConditions() . "))" . PHP_EOL;
62 | }
63 |
64 | // all events
65 | echo "[OptimizelyConfig] events:" . PHP_EOL;
66 | $events = $config->getEvents();
67 | foreach($events as $event)
68 | {
69 | echo "[OptimizelyEvent] -- (id, key, experimentIds) = ((" . $event->getId(). "), (". $event->getKey() . "), (". $event->getExperimentIds() . "))" . PHP_EOL;
70 | }
71 |
72 | // all flags
73 | $flags = array_values((array)$config->getFeaturesMap());
74 | foreach ($flags as $flag)
75 | {
76 | // Use experimentRules and deliverRules
77 | $experimentRules = $flag->getExperimentRules();
78 | echo "------ Experiment rules -----" . PHP_EOL;
79 | foreach ($experimentRules as $experimentRule)
80 | {
81 | echo "---" . PHP_EOL;
82 | echo "[OptimizelyExperiment] - experiment rule-key = " . $experimentRule->getKey() . PHP_EOL;
83 | echo "[OptimizelyExperiment] - experiment audiences = " . PHP_EOL;$experimentRule->getExperimentAudiences();
84 | // all variations
85 | $variations = array_values((array)$experimentRule->getVariationsMap());
86 | foreach ($variations as $variation)
87 | {
88 | echo "[OptimizelyVariation] -- variation = { key: " . $variation->getKey() . ", . id: " . $variation->getId() . ", featureEnabled: " . $variation->getFeatureEnabled() . " }" . PHP_EOL;
89 | $variables = $variation->getVariablesMap();
90 | foreach ($variables as $variable)
91 | {
92 | echo "[OptimizelyVariable] --- variable: " . $variable->getKey() . ", " . $variable->getId() . PHP_EOL;
93 | // use variable data here.
94 | }
95 | // use experimentRule data here.
96 | }
97 | }
98 | $deliveryRules = $flag->getDeliveryRules();
99 | echo "------ Delivery rules -----" . PHP_EOL;
100 | foreach ($deliveryRules as $deliveryRule)
101 | {
102 | echo "---";
103 | echo "[OptimizelyExperiment] - delivery rule-key = " . $deliveryRule->getKey() . PHP_EOL;
104 | echo "[OptimizelyExperiment] - delivery audiences = " . $deliveryRule->getExperimentAudiences() . PHP_EOL;
105 |
106 | // use delivery rule data here...
107 | }
108 | }
109 | // $optimizelyClient->notificationCenter->addNotificationListener(
110 | // NotificationType::OPTIMIZELY_CONFIG_UPDATE,
111 | // function () {
112 | // $newConfig = $optimizelyClient->getOptimizelyConfig();
113 | // echo "[OptimizelyConfig] revision = " . $newConfig ? $newConfig->getRevision() : "" . PHP_EOL;
114 | // }
115 | // );
116 |
117 |
--------------------------------------------------------------------------------
/bug-bash/TrackEvent.php:
--------------------------------------------------------------------------------
1 | ';
17 |
18 | // 2. Add an event to your project, adding it to your Experiment flag as a metric, then set the key here
19 | const EVENT_KEY = '';
20 |
21 | // 3. Uncomment each scenario 1 by 1 modifying the contents of the method
22 | // to test additional scenarios.
23 |
24 | $test = new TrackEventTests();
25 | $test->checkTrackNotificationListenerProducesEvent();
26 | // $test->checkConversionEventLogDispatchedOnTrackEvent();
27 | // $test->checkConversionEventLogIsNOTDispatchedOnTrackEventForInvalidEventName();
28 | // $test->testEventTagsShowInDispatchedEventAndAppOptimizelyCom();
29 |
30 | // 4. Change the current folder into the bug-bash directory if you've not already
31 | // cd bug-bash/
32 |
33 | // 5. Run the following command to execute the uncommented tests above:
34 | // php TrackEvent.php
35 |
36 | // https://docs.developers.optimizely.com/feature-experimentation/docs/track-event-php
37 | class TrackEventTests
38 | {
39 | // check that track notification listener produces event with event key
40 | public function checkTrackNotificationListenerProducesEvent(): void
41 | {
42 | $this->optimizelyClient->notificationCenter->addNotificationListener(
43 | NotificationType::TRACK,
44 | $this->onTrackEvent // ⬅️ This should be called with a valid EVENT_NAME
45 | );
46 |
47 | // ...send track event.
48 | $this->userContext->trackEvent(EVENT_KEY);
49 | }
50 |
51 | // check that conversion event in the dispatch logs contains event key below
52 | public function checkConversionEventLogDispatchedOnTrackEvent(): void
53 | {
54 | $logger = new DefaultLogger(Logger::DEBUG);
55 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
56 | $localUserContext = $localOptimizelyClient->createUserContext($this->userId);
57 |
58 | $localUserContext->trackEvent(EVENT_KEY);
59 | }
60 |
61 | // check that event is NOT dispatched if invalid event key is used
62 | // test changing event key in the UI and in the code
63 | public function checkConversionEventLogIsNOTDispatchedOnTrackEventForInvalidEventName(): void
64 | {
65 | $logger = new DefaultLogger(Logger::DEBUG);
66 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
67 | $localUserContext = $localOptimizelyClient->createUserContext($this->userId);
68 | $this->optimizelyClient->notificationCenter->addNotificationListener(
69 | NotificationType::TRACK,
70 | $this->onTrackEvent // ⬅️ There should not be a Notification Listener OnTrackEvent called on invalid event name
71 | );
72 |
73 | // You should not see any "Optimizely.DEBUG: Dispatching conversion event" but instead see
74 | // "Optimizely.INFO: Not tracking user "{user-id}" for event "an_invalid_event_name_not_in_the_project".
75 | $localUserContext->trackEvent("an_invalid_event_name_not_in_the_project");
76 | }
77 |
78 | // try adding event tags (in the project and in the line below) and see if they show in the event body
79 | public function testEventTagsShowInDispatchedEventAndAppOptimizelyCom(): void
80 | {
81 | $logger = new DefaultLogger(Logger::DEBUG);
82 | $localOptimizelyClient = new Optimizely(datafile: null, logger: $logger, sdkKey: SDK_KEY);
83 | $localUserContext = $localOptimizelyClient->createUserContext($this->userId);
84 | $custom_tags = [
85 | 'shoe_size_paris_points' => 44,
86 | 'shoe_size_us_size' => 11.5,
87 | 'use_us_size' => false,
88 | 'color' => 'blue'
89 | ];
90 |
91 | // Dispatched event should have the tags added to the payload `params { ... }` and also
92 | // should show on app.optimizely.com Reports tab after 5-10 minutes
93 | $localUserContext->trackEvent(EVENT_KEY, $custom_tags);
94 | }
95 |
96 | private Optimizely $optimizelyClient;
97 | private string $userId;
98 | private ?OptimizelyUserContext $userContext;
99 | private string $outputTag = "Track Event";
100 | private \Closure $onTrackEvent;
101 |
102 | public function __construct()
103 | {
104 | $this->optimizelyClient = OptimizelyFactory::createDefaultInstance(SDK_KEY);
105 |
106 | $this->userId = 'user-' . mt_rand(10, 99);
107 | $attributes = ['age' => 19, 'country' => 'bangledesh', 'has_purchased' => true];
108 | $this->userContext = $this->optimizelyClient->createUserContext($this->userId, $attributes);
109 |
110 | $this->onTrackEvent = function ($type, $userId, $attributes, $decisionInfo) {
111 | print ">>> [$this->outputTag] OnTrackEvent:
112 | type: $type,
113 | userId: $userId,
114 | attributes: " . print_r($attributes, true) . "
115 | decisionInfo: " . print_r($decisionInfo, true) . "\r\n";
116 | };
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/bug-bash/_bug-bash-autoload.php:
--------------------------------------------------------------------------------
1 | =8.1",
19 | "guzzlehttp/guzzle": ">=6.2",
20 | "justinrainbow/json-schema": "^1.6 || ^2.0 || ^4.0 || ^5.0",
21 | "lastguest/murmurhash": "^1.3.0",
22 | "monolog/monolog": ">=1.21"
23 | },
24 | "require-dev": {
25 | "phpunit/phpunit": "^9.3",
26 | "php-coveralls/php-coveralls": "v2.5.3",
27 | "squizlabs/php_codesniffer": "3.7",
28 | "icecave/parity": "^3.0.1"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "Optimizely\\": "src/Optimizely"
33 | }
34 | },
35 | "config": {
36 | "sort-packages": true
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A custom coding standard for Optimizely PHP-SDK based on PSR2
5 |
6 |
7 | ./src
8 | ./tests
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | tests
13 |
14 |
15 |
16 |
17 | src/Optimizely
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Optimizely/Decide/OptimizelyDecideOption.php:
--------------------------------------------------------------------------------
1 | variationKey = $variationKey;
41 | $this->enabled = $enabled === null ? false : $enabled;
42 | $this->variables = $variables === null ? [] : $variables;
43 | $this->ruleKey = $ruleKey;
44 | $this->flagKey = $flagKey;
45 | $this->userContext = $userContext;
46 | $this->reasons = $reasons === null ? [] : $reasons;
47 | }
48 |
49 | public function getVariationKey()
50 | {
51 | return $this->variationKey;
52 | }
53 |
54 | public function getEnabled()
55 | {
56 | return $this->enabled;
57 | }
58 |
59 | public function getVariables()
60 | {
61 | return $this->variables;
62 | }
63 |
64 | public function getRuleKey()
65 | {
66 | return $this->ruleKey;
67 | }
68 |
69 | public function getFlagKey()
70 | {
71 | return $this->flagKey;
72 | }
73 |
74 | public function getUserContext()
75 | {
76 | return $this->userContext;
77 | }
78 |
79 | public function getReasons()
80 | {
81 | return $this->reasons;
82 | }
83 |
84 | #[\ReturnTypeWillChange]
85 | public function jsonSerialize()
86 | {
87 | return get_object_vars($this);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Optimizely/Decide/OptimizelyDecisionMessage.php:
--------------------------------------------------------------------------------
1 | _experiment = $experiment;
63 | $this->_variation = $variation;
64 | $this->_source = $source;
65 | $this->reasons = $reasons;
66 | }
67 |
68 | public function getExperiment()
69 | {
70 | return $this->_experiment;
71 | }
72 |
73 | public function getVariation()
74 | {
75 | return $this->_variation;
76 | }
77 |
78 | public function getSource()
79 | {
80 | return $this->_source;
81 | }
82 |
83 | public function getReasons()
84 | {
85 | return $this->reasons;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/Attribute.php:
--------------------------------------------------------------------------------
1 | _id = $id;
36 | $this->_key = $key;
37 | }
38 |
39 | /**
40 | * @return string Get attribute ID.
41 | */
42 | public function getId()
43 | {
44 | return $this->_id;
45 | }
46 |
47 | /**
48 | * @param $id string ID for attribute.
49 | */
50 | public function setId($id)
51 | {
52 | $this->_id = $id;
53 | }
54 |
55 | /**
56 | * @return string Get attribute key.
57 | */
58 | public function getKey()
59 | {
60 | return $this->_key;
61 | }
62 |
63 | /**
64 | * @param $key string Key for attribute.
65 | */
66 | public function setKey($key)
67 | {
68 | $this->_key = $key;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/Audience.php:
--------------------------------------------------------------------------------
1 | _id = $id;
46 | $this->_name = $name;
47 | $this->_conditions = $conditions;
48 | }
49 |
50 | /**
51 | * @return string ID of audience.
52 | */
53 | public function getId()
54 | {
55 | return $this->_id;
56 | }
57 |
58 | /**
59 | * @param $id string ID for audience.
60 | */
61 | public function setId($id)
62 | {
63 | $this->_id = $id;
64 | }
65 |
66 | /**
67 | * @return string Audience name.
68 | */
69 | public function getName()
70 | {
71 | return $this->_name;
72 | }
73 |
74 | /**
75 | * @param $name string Audience name
76 | */
77 | public function setName($name)
78 | {
79 | $this->_name = $name;
80 | }
81 |
82 | /**
83 | * @return string Conditions building the audience.
84 | */
85 | public function getConditions()
86 | {
87 | return $this->_conditions;
88 | }
89 |
90 | /**
91 | * @param $conditions string Audience conditions.
92 | */
93 | public function setConditions($conditions)
94 | {
95 | $this->_conditions = $conditions;
96 | }
97 |
98 | /**
99 | * @return array De-serialized audience conditions.
100 | */
101 | public function getConditionsList()
102 | {
103 | return $this->_conditionsList;
104 | }
105 |
106 | /**
107 | * @param $conditionsList array De-serialized audience conditions.
108 | */
109 | public function setConditionsList($conditionsList)
110 | {
111 | $this->_conditionsList = $conditionsList;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/Event.php:
--------------------------------------------------------------------------------
1 | _id = $id;
41 | $this->_key = $key;
42 | $this->_experimentIds = $experimentIds;
43 | }
44 |
45 | /**
46 | * @return string ID of event.
47 | */
48 | public function getId()
49 | {
50 | return $this->_id;
51 | }
52 |
53 | /**
54 | * @param $id string ID for event.
55 | */
56 | public function setId($id)
57 | {
58 | $this->_id = $id;
59 | }
60 |
61 | /**
62 | * @return string Event key.
63 | */
64 | public function getKey()
65 | {
66 | return $this->_key;
67 | }
68 |
69 | /**
70 | * @param $key string Key for event.
71 | */
72 | public function setKey($key)
73 | {
74 | $this->_key = $key;
75 | }
76 |
77 | /**
78 | * @return array Experiments using event.
79 | */
80 | public function getExperimentIds()
81 | {
82 | return $this->_experimentIds;
83 | }
84 |
85 | /**
86 | * @param $experimentIds array Experiments using event.
87 | */
88 | public function setExperimentIds($experimentIds)
89 | {
90 | $this->_experimentIds = $experimentIds;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/FeatureFlag.php:
--------------------------------------------------------------------------------
1 | _id = $id;
64 | $this->_key = $key;
65 | $this->_rolloutId = $rolloutId;
66 | $this->_experimentIds = $experimentIds;
67 | $this->_variables = ConfigParser::generateMap($variables, null, FeatureVariable::class);
68 | }
69 |
70 | /**
71 | * @return String feature flag ID
72 | */
73 | public function getId()
74 | {
75 | return $this->_id;
76 | }
77 |
78 | /**
79 | * @param String $id feature flag ID
80 | */
81 | public function setId($id)
82 | {
83 | $this->_id = $id;
84 | }
85 |
86 | /**
87 | * @return String feature flag key
88 | */
89 | public function getKey()
90 | {
91 | return $this->_key;
92 | }
93 |
94 | /**
95 | * @param String $key feature flag key
96 | */
97 | public function setKey($key)
98 | {
99 | $this->_key = $key;
100 | }
101 |
102 | /**
103 | * @return String attached rollout ID
104 | */
105 | public function getRolloutId()
106 | {
107 | return $this->_rolloutId;
108 | }
109 |
110 | /**
111 | * @param String $rolloutId attached rollout ID
112 | */
113 | public function setRolloutId($rolloutId)
114 | {
115 | $this->_rolloutId = $rolloutId;
116 | }
117 |
118 | /**
119 | * @return [String] attached experiment IDs
120 | */
121 | public function getExperimentIds()
122 | {
123 | return $this->_experimentIds;
124 | }
125 |
126 | /**
127 | * @param [String] $experimentIds attached experiment IDs
128 | */
129 | public function setExperimentIds($experimentIds)
130 | {
131 | $this->_experimentIds = $experimentIds;
132 | }
133 |
134 | /**
135 | * @return [FeatureVariable] feature variables that are part of this feature
136 | */
137 | public function getVariables()
138 | {
139 | return $this->_variables;
140 | }
141 |
142 | /**
143 | * @param [FeatureVariable] $variables feature variables that are part of this feature
144 | */
145 | public function setVariables($variables)
146 | {
147 | $this->_variables = ConfigParser::generateMap($variables, null, FeatureVariable::class);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/FeatureVariable.php:
--------------------------------------------------------------------------------
1 | _id = $id;
69 | $this->_key = $key;
70 | $this->_type = $type;
71 | $this->_defaultValue = $defaultValue;
72 | }
73 |
74 | /**
75 | * @return String Feature variable ID
76 | */
77 | public function getId()
78 | {
79 | return $this->_id;
80 | }
81 |
82 | /**
83 | * @param String $id Feature variable ID
84 | */
85 | public function setId($id)
86 | {
87 | $this->_id = $id;
88 | }
89 |
90 | /**
91 | * @return String Feature variable Key
92 | */
93 | public function getKey()
94 | {
95 | return $this->_key;
96 | }
97 |
98 | /**
99 | * @param String $key Feature variable Key
100 | */
101 | public function setKey($key)
102 | {
103 | $this->_key = $key;
104 | }
105 |
106 | /**
107 | * @return String Feature variable primitive type
108 | */
109 | public function getType()
110 | {
111 | return $this->_type;
112 | }
113 |
114 | /**
115 | * @param String $type Feature variable primitive type
116 | */
117 | public function setType($type)
118 | {
119 | $this->_type = $type;
120 | }
121 |
122 | /**
123 | * @return String Default value of the feature variable
124 | */
125 | public function getDefaultValue()
126 | {
127 | return $this->_defaultValue;
128 | }
129 |
130 | /**
131 | * @param String $value Default value of the feature variable
132 | */
133 | public function setDefaultValue($value)
134 | {
135 | $this->_defaultValue = $value;
136 | }
137 |
138 | /**
139 | * Returns feature variable method name based on
140 | * feature variable type.
141 | *
142 | * @param String $type Feature variable type.
143 | */
144 | public static function getFeatureVariableMethodName($type)
145 | {
146 | switch ($type) {
147 | case FeatureVariable::BOOLEAN_TYPE:
148 | return "getFeatureVariableBoolean";
149 | case FeatureVariable::DOUBLE_TYPE:
150 | return "getFeatureVariableDouble";
151 | case FeatureVariable::INTEGER_TYPE:
152 | return "getFeatureVariableInteger";
153 | case FeatureVariable::JSON_TYPE:
154 | return "getFeatureVariableJSON";
155 | case FeatureVariable::STRING_TYPE:
156 | return "getFeatureVariableString";
157 | default:
158 | return null;
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/Group.php:
--------------------------------------------------------------------------------
1 | _id = $id;
48 | $this->_policy = $policy;
49 | $this->_experiments = $experiments;
50 | $this->_trafficAllocation = $trafficAllocation;
51 | }
52 |
53 | /**
54 | * @return string ID of the group.
55 | */
56 | public function getId()
57 | {
58 | return $this->_id;
59 | }
60 |
61 | /**
62 | * @param $id string ID for group.
63 | */
64 | public function setId($id)
65 | {
66 | $this->_id = $id;
67 | }
68 |
69 | /**
70 | * @return string Policy of the group.
71 | */
72 | public function getPolicy()
73 | {
74 | return $this->_policy;
75 | }
76 |
77 | /**
78 | * @param $policy string Policy for group.
79 | */
80 | public function setPolicy($policy)
81 | {
82 | $this->_policy = $policy;
83 | }
84 |
85 | /**
86 | * @return array Experiments in the group.
87 | */
88 | public function getExperiments()
89 | {
90 | return $this->_experiments;
91 | }
92 |
93 | /**
94 | * @param $experiments array Experiments in the group.
95 | */
96 | public function setExperiments($experiments)
97 | {
98 | $this->_experiments = $experiments;
99 | }
100 |
101 | /**
102 | * @return array Traffic allocation of experiments in group.
103 | */
104 | public function getTrafficAllocation()
105 | {
106 | return $this->_trafficAllocation;
107 | }
108 |
109 | /**
110 | * @param $trafficAllocation array Traffic allocation of experiments in group.
111 | */
112 | public function setTrafficAllocation($trafficAllocation)
113 | {
114 | $this->_trafficAllocation = ConfigParser::generateMap($trafficAllocation, null, TrafficAllocation::class);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/Rollout.php:
--------------------------------------------------------------------------------
1 | _id = $id;
42 | $this->_experiments = ConfigParser::generateMap($experiments, null, Experiment::class);
43 | }
44 |
45 | /**
46 | * @return String ID of the rollout
47 | */
48 | public function getId()
49 | {
50 | return $this->_id;
51 | }
52 |
53 | /**
54 | * @param String $id ID of the rollout
55 | */
56 | public function setId($id)
57 | {
58 | $this->_id = $id;
59 | }
60 |
61 | /**
62 | * @return [Experiments] A list of experiments representing the different rules of the rollout
63 | */
64 | public function getExperiments()
65 | {
66 | return $this->_experiments;
67 | }
68 |
69 | /**
70 | * @param [Experiments] $experiments A list of experiments representing the different rules of the rollout
71 | */
72 | public function setExperiments($experiments)
73 | {
74 | $this->_experiments = ConfigParser::generateMap($experiments, null, Experiment::class);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/TrafficAllocation.php:
--------------------------------------------------------------------------------
1 | _entityId = $entityId;
35 | $this->_endOfRange = $endOfRange;
36 | }
37 |
38 | /**
39 | * @return string Entity ID representing experiment or variation.
40 | */
41 | public function getEntityId()
42 | {
43 | return $this->_entityId;
44 | }
45 |
46 | /**
47 | * @param $entityId string ID of experiment or group.
48 | */
49 | public function setEntityId($entityId)
50 | {
51 | $this->_entityId = $entityId;
52 | }
53 |
54 | /**
55 | * @return integer End of range for experiment or variation.
56 | */
57 | public function getEndOfRange()
58 | {
59 | return $this->_endOfRange;
60 | }
61 |
62 | /**
63 | * @param $endOfRange integer End of range for experiment or variation.
64 | */
65 | public function setEndOfRange($endOfRange)
66 | {
67 | $this->_endOfRange = $endOfRange;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/VariableUsage.php:
--------------------------------------------------------------------------------
1 | _id = $id;
40 | $this->_value = $value;
41 | }
42 |
43 | /**
44 | * @return String ID of the live variable this usage is modifying
45 | */
46 | public function getId()
47 | {
48 | return $this->_id;
49 | }
50 |
51 | /**
52 | * @param String $id ID of the live variable this usage is modifying
53 | */
54 | public function setId($id)
55 | {
56 | $this->_id = $id;
57 | }
58 |
59 | /**
60 | * @return String variable value for users in this particular variation
61 | */
62 | public function getValue()
63 | {
64 | return $this->_value;
65 | }
66 |
67 | /**
68 | * @param String $value variable value for users in this particular variation
69 | */
70 | public function setValue($value)
71 | {
72 | $this->_value = $value;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Optimizely/Entity/Variation.php:
--------------------------------------------------------------------------------
1 | associative array
46 | */
47 | private $_variableIdToVariableUsageInstanceMap;
48 |
49 | /**
50 | * @var mixed|null
51 | */
52 | private mixed $_featureEnabled;
53 |
54 | public function __construct($id = null, $key = null, $featureEnabled = null, $variableUsageInstances = [])
55 | {
56 | $this->_id = $id;
57 | $this->_key = $key;
58 | $this->_featureEnabled = $featureEnabled;
59 |
60 | $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null, VariableUsage::class);
61 |
62 | $this->generateVariableIdToVariableUsageMap();
63 | }
64 |
65 | /**
66 | * @return string Variation ID.
67 | */
68 | public function getId()
69 | {
70 | return $this->_id;
71 | }
72 |
73 | /**
74 | * @param $id string ID for variation.
75 | */
76 | public function setId($id)
77 | {
78 | $this->_id = $id;
79 | }
80 |
81 | /**
82 | * @return string Variation key.
83 | */
84 | public function getKey()
85 | {
86 | return $this->_key;
87 | }
88 |
89 | /**
90 | * @param $key string Key for variation.
91 | */
92 | public function setKey($key)
93 | {
94 | $this->_key = $key;
95 | }
96 |
97 | /**
98 | * @return boolean featureEnabled property
99 | */
100 | public function getFeatureEnabled()
101 | {
102 | return $this->_featureEnabled;
103 | }
104 |
105 | /**
106 | * @param boolean $flag
107 | */
108 | public function setFeatureEnabled($flag)
109 | {
110 | $this->_featureEnabled = $flag;
111 | }
112 |
113 | /**
114 | * @return [VariableUsage] Variable usage instances in this variation
115 | */
116 | public function getVariables()
117 | {
118 | return $this->_variableUsageInstances;
119 | }
120 |
121 | /**
122 | * @param string Variable ID
123 | *
124 | * @return VariableUsage Variable usage instance corresponding to given variable ID
125 | */
126 | public function getVariableUsageById($variableId)
127 | {
128 | return $this->_variableIdToVariableUsageInstanceMap[$variableId] ?? null;
129 | }
130 |
131 | /**
132 | * @param [VariableUsage] array of variable usage instances
133 | */
134 | public function setVariables($variableUsageInstances)
135 | {
136 | $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null, VariableUsage::class);
137 | $this->generateVariableIdToVariableUsageMap();
138 | }
139 |
140 | /**
141 | * Generates variable ID to Variable usage instance map
142 | * from variable usage instances
143 | */
144 | private function generateVariableIdToVariableUsageMap()
145 | {
146 | if (!empty($this->_variableUsageInstances)) {
147 | foreach ($this->_variableUsageInstances as $variableUsage) {
148 | $this->_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage;
149 | }
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Optimizely/Enums/CommonAudienceEvaluationLogs.php:
--------------------------------------------------------------------------------
1 | httpClient = $httpClient ?: new HttpClient();
42 | }
43 |
44 | public function dispatchEvent(LogEvent $event)
45 | {
46 | $options = [
47 | 'headers' => $event->getHeaders(),
48 | 'json' => $event->getParams(),
49 | 'timeout' => DefaultEventDispatcher::TIMEOUT,
50 | 'connect_timeout' => DefaultEventDispatcher::TIMEOUT
51 | ];
52 |
53 | $this->httpClient->request($event->getHttpVerb(), $event->getUrl(), $options);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Optimizely/Event/Dispatcher/EventDispatcherInterface.php:
--------------------------------------------------------------------------------
1 | _url = $url;
53 | $this->_params = $params;
54 | $this->_httpVerb = $httpVerb;
55 | $this->_headers = $headers;
56 | }
57 |
58 | /**
59 | * @return string The URL to dispatch the request to.
60 | */
61 | public function getUrl()
62 | {
63 | return $this->_url;
64 | }
65 |
66 | /**
67 | * @return array The parameters to send with the request.
68 | */
69 | public function getParams()
70 | {
71 | return $this->_params;
72 | }
73 |
74 | /**
75 | * @return string HTTP verb to be used when dispatching the request.
76 | */
77 | public function getHttpVerb()
78 | {
79 | return $this->_httpVerb;
80 | }
81 |
82 | /**
83 | * @return array The headers to set for the request.
84 | */
85 | public function getHeaders()
86 | {
87 | return $this->_headers;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Optimizely/Exceptions/InvalidAttributeException.php:
--------------------------------------------------------------------------------
1 | setFormatter($formatter);
46 | $this->logger = new Logger('Optimizely');
47 | $this->logger->pushHandler($streamHandler);
48 | }
49 |
50 | public function log($logLevel, $logMessage)
51 | {
52 | $this->logger->log($logLevel, $logMessage);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Optimizely/Logger/LoggerInterface.php:
--------------------------------------------------------------------------------
1 | getConstants());
49 |
50 | return in_array($notification_type, $notificationTypeList);
51 | }
52 |
53 | public static function getAll()
54 | {
55 | $oClass = new \ReflectionClass(__CLASS__);
56 | return $oClass->getConstants();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php:
--------------------------------------------------------------------------------
1 | id = $id;
34 | $this->key = $key;
35 | }
36 |
37 | /**
38 | * @return string attribute id.
39 | */
40 | public function getId()
41 | {
42 | return $this->id;
43 | }
44 |
45 | /**
46 | * @return string attribute key.
47 | */
48 | public function getKey()
49 | {
50 | return $this->key;
51 | }
52 |
53 | /**
54 | * @return string JSON representation of the object.
55 | */
56 | #[\ReturnTypeWillChange]
57 | public function jsonSerialize()
58 | {
59 | return get_object_vars($this);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php:
--------------------------------------------------------------------------------
1 | id = $id;
40 | $this->name = $name;
41 | $this->conditions = $conditions;
42 | }
43 |
44 | /**
45 | * @return string audience id.
46 | */
47 | public function getId()
48 | {
49 | return $this->id;
50 | }
51 |
52 | /**
53 | * @return string audience name.
54 | */
55 | public function getName()
56 | {
57 | return $this->name;
58 | }
59 |
60 | /**
61 | * @return string audience conditions.
62 | */
63 | public function getConditions()
64 | {
65 | return $this->conditions;
66 | }
67 |
68 | /**
69 | * @return string JSON representation of the object.
70 | */
71 | #[\ReturnTypeWillChange]
72 | public function jsonSerialize()
73 | {
74 | return get_object_vars($this);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php:
--------------------------------------------------------------------------------
1 | associative array
44 | */
45 | private $experimentsMap;
46 |
47 | /**
48 | * Map of Feature Keys to OptimizelyFeatures.
49 | *
50 | * @var associative array
51 | */
52 | private $featuresMap;
53 |
54 | /**
55 | * Array of attributes as OptimizelyAttribute.
56 | *
57 | * @var [OptimizelyAttribute]
58 | */
59 | private $attributes;
60 |
61 | /**
62 | * Array of audiences as OptimizelyAudience.
63 | *
64 | * @var [OptimizelyAudience]
65 | */
66 | private $audiences;
67 |
68 | /**
69 | * Array of events as OptimizelyEvent.
70 | *
71 | * @var [OptimizelyEvent]
72 | */
73 | private $events;
74 |
75 | /**
76 | * @var string Contents of datafile.
77 | */
78 | private $datafile;
79 |
80 |
81 | public function __construct($revision, array $experimentsMap, array $featuresMap, $datafile = null, $environmentKey = '', $sdkKey = '', $attributes = [], $audiences = [], $events = [])
82 | {
83 | $this->environmentKey = $environmentKey;
84 | $this->sdkKey = $sdkKey;
85 | $this->revision = $revision;
86 | $this->experimentsMap = $experimentsMap;
87 | $this->featuresMap = $featuresMap;
88 | $this->attributes = $attributes;
89 | $this->audiences = $audiences;
90 | $this->events = $events;
91 | $this->datafile = $datafile;
92 | }
93 |
94 | /**
95 | * @return string Config environmentKey.
96 | */
97 | public function getEnvironmentKey()
98 | {
99 | return $this->environmentKey;
100 | }
101 |
102 | /**
103 | * @return string Config sdkKey.
104 | */
105 | public function getSdkKey()
106 | {
107 | return $this->sdkKey;
108 | }
109 |
110 | /**
111 | * @return string Config revision.
112 | */
113 | public function getRevision()
114 | {
115 | return $this->revision;
116 | }
117 |
118 | /**
119 | * @return string Datafile contents.
120 | */
121 | public function getDatafile()
122 | {
123 | return $this->datafile;
124 | }
125 |
126 | /**
127 | * @return array Map of Experiment Keys to OptimizelyExperiments.
128 | */
129 | public function getExperimentsMap()
130 | {
131 | return $this->experimentsMap;
132 | }
133 |
134 | /**
135 | * @return array Map of Feature Keys to OptimizelyFeatures.
136 | */
137 | public function getFeaturesMap()
138 | {
139 | return $this->featuresMap;
140 | }
141 |
142 | /**
143 | * @return array Attributes as OptimizelyAttribute.
144 | */
145 | public function getAttributes()
146 | {
147 | return $this->attributes;
148 | }
149 |
150 | /**
151 | * @return array Audiences as OptimizelyAudience.
152 | */
153 | public function getAudiences()
154 | {
155 | return $this->audiences;
156 | }
157 |
158 | /**
159 | * @return array Events as OptimizelyEvent.
160 | */
161 | public function getEvents()
162 | {
163 | return $this->events;
164 | }
165 |
166 | /**
167 | * @return string JSON representation of the object.
168 | */
169 | #[\ReturnTypeWillChange]
170 | public function jsonSerialize()
171 | {
172 | return get_object_vars($this);
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php:
--------------------------------------------------------------------------------
1 | id = $id;
40 | $this->key = $key;
41 | $this->experimentIds = $experimentIds;
42 | }
43 |
44 | /**
45 | * @return string event ID.
46 | */
47 | public function getId()
48 | {
49 | return $this->id;
50 | }
51 |
52 | /**
53 | * @return string event Key.
54 | */
55 | public function getKey()
56 | {
57 | return $this->key;
58 | }
59 |
60 | /**
61 | * @return array experimentIds representing event experiment ids.
62 | */
63 | public function getExperimentIds()
64 | {
65 | return $this->experimentIds;
66 | }
67 |
68 | /**
69 | * @return string JSON representation of the object.
70 | */
71 | #[\ReturnTypeWillChange]
72 | public function jsonSerialize()
73 | {
74 | return get_object_vars($this);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php:
--------------------------------------------------------------------------------
1 | associative array
40 | */
41 | private $variationsMap;
42 |
43 | public function __construct($id, $key, array $variationsMap, $audiences)
44 | {
45 | $this->id = $id;
46 | $this->key = $key;
47 | $this->audiences = $audiences;
48 | $this->variationsMap = $variationsMap;
49 | }
50 |
51 | /**
52 | * @return string Experiment ID.
53 | */
54 | public function getId()
55 | {
56 | return $this->id;
57 | }
58 |
59 | /**
60 | * @return string Experiment key.
61 | */
62 | public function getKey()
63 | {
64 | return $this->key;
65 | }
66 |
67 | /**
68 | * @return string Experiment audiences.
69 | */
70 | public function getExperimentAudiences()
71 | {
72 | return $this->audiences;
73 | }
74 |
75 | /**
76 | * @return array Map of Variation Keys to OptimizelyVariations.
77 | */
78 | public function getVariationsMap()
79 | {
80 | return $this->variationsMap;
81 | }
82 |
83 | /**
84 | * @return string JSON representation of the object.
85 | */
86 | #[\ReturnTypeWillChange]
87 | public function jsonSerialize()
88 | {
89 | return get_object_vars($this);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php:
--------------------------------------------------------------------------------
1 | associative array
49 | */
50 | private $experimentsMap;
51 |
52 | /**
53 | * Map of Variable Keys to OptimizelyVariables.
54 | *
55 | * @var associative array
56 | */
57 | private $variablesMap;
58 |
59 | public function __construct($id, $key, array $experimentsMap, array $variablesMap, array $experimentRules, array $deliveryRules)
60 | {
61 | $this->id = $id;
62 | $this->key = $key;
63 | $this->experimentRules = $experimentRules;
64 | $this->deliveryRules = $deliveryRules;
65 | $this->experimentsMap = $experimentsMap;
66 | $this->variablesMap = $variablesMap;
67 | }
68 |
69 | /**
70 | * @return string Feature ID.
71 | */
72 | public function getId()
73 | {
74 | return $this->id;
75 | }
76 |
77 | /**
78 | * @return string Feature Key.
79 | */
80 | public function getKey()
81 | {
82 | return $this->key;
83 | }
84 |
85 | /**
86 | * @return array array of feature Experiments as OptimizelyExperiments.
87 | */
88 | public function getExperimentRules()
89 | {
90 | return $this->experimentRules;
91 | }
92 |
93 | /**
94 | * @return array array of Rollout Experiments of feature as OptimizelyExperiments.
95 | */
96 | public function getDeliveryRules()
97 | {
98 | return $this->deliveryRules;
99 | }
100 |
101 | /**
102 | * @return array Map of Experiment Keys to OptimizelyExperiments.
103 | */
104 | public function getExperimentsMap()
105 | {
106 | # This experimentsMap is deprecated. Use experimentRules and deliveryRules instead.
107 | return $this->experimentsMap;
108 | }
109 |
110 | /**
111 | * @return array Map of Variable Keys to OptimizelyVariables.
112 | */
113 | public function getVariablesMap()
114 | {
115 | return $this->variablesMap;
116 | }
117 |
118 | /**
119 | * @return string JSON representation of the object.
120 | */
121 | #[\ReturnTypeWillChange]
122 | public function jsonSerialize()
123 | {
124 | return get_object_vars($this);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyVariable.php:
--------------------------------------------------------------------------------
1 | id = $id;
45 | $this->key = $key;
46 | $this->type = $type;
47 | $this->value = $value;
48 | }
49 |
50 | /**
51 | * @return string Variable ID.
52 | */
53 | public function getId()
54 | {
55 | return $this->id;
56 | }
57 |
58 | /**
59 | * @return string Variable key.
60 | */
61 | public function getKey()
62 | {
63 | return $this->key;
64 | }
65 |
66 | /**
67 | * @return string Variable type.
68 | */
69 | public function getType()
70 | {
71 | return $this->type;
72 | }
73 |
74 | /**
75 | * @return string Variable value.
76 | */
77 | public function getValue()
78 | {
79 | return $this->value;
80 | }
81 |
82 | /**
83 | * @return string JSON representation of the object.
84 | */
85 | #[\ReturnTypeWillChange]
86 | public function jsonSerialize()
87 | {
88 | return get_object_vars($this);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyConfig/OptimizelyVariation.php:
--------------------------------------------------------------------------------
1 | associative array
40 | */
41 | private $variablesMap;
42 |
43 | public function __construct($id, $key, $featureEnabled, array $variablesMap)
44 | {
45 | $this->id = $id;
46 | $this->key = $key;
47 | $this->featureEnabled = $featureEnabled;
48 | $this->variablesMap = $variablesMap;
49 | }
50 |
51 | /**
52 | * @return string Variation ID.
53 | */
54 | public function getId()
55 | {
56 | return $this->id;
57 | }
58 |
59 | /**
60 | * @return string Variation key.
61 | */
62 | public function getKey()
63 | {
64 | return $this->key;
65 | }
66 |
67 | /**
68 | * @return boolean featureEnabled property
69 | */
70 | public function getFeatureEnabled()
71 | {
72 | return $this->featureEnabled;
73 | }
74 |
75 | /**
76 | * @return array Map of Variable Keys to OptimizelyVariables.
77 | */
78 | public function getVariablesMap()
79 | {
80 | return $this->variablesMap;
81 | }
82 |
83 | /**
84 | * @return string JSON representation of the object.
85 | * Unsets featureEnabled property for variations of ab experiments.
86 | */
87 | #[\ReturnTypeWillChange]
88 | public function jsonSerialize()
89 | {
90 | $props = get_object_vars($this);
91 | // featureEnabled prop is irrelevant for ab experiments.
92 | if ($this->featureEnabled === null) {
93 | unset($props['featureEnabled']);
94 | }
95 |
96 | return $props;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Optimizely/OptimizelyFactory.php:
--------------------------------------------------------------------------------
1 | _logger = $logger;
55 | $this->_errorHandler = $errorHandler;
56 | $this->_config = DatafileProjectConfig::createProjectConfigFromDatafile($datafile, $skipJsonValidation, $logger, $errorHandler);
57 | }
58 |
59 | /**
60 | * Returns instance of ProjectConfig.
61 | * @return null|DatafileProjectConfig DatafileProjectConfig instance.
62 | */
63 | public function getConfig()
64 | {
65 | return $this->_config;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Optimizely/UserProfile/Decision.php:
--------------------------------------------------------------------------------
1 | _variationId = $variationId;
35 | }
36 |
37 | public function getVariationId()
38 | {
39 | return $this->_variationId;
40 | }
41 |
42 | public function setVariationId($variationId)
43 | {
44 | $this->_variationId = $variationId;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Optimizely/UserProfile/UserProfile.php:
--------------------------------------------------------------------------------
1 | _userId = $userId;
41 | $this->_experiment_bucket_map = $experiment_bucket_map;
42 | }
43 |
44 | /**
45 | * @return string ID of the user.
46 | */
47 | public function getUserId()
48 | {
49 | return $this->_userId;
50 | }
51 |
52 | /**
53 | * @return array Experiment to decision map.
54 | */
55 | public function getExperimentBucketMap()
56 | {
57 | return $this->_experiment_bucket_map;
58 | }
59 |
60 | /**
61 | * Get the variation ID for the given experiment from the experiment bucket map.
62 | *
63 | * @param $experimentId string The ID of the experiment.
64 | *
65 | * @return null|string The variation ID.
66 | */
67 | public function getVariationForExperiment($experimentId)
68 | {
69 | $decision = $this->getDecisionForExperiment($experimentId);
70 | if (!is_null($decision)) {
71 | return $decision->getVariationId();
72 | }
73 |
74 | return null;
75 | }
76 |
77 | /**
78 | * Get the decision for the given experiment from the experiment bucket map.
79 | *
80 | * @param $experimentId string The ID of the experiment.
81 | *
82 | * @return null|Decision The decision for the given experiment.
83 | */
84 | public function getDecisionForExperiment($experimentId)
85 | {
86 | if (isset($this->_experiment_bucket_map[$experimentId])) {
87 | return $this->_experiment_bucket_map[$experimentId];
88 | }
89 |
90 | return null;
91 | }
92 |
93 | /**
94 | * Set the decision for the given experiment.
95 | *
96 | * @param $experimentId string The ID of the experiment.
97 | * @param $decision Decision The decision for the experiment.
98 | */
99 | public function saveDecisionForExperiment($experimentId, Decision $decision)
100 | {
101 | $this->_experiment_bucket_map[$experimentId] = $decision;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Optimizely/UserProfile/UserProfileServiceInterface.php:
--------------------------------------------------------------------------------
1 | $decision) {
55 | if (!is_array($decision)) {
56 | return false;
57 | }
58 |
59 | // make sure that there is a variation ID property in every decision
60 | if (!isset($decision[self::VARIATION_ID_KEY])) {
61 | return false;
62 | }
63 | }
64 |
65 | // Looks good to me
66 | return true;
67 | }
68 |
69 | /**
70 | * Convert the given user profile map into a UserProfile object.
71 | *
72 | * @param $userProfileMap array
73 | *
74 | * @return UserProfile The user profile object constructed from the given map.
75 | */
76 | public static function convertMapToUserProfile($userProfileMap)
77 | {
78 | $userId = $userProfileMap[self::USER_ID_KEY];
79 | $experimentBucketMap = array();
80 | foreach ($userProfileMap[self::EXPERIMENT_BUCKET_MAP_KEY] as $experimentId => $decisionMap) {
81 | $variationId = $decisionMap[self::VARIATION_ID_KEY];
82 | $experimentBucketMap[$experimentId] = new Decision($variationId);
83 | }
84 |
85 | return new UserProfile($userId, $experimentBucketMap);
86 | }
87 |
88 | /**
89 | * Convert the given UserProfile object into a map.
90 | *
91 | * @param $userProfile UserProfile The user profile object to convert to a map.
92 | *
93 | * @return array The map representing the user profile object.
94 | */
95 | public static function convertUserProfileToMap($userProfile)
96 | {
97 | $userProfileMap = array(
98 | self::USER_ID_KEY => $userProfile->getUserId(),
99 | self::EXPERIMENT_BUCKET_MAP_KEY => array()
100 | );
101 | $experimentBucketMap = $userProfile->getExperimentBucketMap();
102 | foreach ($experimentBucketMap as $experimentId => $decision) {
103 | $userProfileMap[self::EXPERIMENT_BUCKET_MAP_KEY][$experimentId] = array(
104 | self::VARIATION_ID_KEY => $decision->getVariationId()
105 | );
106 | }
107 | return $userProfileMap;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Optimizely/Utils/ConditionTreeEvaluator.php:
--------------------------------------------------------------------------------
1 | evaluate($condition, $leafEvaluator);
69 |
70 | if ($result === false) {
71 | return false;
72 | }
73 |
74 | if ($result === null) {
75 | $sawNullResult = true;
76 | }
77 | }
78 |
79 | return $sawNullResult ? null : true;
80 | }
81 |
82 | /**
83 | * Evaluates an array of conditions as if the evaluator had been applied
84 | * to each entry and the results OR-ed together.
85 | *
86 | * @param array $conditions Audience conditions list.
87 | * @param callable $leafEvaluator Method to evaluate leaf condition.
88 | *
89 | * @return null|boolean True if any operand evaluates to true.
90 | * False if all operands evaluate to false.
91 | * Null if conditions couldn't be evaluated.
92 | */
93 | protected function orEvaluator(array $conditions, callable $leafEvaluator)
94 | {
95 | $sawNullResult = false;
96 | foreach ($conditions as $condition) {
97 | $result = $this->evaluate($condition, $leafEvaluator);
98 |
99 | if ($result === true) {
100 | return true;
101 | }
102 |
103 | if ($result === null) {
104 | $sawNullResult = true;
105 | }
106 | }
107 |
108 | return $sawNullResult ? null : false;
109 | }
110 |
111 | /**
112 | * Evaluates an array of conditions as if the evaluator had been applied
113 | * to a single entry and NOT was applied to the result.
114 | *
115 | * @param array $conditions Audience conditions list.
116 | * @param callable $leafEvaluator Method to evaluate leaf condition.
117 | *
118 | * @return null|boolean True if the operand evaluates to false.
119 | * False if the operand evaluates to true.
120 | * Null if conditions is empty or couldn't be evaluated.
121 | */
122 | protected function notEvaluator(array $condition, callable $leafEvaluator)
123 | {
124 | if (empty($condition)) {
125 | return null;
126 | }
127 |
128 | $result = $this->evaluate($condition[0], $leafEvaluator);
129 | return $result === null ? null: !$result;
130 | }
131 |
132 | /**
133 | * Function to evaluate audience conditions against user's attributes.
134 | *
135 | * @param array $conditions Nested array of and/or/not conditions representing the audience conditions.
136 | * @param callable $leafEvaluator Method to evaluate leaf condition.
137 | *
138 | * @return null|boolean Result of evaluating the conditions using the operator rules.
139 | * and the leaf evaluator. Null if conditions couldn't be evaluated.
140 | */
141 | public function evaluate($conditions, callable $leafEvaluator)
142 | {
143 | // When parsing audiences tree the leaf node is a string representing an audience ID.
144 | // When parsing conditions of a single audience the leaf node is an associative array with all keys of type string.
145 | if (is_string($conditions) || Validator::doesArrayContainOnlyStringKeys($conditions)) {
146 | $leafCondition = $conditions;
147 | return $leafEvaluator($leafCondition);
148 | }
149 |
150 | if (in_array($conditions[0], $this->getOperators())) {
151 | $operator = array_shift($conditions);
152 | } else {
153 | $operator = self::OR_OPERATOR;
154 | }
155 |
156 | $evaluatorFunc = $this->getEvaluatorByOperatorType($operator);
157 | return $this->{$evaluatorFunc}($conditions, $leafEvaluator);
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/Optimizely/Utils/ConfigParser.php:
--------------------------------------------------------------------------------
1 | $value) {
38 | $propSetter = 'set'.ucfirst($key);
39 | if (method_exists($entityObject, $propSetter)) {
40 | $entityObject->$propSetter($value);
41 | }
42 | }
43 | }
44 |
45 | if (is_null($entityId)) {
46 | array_push($entityMap, $entityObject);
47 | } else {
48 | $propKeyGetter = 'get'.ucfirst($entityId);
49 | $entityMap[$entityObject->$propKeyGetter()] = $entityObject;
50 | }
51 | }
52 |
53 | return $entityMap;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Optimizely/Utils/Errors.php:
--------------------------------------------------------------------------------
1 | log(Logger::DEBUG, "Event tags is undefined.");
47 | return null;
48 | }
49 | if (!is_array($eventTags)) {
50 | $logger->log(Logger::DEBUG, "Event tags is not a dictionary.");
51 | return null;
52 | }
53 |
54 | if (!isset($eventTags[self::REVENUE_EVENT_METRIC_NAME])) {
55 | $logger->log(Logger::DEBUG, "The revenue key is not defined in the event tags or is null.");
56 | return null;
57 | }
58 |
59 | $rawValue = $eventTags[self::REVENUE_EVENT_METRIC_NAME];
60 |
61 | if (!is_numeric($rawValue)) {
62 | $logger->log(Logger::DEBUG, "Revenue value is not an integer or float, or is not a numeric string.");
63 | return null;
64 | }
65 |
66 | if ($rawValue != intval($rawValue)) {
67 | $logger->log(Logger::DEBUG, "Revenue value couldn't be parsed as an integer.");
68 | return null;
69 | }
70 |
71 | $rawValue = intval($rawValue);
72 | $logger->log(Logger::INFO, "The revenue value {$rawValue} will be sent to results.");
73 | return $rawValue;
74 | }
75 |
76 | /**
77 | * Grab the numeric event value from the event tags. "value" is a reserved keyword.
78 | * The value of 'value' can be a float or a numeric string
79 | *
80 | * @param $eventTags array Representing metadata associated with the event.
81 | * @param $logger instance of LoggerInterface
82 | *
83 | * @return float value of 'value' or null
84 | */
85 | public static function getNumericValue($eventTags, $logger)
86 | {
87 | if (!$eventTags) {
88 | $logger->log(Logger::DEBUG, "Event tags is undefined.");
89 | return null;
90 | }
91 |
92 | if (!is_array($eventTags)) {
93 | $logger->log(Logger::DEBUG, "Event tags is not a dictionary.");
94 | return null;
95 | }
96 |
97 | if (!isset($eventTags[self::NUMERIC_EVENT_METRIC_NAME])) {
98 | $logger->log(Logger::DEBUG, "The numeric metric key is not defined in the event tags or is null.");
99 | return null;
100 | }
101 |
102 | if (!is_numeric($eventTags[self::NUMERIC_EVENT_METRIC_NAME])) {
103 | $logger->log(Logger::DEBUG, "Numeric metric value is not an integer or float, or is not a numeric string.");
104 | return null;
105 | }
106 |
107 | if (is_nan($eventTags[self::NUMERIC_EVENT_METRIC_NAME]) || is_infinite(floatval($eventTags[self::NUMERIC_EVENT_METRIC_NAME]))) {
108 | $logger->log(Logger::DEBUG, "Provided numeric value is in an invalid format.");
109 | return null;
110 | }
111 |
112 | $rawValue = $eventTags[self::NUMERIC_EVENT_METRIC_NAME];
113 | // # Log the final numeric metric value
114 | $logger->log(Logger::INFO, "The numeric metric value {$rawValue} will be sent to results.");
115 |
116 | return floatval($rawValue);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Optimizely/Utils/GeneratorUtils.php:
--------------------------------------------------------------------------------
1 | log(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$variableType}'.");
59 | }
60 |
61 | return $return_value;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/ErrorHandlerTests/DefaultErrorHandlerTest.php:
--------------------------------------------------------------------------------
1 | expectExceptionMessage("Throw me please.");
28 | $this->expectException(Exception::class);
29 | $exception = new Exception('Throw me please.');
30 | $errorHandler = new DefaultErrorHandler();
31 |
32 | $errorHandler->handleError($exception);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/ErrorHandlerTests/NoOpErrorHandlerTest.php:
--------------------------------------------------------------------------------
1 | handleError($exception);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/EventTests/DefaultEventDispatcherTest.php:
--------------------------------------------------------------------------------
1 | '1234',
32 | 'projectId' => '9876',
33 | 'visitorId' => 'testUser'
34 | ],
35 | 'POST',
36 | [
37 | 'Content-Type' => 'application/json'
38 | ]
39 | );
40 |
41 | $expectedOptions = [
42 | 'headers' => $logEvent->getHeaders(),
43 | 'json' => $logEvent->getParams(),
44 | 'timeout' => 10,
45 | 'connect_timeout' => 10
46 | ];
47 |
48 | $guzzleClientMock = $this->getMockBuilder(HttpClient::class)
49 | ->getMock();
50 |
51 | $guzzleClientMock->expects($this->once())
52 | ->method('request')
53 | ->with($logEvent->getHttpVerb(), $logEvent->getUrl(), $expectedOptions);
54 |
55 | $eventDispatcher = new DefaultEventDispatcher($guzzleClientMock);
56 | $eventDispatcher->dispatchEvent($logEvent);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/EventTests/LogEventTest.php:
--------------------------------------------------------------------------------
1 | logEvent = new LogEvent(
30 | 'https://logx.optimizely.com',
31 | [
32 | 'accountId' => '1234',
33 | 'projectId' => '9876',
34 | 'visitorId' => 'testUser'
35 | ],
36 | 'POST',
37 | [
38 | 'Content-Type' => 'application/json'
39 | ]
40 | );
41 | }
42 |
43 | public function testGetUrl()
44 | {
45 | $this->assertEquals('https://logx.optimizely.com', $this->logEvent->getUrl());
46 | }
47 |
48 | public function testGetParams()
49 | {
50 | $this->assertEquals(
51 | [
52 | 'accountId' => '1234',
53 | 'projectId' => '9876',
54 | 'visitorId' => 'testUser'
55 | ],
56 | $this->logEvent->getParams()
57 | );
58 | }
59 |
60 | public function testGetHttpVerb()
61 | {
62 | $this->assertEquals('POST', $this->logEvent->getHttpVerb());
63 | }
64 |
65 | public function testGetHeaders()
66 | {
67 | $this->assertEquals(['Content-Type' => 'application/json'], $this->logEvent->getHeaders());
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/LoggerTests/DefaultLoggerTest.php:
--------------------------------------------------------------------------------
1 | log(Logger::INFO, 'Log me please.');
29 |
30 | $this->expectOutputRegex('/Log me please./');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/LoggerTests/NoOpLoggerTest.php:
--------------------------------------------------------------------------------
1 | log(Logger::INFO, 'I wont be logged.');
29 |
30 | $this->expectOutputString('');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/OptimizelyFactoryTest.php:
--------------------------------------------------------------------------------
1 | datafile = DATAFILE;
34 | $this->typedAudiencesDataFile = DATAFILE_WITH_TYPED_AUDIENCES;
35 | }
36 |
37 | public function testDefaultInstance()
38 | {
39 | $optimizelyClient = OptimizelyFactory::createDefaultInstance("some-sdk-key", $this->datafile);
40 |
41 | // client hasn't been mocked yet. Hence, config manager should return config of hardcoded
42 | // datafile.
43 | $this->assertEquals('15', $optimizelyClient->configManager->getConfig()->getRevision());
44 |
45 | // Mock http client to return a valid datafile
46 | $mock = new MockHandler([
47 | new Response(200, [], $this->typedAudiencesDataFile)
48 | ]);
49 |
50 | $handler = HandlerStack::create($mock);
51 |
52 | $client = new Client(['handler' => $handler]);
53 | $httpClient = new \ReflectionProperty(HTTPProjectConfigManager::class, 'httpClient');
54 | $httpClient->setAccessible(true);
55 | $httpClient->setValue($optimizelyClient->configManager, $client);
56 |
57 | /// Fetch datafile
58 | $optimizelyClient->configManager->fetch();
59 |
60 | $this->assertEquals('3', $optimizelyClient->configManager->getConfig()->getRevision());
61 | }
62 |
63 | public function testDefaultInstanceWithAccessToken()
64 | {
65 | $optimizelyClient = OptimizelyFactory::createDefaultInstance(
66 | "some-sdk-key",
67 | null,
68 | "some_token"
69 | );
70 |
71 | // Mock http client to return a valid datafile
72 | $mock = new MockHandler([
73 | new Response(200, [], $this->typedAudiencesDataFile)
74 | ]);
75 |
76 | $container = [];
77 | $history = Middleware::history($container);
78 | $handler = HandlerStack::create($mock);
79 | $handler->push($history);
80 |
81 | $client = new Client(['handler' => $handler]);
82 | $httpClient = new \ReflectionProperty(HTTPProjectConfigManager::class, 'httpClient');
83 | $httpClient->setAccessible(true);
84 | $httpClient->setValue($optimizelyClient->configManager, $client);
85 |
86 | /// Fetch datafile
87 | $optimizelyClient->configManager->fetch();
88 |
89 | $this->assertEquals('3', $optimizelyClient->configManager->getConfig()->getRevision());
90 |
91 | // assert that https call is made to mock as expected.
92 | $transaction = $container[0];
93 | $this->assertEquals(
94 | 'https://config.optimizely.com/datafiles/auth/some-sdk-key.json',
95 | $transaction['request']->getUri()
96 | );
97 |
98 | // assert that headers include authorization access token
99 | $this->assertEquals(
100 | 'Bearer some_token',
101 | $transaction['request']->getHeaders()['Authorization'][0]
102 | );
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tests/ProjectConfigManagerTests/StaticProjectConfigManagerTest.php:
--------------------------------------------------------------------------------
1 | loggerMock = $this->getMockBuilder(NoOpLogger::class)
37 | ->setMethods(array('log'))
38 | ->getMock();
39 |
40 | $this->errorHandlerMock = $this->getMockBuilder(NoOpErrorHandler::class)
41 | ->setMethods(array('handleError'))
42 | ->getMock();
43 | }
44 |
45 | public function testStaticProjectConfigManagerWithInvalidDatafile()
46 | {
47 | $this->loggerMock->expects($this->once())
48 | ->method('log')
49 | ->with(Logger::ERROR, 'Provided "datafile" has invalid schema.');
50 |
51 | $configManager = new StaticProjectConfigManager('Invalid datafile', false, $this->loggerMock, $this->errorHandlerMock);
52 | $this->assertNull($configManager->getConfig());
53 | }
54 |
55 | public function testStaticProjectConfigManagerWithUnsupportedVersionDatafile()
56 | {
57 | $this->loggerMock->expects($this->once())
58 | ->method('log')
59 | ->with(Logger::ERROR, 'This version of the PHP SDK does not support the given datafile version: 5.');
60 |
61 | $this->errorHandlerMock->expects($this->once())
62 | ->method('handleError')
63 | ->with(new InvalidDatafileVersionException('This version of the PHP SDK does not support the given datafile version: 5.'));
64 |
65 | $configManager = new StaticProjectConfigManager(UNSUPPORTED_DATAFILE, false, $this->loggerMock, $this->errorHandlerMock);
66 | $this->assertNull($configManager->getConfig());
67 | }
68 |
69 | public function testStaticProjectConfigManagerWithValidDatafile()
70 | {
71 | $config = new DatafileProjectConfig(DATAFILE, $this->loggerMock, $this->errorHandlerMock);
72 | $configManager = new StaticProjectConfigManager(DATAFILE, false, $this->loggerMock, $this->errorHandlerMock);
73 | $this->assertEquals($config, $configManager->getConfig());
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tests/UserProfileTests/UserProfileTest.php:
--------------------------------------------------------------------------------
1 | userProfile = new UserProfile(
33 | 'user_1',
34 | $experimentBucketMap
35 | );
36 | }
37 |
38 | public function testGetVariationForExperiment()
39 | {
40 | $this->assertEquals('211111', $this->userProfile->getVariationForExperiment('111111'));
41 | }
42 |
43 | public function testGetDecisionForExperiment()
44 | {
45 | $expectedDecision = new Decision('211111');
46 | $this->assertEquals($expectedDecision, $this->userProfile->getDecisionForExperiment('111111'));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/UserProfileTests/UserProfileUtilsTest.php:
--------------------------------------------------------------------------------
1 | userProfileMap = array(
33 | 'user_id' => 'test_user',
34 | 'experiment_bucket_map' => array(
35 | '111111' => array(
36 | 'variation_id' => '211111'
37 | ),
38 | '111112' => array(
39 | 'variation_id' => '211113'
40 | )
41 | )
42 | );
43 | }
44 |
45 | public function testIsValidUserProfileMap()
46 | {
47 | $profileNotArray = 'string';
48 | $this->assertFalse(UserProfileUtils::isValidUserProfileMap($profileNotArray));
49 |
50 | $profileWithMissingId = array(
51 | 'experiment_bucket_map' => array()
52 | );
53 | $this->assertFalse(UserProfileUtils::isValidUserProfileMap($profileWithMissingId));
54 |
55 | $profileWithMissingExperimentBucketMap = array(
56 | 'user_id' => null,
57 | );
58 | $this->assertFalse(UserProfileUtils::isValidUserProfileMap($profileWithMissingExperimentBucketMap));
59 |
60 | $profileWithBadExperimentBucketMap = array(
61 | 'user_id' => null,
62 | 'experiment_bucket_map' => array(
63 | '111111' => array(
64 | 'not_expected_key' => '211111'
65 | )
66 | )
67 | );
68 | $this->assertFalse(UserProfileUtils::isValidUserProfileMap($profileWithBadExperimentBucketMap));
69 |
70 | $validUserProfileMap = $this->userProfileMap;
71 | $this->assertTrue(UserProfileUtils::isValidUserProfileMap($validUserProfileMap));
72 | }
73 |
74 | public function testConvertMapToUserProfile()
75 | {
76 | $expectedUserProfile = new UserProfile(
77 | 'test_user',
78 | array(
79 | '111111' => new Decision('211111'),
80 | '111112' => new Decision('211113')
81 | )
82 | );
83 |
84 | $this->assertEquals($expectedUserProfile, UserProfileUtils::convertMapToUserProfile($this->userProfileMap));
85 | }
86 |
87 | public function testConvertUserProfileToMap()
88 | {
89 | $userProfileObject = new UserProfile(
90 | 'test_user',
91 | array(
92 | '111111' => new Decision('211111'),
93 | '111112' => new Decision('211113')
94 | )
95 | );
96 |
97 | $expectedUserProfileMap = array(
98 | 'user_id' => 'test_user',
99 | 'experiment_bucket_map' => array(
100 | '111111' => array('variation_id' => '211111'),
101 | '111112' => array('variation_id' => '211113')
102 | )
103 | );
104 |
105 | $this->assertEquals($expectedUserProfileMap, UserProfileUtils::convertUserProfileToMap($userProfileObject));
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/UtilsTests/ValidatorLoggingTest.php:
--------------------------------------------------------------------------------
1 | loggerMock = $this->getMockBuilder(NoOpLogger::class)
32 | ->setMethods(array('log'))
33 | ->getMock();
34 |
35 | $this->config = new DatafileProjectConfig(DATAFILE, new NoOpLogger(), new NoOpErrorHandler());
36 |
37 | $this->typedConfig = new DatafileProjectConfig(DATAFILE_WITH_TYPED_AUDIENCES, new NoOpLogger(), new NoOpErrorHandler());
38 |
39 | $this->collectedLogs = [];
40 |
41 | $this->collectLogsForAssertion = function ($a, $b) {
42 | $this->collectedLogs[] = array($a,$b);
43 | };
44 | }
45 |
46 | public function testdoesUserMeetAudienceConditionsWithNoAudience()
47 | {
48 | $experiment = $this->config->getExperimentFromKey('test_experiment');
49 | $experiment->setAudienceIds([]);
50 | $experiment->setAudienceConditions([]);
51 |
52 | $this->loggerMock->expects($this->at(0))
53 | ->method('log')
54 | ->with(Logger::DEBUG, "Evaluating audiences for experiment \"test_experiment\": [].");
55 |
56 | $evalCombinedAudienceMessage = "Audiences for experiment \"test_experiment\" collectively evaluated to TRUE.";
57 | $this->loggerMock->expects($this->at(1))
58 | ->method('log')
59 | ->with(Logger::INFO, $evalCombinedAudienceMessage);
60 |
61 | list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($this->config, $experiment, [], $this->loggerMock);
62 | $this->assertTrue($evalResult);
63 | $this->assertContains($evalCombinedAudienceMessage, $reasons);
64 | $this->assertCount(1, $reasons);
65 | }
66 |
67 | public function testdoesUserMeetAudienceConditionsEvaluatesAudienceIds()
68 | {
69 | $userAttributes = [
70 | "test_attribute" => "test_value_1"
71 | ];
72 |
73 | $experiment = $this->config->getExperimentFromKey('test_experiment');
74 | $experiment->setAudienceIds(['11155']);
75 | $experiment->setAudienceConditions(null);
76 |
77 | $this->loggerMock->expects($this->any())
78 | ->method('log')
79 | ->will($this->returnCallback($this->collectLogsForAssertion));
80 |
81 | Validator::doesUserMeetAudienceConditions($this->config, $experiment, $userAttributes, $this->loggerMock);
82 |
83 | $this->assertContains([Logger::DEBUG, "Evaluating audiences for experiment \"test_experiment\": [\"11155\"]."], $this->collectedLogs);
84 | $this->assertContains(
85 | [Logger::DEBUG, "Starting to evaluate audience \"11155\" with conditions: [\"and\",[\"or\",[\"or\",{\"name\":\"browser_type\",\"type\":\"custom_attribute\",\"value\":\"chrome\"}]]]."],
86 | $this->collectedLogs
87 | );
88 | $this->assertContains([Logger::DEBUG, "Audience \"11155\" evaluated to UNKNOWN."], $this->collectedLogs);
89 | $this->assertContains([Logger::INFO, "Audiences for experiment \"test_experiment\" collectively evaluated to FALSE."], $this->collectedLogs);
90 | }
91 |
92 | public function testIsUserInExperimenEvaluatesAudienceConditions()
93 | {
94 | $experiment = $this->typedConfig->getExperimentFromKey('audience_combinations_experiment');
95 | $experiment->setAudienceIds([]);
96 | $experiment->setAudienceConditions(['or', ['or', '3468206642', '3988293898']]);
97 |
98 | $this->loggerMock->expects($this->any())
99 | ->method('log')
100 | ->will($this->returnCallback($this->collectLogsForAssertion));
101 |
102 | list($result, $reasons) = Validator::doesUserMeetAudienceConditions($this->typedConfig, $experiment, ["house" => "I am in Slytherin"], $this->loggerMock);
103 |
104 | $this->assertContains(
105 | [Logger::DEBUG, "Evaluating audiences for experiment \"audience_combinations_experiment\": [\"or\",[\"or\",\"3468206642\",\"3988293898\"]]."],
106 | $this->collectedLogs
107 | );
108 | $this->assertContains(
109 | [Logger::DEBUG, "Starting to evaluate audience \"3468206642\" with conditions: [\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Gryffindor\"}]]]."],
110 | $this->collectedLogs
111 | );
112 | $this->assertContains(
113 | [Logger::DEBUG, "Audience \"3468206642\" evaluated to FALSE."],
114 | $this->collectedLogs
115 | );
116 | $this->assertContains(
117 | [Logger::DEBUG, "Starting to evaluate audience \"3988293898\" with conditions: [\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"substring\",\"value\":\"Slytherin\"}]]]."],
118 | $this->collectedLogs
119 | );
120 | $this->assertContains(
121 | [Logger::DEBUG, "Audience \"3988293898\" evaluated to TRUE."],
122 | $this->collectedLogs
123 | );
124 |
125 | $evalCombinedAudienceMessage = "Audiences for experiment \"audience_combinations_experiment\" collectively evaluated to TRUE.";
126 |
127 | $this->assertContains([Logger::INFO, $evalCombinedAudienceMessage], $this->collectedLogs);
128 | $this->assertContains($evalCombinedAudienceMessage, $reasons);
129 | $this->assertCount(1, $reasons);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/UtilsTests/VariableTypeUtilsTest.php:
--------------------------------------------------------------------------------
1 | loggerMock = $this->getMockBuilder(NoOpLogger::class)
35 | ->setMethods(array('log'))
36 | ->getMock();
37 |
38 | $this->variableUtilObj = new VariableTypeUtils();
39 | }
40 |
41 | public function testValueCastingToBoolean()
42 | {
43 | $this->assertTrue($this->variableUtilObj->castStringToType('true', 'boolean'));
44 | $this->assertTrue($this->variableUtilObj->castStringToType('True', 'boolean'));
45 | $this->assertFalse($this->variableUtilObj->castStringToType('false', 'boolean'));
46 | $this->assertFalse($this->variableUtilObj->castStringToType('somestring', 'boolean'));
47 | }
48 |
49 | public function testValueCastingToInteger()
50 | {
51 | $this->assertSame(1000, $this->variableUtilObj->castStringToType('1000', 'integer'));
52 | $this->assertSame(123, $this->variableUtilObj->castStringToType('123', 'integer'));
53 |
54 | // should return nulll and log a message if value can not be casted to an integer
55 | $value = '123.5'; // any string with non-decimal digits
56 | $type = 'integer';
57 | $this->loggerMock->expects($this->exactly(1))
58 | ->method('log')
59 | ->with(
60 | Logger::ERROR,
61 | "Unable to cast variable value '{$value}' to type '{$type}'."
62 | );
63 |
64 | $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock));
65 | }
66 |
67 | public function testValueCastingToDouble()
68 | {
69 | $this->assertSame(1000.0, $this->variableUtilObj->castStringToType('1000', 'double'));
70 | $this->assertSame(3.0, $this->variableUtilObj->castStringToType('3.0', 'double'));
71 | $this->assertSame(13.37, $this->variableUtilObj->castStringToType('13.37', 'double'));
72 |
73 | // should return nil and log a message if value can not be casted to a double
74 | $value = 'any-non-numeric-string';
75 | $type = 'double';
76 | $this->loggerMock->expects($this->exactly(1))
77 | ->method('log')
78 | ->with(
79 | Logger::ERROR,
80 | "Unable to cast variable value '{$value}' to type '{$type}'."
81 | );
82 |
83 | $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock));
84 | }
85 |
86 | public function testValueCastingToString()
87 | {
88 | $this->assertSame('13.37', $this->variableUtilObj->castStringToType('13.37', 'string'));
89 | $this->assertSame('a string', $this->variableUtilObj->castStringToType('a string', 'string'));
90 | $this->assertSame('3', $this->variableUtilObj->castStringToType('3', 'string'));
91 | $this->assertSame('false', $this->variableUtilObj->castStringToType('false', 'string'));
92 | }
93 | }
94 |
--------------------------------------------------------------------------------