diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..a0383db
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+/tests export-ignore
+/workbench export-ignore
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2035aaf..3109d18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,8 @@
-/vendor/
+vendor
+# Before you hate on me for this https://stackoverflow.com/a/21589454
composer.lock
-package-lock.json
-/node_modules/
\ No newline at end of file
+node_modules
+output
+.env
+.phpunit.cache
+package-lock.json
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
index c430246..16c17d2 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,6 +1,17 @@
{
- "trailingCommaPHP": true,
- "singleQuote": true,
- "useTabs": false,
- "braceStyle": "psr-2"
+ "tabWidth": 4,
+ "singleQuote": true,
+ "useTabs": false,
+ "plugins": ["@prettier/plugin-php"],
+ "overrides": [
+ {
+ "files": ["*.php"],
+ "options": {
+ "parser": "php",
+ "phpVersion": "8.2",
+ "trailingCommaPHP": true,
+ "braceStyle": "psr-2"
+ }
+ }
+ ]
}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..9b24c3a
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Listen for XDebug",
+ "type": "php",
+ "request": "launch",
+ "port": 9001
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 56acef9..8f77bbd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,12 +1,25 @@
{
- // I recommend to disable VS Code's built-in PHP IntelliSense by setting php.suggest.basic to false to avoid duplicate suggestions.
- "php.suggest.basic": false,
- "editor.formatOnSave": true,
- "[php]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
- },
- "workbench.colorCustomizations": {},
- "taskExplorer.pathToMake": "make",
- "coverage-gutters.showLineCoverage": true,
- "coverage-gutters.showRulerCoverage": true,
+ // I recommend to disable VS Code's built-in PHP IntelliSense by setting php.suggest.basic to false to avoid duplicate suggestions.
+ "php.suggest.basic": false,
+ "editor.formatOnSave": true,
+ "[php]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "workbench.colorCustomizations": {},
+ "psalm.configPaths": ["psalm.xml"],
+ "psalm.psalmScriptPath": "vendor/bin/psalm-language-server",
+ "phpCodeSniffer.exec.linux": "vendor/bin/phpcs",
+ "phpCodeSniffer.standard": "Custom",
+ "phpCodeSniffer.standardCustom": "phpcs.xml",
+ "psalm.psalmScriptArgs": [
+ "--on-change-debounce-ms=500",
+ "--show-diagnostic-warnings=false"
+ ],
+ "editor.rulers": [80],
+ "intelephense.environment.phpVersion": "8.2.0",
+ "coverage-gutters.showLineCoverage": true,
+ "coverage-gutters.showRulerCoverage": true,
+ "coverage-gutters.coverageBaseDir": "output/**",
+ "coverage-gutters.remotePathResolve": ["./"],
+ "prettier.documentSelectors": ["**/*.php", "**/*.blade.php"]
}
diff --git a/README.md b/README.md
index a01757d..9daca06 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,15 @@ Laravel-flagsmith was created by, and is maintained by **[Andrew Nagy](https://g
## Features
-- Provides a trait to be able to get features based on Laravel Users ([Flagsmith Identities](https://docs.flagsmith.com/basic-features/managing-identities))
-- Utilizes [Laravel's Queue](https://laravel.com/docs/8.x/queues) system to update features in the background
-- Utilizes [Laravel's Cache](https://laravel.com/docs/8.x/cache) system to store features in a cache for quick access
-- Utilizes [Laravel's Task Scheduling](https://laravel.com/docs/8.x/scheduling) system to update features on a schedule
-- Adds a route to utilize [Flagsmith's webhooks](https://docs.flagsmith.com/advanced-use/system-administration) to update the cache when features change
+- Provides a trait to be able to get flags based on Laravel Users ([Flagsmith Identities](https://docs.flagsmith.com/basic-features/managing-identities))
+- Utilizes [Laravel's Queue](https://laravel.com/docs/8.x/queues) system to update flags in the background
+- Utilizes [Laravel's Cache](https://laravel.com/docs/8.x/cache) system to store flags in a cache for quick access
+- Utilizes [Laravel's Task Scheduling](https://laravel.com/docs/8.x/scheduling) system to update flags on a schedule
+- Adds a route to utilize [Flagsmith's webhooks](https://docs.flagsmith.com/advanced-use/system-administration) to update the cache when flags change
## Installation & Usage
-> **Requires [PHP 7.4+](https://php.net/releases/)**
+> **Requires [PHP 8.2+](https://php.net/releases/)**
Require Laravel-flagsmith using [Composer](https://getcomposer.org):
@@ -32,46 +32,48 @@ composer require clearlyip/laravel-flagsmith
| :------ | :---------------- |
| 8.x | 1.x |
| 9.x | 2.x |
-| 10.x | 2.1.x |
+| 10.x | 3.x |
## Usage
### Configuration Files
-- Publish the Laravel Flagsmith configuration file using the `vendor:publish` Artisan command. The `flagsmith` configuration file will be placed in your `config` directory (Use `--force` to overwrite your existing `clearly` config file):
- - `php artisan vendor:publish --tag="flagsmith" [--force]`
+- Publish the Laravel Flagsmith configuration file using the `vendor:publish` Artisan command. The `flagsmith` configuration file will be placed in your `config` directory (Use `--force` to overwrite your existing `clearly` config file):
+ - `php artisan vendor:publish --tag="flagsmith" [--force]`
All options are fully documented in the configuration file
### User
-It's advised to add the trait `Clearlyip\LaravelFlagsmith\Concerns\HasFeatures` to your user model. This will give you the ability to access features directly from your user object.
+It's advised to add the interface `Clearlyip\LaravelFlagsmith\Contracts\UserFlags` to your user model. This will give you the ability to access flags directly from your user object.
-During inital login user features are synced through a queue which keeps them as up to date as possible
+You can add the following trait `Clearlyip\LaravelFlagsmith\Concerns\HasFlagss` to your user model to fulfill the requirements of `UserFlags`
-#### List All Features for a User
+During initial login user flags are synced through a queue which keeps them as up to date as possible
+
+#### Get All Flags for a User
```php
$user = Auth::user();
-$features = $user->getFeatures();
+$flags = $user->getFlags();
```
-### Check if feature is enabled for a user
+### Check if flag is enabled for a user
-An optional second parameter can be added as the default if the feature does not exist
+An optional second parameter can be added as the default if the flag does not exist
```php
$user = Auth::user();
-$features = $user->isFeatureEnabled('foo');
+$flags = $user->isFlagEnabled('foo');
```
-#### Get a Features value for a User
+#### Get a Flag value for a User
-An optional second parameter can be added as the default if the feature does not exist
+An optional second parameter can be added as the default if the flag does not exist
```php
$user = Auth::user();
-$features = $user->getFeatureValue('foo');
+$vakue = $user->getFlagValue('foo');
```
### Accessing
diff --git a/composer.json b/composer.json
index e2d7407..0ef26b5 100644
--- a/composer.json
+++ b/composer.json
@@ -10,15 +10,18 @@
}
],
"require": {
- "php": ">=7.4",
- "laravel/framework": ">=8.0",
- "flagsmith/flagsmith-php-client": "^2.0"
+ "php": "^8.2",
+ "laravel/framework": "^10.44.0 || ^11.0",
+ "flagsmith/flagsmith-php-client": "^4.2.0"
},
"extra": {
"laravel": {
"providers": [
"Clearlyip\\LaravelFlagsmith\\ServiceProvider"
- ]
+ ],
+ "aliases": {
+ "Flag": "Clearlyip\\LaravelFlagsmith\\Facades\\Flag"
+ }
}
},
"autoload": {
@@ -27,7 +30,43 @@
}
},
"require-dev": {
- "guzzlehttp/psr7": "^2.1",
- "guzzlehttp/guzzle": "^7.4"
+ "guzzlehttp/psr7": "^2.6.2",
+ "guzzlehttp/guzzle": "^7.8.1",
+ "orchestra/testbench": "^8.22.0",
+ "phpunit/phpunit": "^10.5.13",
+ "vimeo/psalm": "^5.23.1",
+ "squizlabs/php_codesniffer": "^3.9.0"
+ },
+ "config": {
+ "allow-plugins": {
+ "php-http/discovery": true
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "CIP\\Tests\\": "tests/",
+ "Workbench\\App\\": "workbench/app/",
+ "Workbench\\Database\\Factories\\": "workbench/database/factories/",
+ "Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
+ }
+ },
+ "scripts": {
+ "post-autoload-dump": [
+ "@clear",
+ "@prepare"
+ ],
+ "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
+ "prepare": "@php vendor/bin/testbench package:discover --ansi",
+ "build": "@php vendor/bin/testbench workbench:build --ansi",
+ "serve": [
+ "Composer\\Config::disableProcessTimeout",
+ "@build",
+ "@php vendor/bin/testbench serve"
+ ],
+ "test": "XDEBUG_MODE=coverage phpunit --configuration phpunit.xml",
+ "test:filter": "XDEBUG_MODE=coverage,debug phpunit --configuration phpunit.xml --filter",
+ "psalm": "psalm",
+ "phpcs": "phpcs --standard=phpcs.xml",
+ "phpcbf": "phpcbf --standard=phpcs.xml"
}
}
diff --git a/package.json b/package.json
index 3d2149e..5de390c 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,6 @@
{
- "name": "@clearlyip/laravel-flagsmith",
- "version": "1.0.0",
- "main": "index.js",
- "directories": {
- "test": "tests"
- },
- "private": true,
- "repository": {
- "type": "git",
- "url": "ssh://git@github.com:clearlyip/laravel-flagsmith.git"
- },
- "author": "",
- "license": "BSD 3-Clause",
- "dependencies": {
- "@prettier/plugin-php": "^0.14.3",
- "prettier": "^2.1.2"
+ "devDependencies": {
+ "@prettier/plugin-php": "^0.21.0",
+ "prettier": "^3.1.0"
}
}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..8d89144
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,53 @@
+
+
+
+ ClearlyIP coding standards
+ ./src
+ */data/*
+ */node_modules/*
+ */vendor/*
+ */tests/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..50f1050
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..17b2b2a
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Concerns/HasFeatures.php b/src/Concerns/HasFeatures.php
deleted file mode 100644
index 8ff1f6c..0000000
--- a/src/Concerns/HasFeatures.php
+++ /dev/null
@@ -1,140 +0,0 @@
-flagsmith)) {
- $this->flagsmith = App::make(Flagsmith::class);
- }
- return $this->flagsmith;
- }
-
- /**
- * Check if features are in cache for this User
- *
- * @return boolean
- */
- public function featuresInCache(): bool
- {
- return $this->getFlagsmith()->hasIdentityInCache(
- $this->getFeatureIdentity()
- );
- }
-
- /**
- * Skip Cache
- *
- * @param boolean $disable
- * @return self
- */
- public function skipFeatureCache(bool $disable = true): self
- {
- //Since Flagsmith is immutable, we need to replace the instance
- $this->flagsmith = $this->getFlagsmith()->withSkipCache($disable);
- return $this;
- }
-
- /**
- * Check if Feature is Enabled against this user
- *
- * @param string $name
- * @param boolean $default
- * @return boolean
- */
- public function isFeatureEnabled(string $name, bool $default = false): bool
- {
- return $this->getFlagsmith()->isFeatureEnabledByIdentity(
- $this->getFeatureIdentity(),
- $name,
- $default
- );
- }
-
- /**
- * Get feature value against this user
- *
- * @param string $name
- * @param mixed $default
- * @return mixed
- */
- public function getFeatureValue(string $name, $default = null)
- {
- return $this->getFlagsmith()->getFeatureValueByIdentity(
- $this->getFeatureIdentity(),
- $name,
- $default
- );
- }
-
- /**
- * Get All Features (Flags) for this user
- *
- * @return array
- */
- public function getFeatures(): array
- {
- return $this->getFlagsmith()->getFlagsByIdentity(
- $this->getFeatureIdentity()
- );
- }
-
- /**
- * Get the Identity used for the Flagsmith API
- *
- * @return string
- */
- public function getFeatureIdentityId(): string
- {
- $key = config('flagsmith.identity.identifier');
- return (string) $this->{$key};
- }
-
- /**
- * Get the Traits to send to the API for this Identity
- *
- * @return array
- */
- public function getFeatureTraits(): array
- {
- return array_reduce(
- config('flagsmith.identity.traits'),
- function ($carry, $attribute) {
- $carry[$attribute] = $this->{$attribute};
- return $carry;
- },
- []
- );
- }
-
- /**
- * Get Identity Class
- *
- * @return Identity
- */
- public function getFeatureIdentity(): Identity
- {
- $identity = new Identity($this->getFeatureIdentityId());
- foreach ($this->getFeatureTraits() as $key => $value) {
- $identity = $identity->withTrait(
- (new IdentityTrait($key))->withValue($value)
- );
- }
-
- return $identity;
- }
-}
diff --git a/src/Concerns/HasFlags.php b/src/Concerns/HasFlags.php
new file mode 100644
index 0000000..dfadad6
--- /dev/null
+++ b/src/Concerns/HasFlags.php
@@ -0,0 +1,136 @@
+flagsmith)) {
+ $this->flagsmith = App::make(Flagsmith::class);
+ }
+ return $this->flagsmith;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFlags(): FlagModelsList
+ {
+ $identity = $this->getFlagIdentity();
+ $finalizedTraits = null;
+ $traits = $identity->getTraits();
+ if ($traits !== null) {
+ $finalizedTraits = [];
+ foreach ($traits as $trait) {
+ $finalizedTraits[$trait->getKey()] = $trait->getValue();
+ }
+ }
+
+ return $this->getFlagsmith()
+ ->getIdentityFlags(
+ $identity->getId(),
+ $finalizedTraits !== null ? (object) $finalizedTraits : null,
+ )
+ ->getFlags();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function skipFlagCache(bool $disable = true): static
+ {
+ //Since Flagsmith is immutable, we need to replace the instance
+ $this->flagsmith = $this->getFlagsmith()->withSkipCache($disable);
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isFlagEnabled(string $name, bool $default = false): bool
+ {
+ $flags = $this->getFlags();
+ if (!isset($flags[$name])) {
+ return $default;
+ }
+ $flag = $flags[$name];
+ if (!($flag instanceof \Flagsmith\Models\Flag)) {
+ return $default;
+ }
+ return $flag->getEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFlagValue(string $name, $default = null): mixed
+ {
+ $flags = $this->getFlags();
+ if (!isset($flags[$name])) {
+ return $default;
+ }
+ $flag = $flags[$name];
+ if (!($flag instanceof \Flagsmith\Models\Flag)) {
+ return $default;
+ }
+ return $flag->getValue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFlagIdentityId(): ?string
+ {
+ $key = config('flagsmith.identity.identifier');
+ if ($key === null) {
+ return null;
+ }
+ return $this->getRawOriginal($key);
+ }
+
+ /**
+ * Get the Traits to send to the API for this Identity
+ *
+ * @return array
+ */
+ public function getFlagTraits(): array
+ {
+ return array_reduce(
+ config('flagsmith.identity.traits', []),
+ function ($carry, $attribute) {
+ $carry[$attribute] = $this->getRawOriginal($attribute);
+ return $carry;
+ },
+ [],
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFlagIdentity(): Identity
+ {
+ $identity = new Identity($this->getFlagIdentityId());
+ foreach ($this->getFlagTraits() as $key => $value) {
+ $identity = $identity->withTrait(
+ (new IdentityTrait($key))->withValue($value),
+ );
+ }
+
+ return $identity;
+ }
+}
diff --git a/src/Contracts/UserFlags.php b/src/Contracts/UserFlags.php
new file mode 100644
index 0000000..193ecd8
--- /dev/null
+++ b/src/Contracts/UserFlags.php
@@ -0,0 +1,71 @@
+input('event_type');
if ($event_type !== 'FLAG_UPDATED') {
@@ -27,95 +35,91 @@ public function feature(Request $request)
$state = $request->input('data.new_state');
$identityId = $request->input('data.new_state.identity_identifier');
$cache = $flagsmith->getCache();
+ if ($cache === null) {
+ return response('');
+ }
//This is specifically an identity flag change
if (!empty($identityId)) {
- $identity = $cache->get("identity.{$identityId}");
- if (!is_null($identity)) {
- //TODO: This does not seem the best way to do this
- $new = true;
- foreach ($identity['flags'] as &$flag) {
- if ($flag['feature']['id'] === $state['feature']['id']) {
- $flag['feature_state_value'] =
- $state['feature_state_value'];
- $flag['enabled'] = $state['enabled'];
- $new = false;
- break;
- }
- }
- if ($new) {
- $identity['flags'][] = [
- 'id' => null,
- 'feature_state_value' => $state['feature_state_value'],
- 'enabled' => $state['enabled'],
- 'environment' => $state['environment']['id'],
- 'feature_segment' => $state['feature_segment'],
- 'feature' => [
- 'id' => $state['feature']['id'],
- 'name' => $state['feature']['name'],
- 'created_date' => $state['feature']['created_date'],
- 'description' => $state['feature']['description'],
- 'initial_value' =>
- $state['feature']['initial_value'],
- 'default_enabled' =>
- $state['feature']['default_enabled'],
- 'type' => $state['feature']['type'],
- ],
- ];
+ $identity = $cache->get("Identity.{$identityId}");
+ if ($identity === null) {
+ //No cache point exists so update all
+ $flagsmith->withSkipCache(true)->getIdentityFlags($identityId);
+ return response('');
+ }
+ $existingKey = null;
+ foreach ($identity->flags as $key => $flag) {
+ if ($flag->feature->id === $state['feature']['id']) {
+ $existingKey = $key;
+ break;
}
+ }
- $cache->set("identity.{$identityId}", $identity);
-
- return response('');
+ if ($existingKey !== null) {
+ $identity->flags[$existingKey] = json_decode(
+ json_encode($state),
+ );
+ } else {
+ $identity->flags[] = json_decode(json_encode($state));
}
- //No cache point exists so update all
- $flagsmith->getIdentityByIndentity(new Identity($identityId));
+ $cache->set("Identity.{$identityId}", $identity);
+
return response('');
}
//Global cache needs to be updated
- $global = $cache->get('global');
-
- //A Previous cache point exists
- if (!is_null($global)) {
- //TODO: This does not seem the best way to do this
- $new = true;
- foreach ($global as &$flag) {
- if ($flag['feature']['id'] === $state['feature']['id']) {
- $flag['feature_state_value'] =
- $state['feature_state_value'];
- $flag['enabled'] = $state['enabled'];
- $new = false;
- break;
- }
- }
- if ($new) {
- $global[] = [
- 'id' => null,
- 'feature_state_value' => $state['feature_state_value'],
- 'enabled' => $state['enabled'],
- 'environment' => $state['environment']['id'],
- 'feature_segment' => $state['feature_segment'],
- 'feature' => [
- 'id' => $state['feature']['id'],
- 'name' => $state['feature']['name'],
- 'created_date' => $state['feature']['created_date'],
- 'description' => $state['feature']['description'],
- 'initial_value' => $state['feature']['initial_value'],
- 'default_enabled' =>
- $state['feature']['default_enabled'],
- 'type' => $state['feature']['type'],
- ],
- ];
- }
+ $global = $cache->get('Global');
- $cache->set('global', $global);
+ if ($global === null) {
+ $flagsmith->getEnvironmentFlags();
return response('');
}
- //No cache point exists so update all
- $flagsmith->getFlags();
+ $existingKey = null;
+ foreach ($global as $key => $flag) {
+ if ($flag->feature->id === $state['feature']['id']) {
+ $existingKey = $key;
+ break;
+ }
+ }
+
+ if ($existingKey !== null) {
+ $global[$existingKey]->feature->default_enabled =
+ $state['feature']['default_enabled'];
+ $global[$existingKey]->feature->description =
+ $state['feature']['description'];
+ $global[$existingKey]->feature->initial_value =
+ $state['feature']['initial_value'];
+ $global[$existingKey]->feature->type = $state['feature']['type'];
+ $global[$existingKey]->feature_state_value =
+ $state['feature_state_value'];
+ $global[$existingKey]->environment = $state['environment']['id'];
+ $global[$existingKey]->identity = $state['identity'];
+ $global[$existingKey]->feature_segment = $state['feature_segment'];
+ $global[$existingKey]->enabled = $state['enabled'];
+ } else {
+ $feature = new stdClass();
+ $feature->id = $state['feature']['id'];
+ $feature->name = $state['feature']['name'];
+ $feature->created_date = $state['feature']['created_date'];
+ $feature->description = $state['feature']['description'];
+ $feature->initial_value = $state['feature']['initial_value'];
+ $feature->default_enabled = $state['feature']['default_enabled'];
+ $feature->type = $state['feature']['type'];
+
+ $flag = new stdClass();
+ $flag->id = null; //unsure what this represents
+ $flag->feature = $feature;
+ $flag->feature_state_value = $state['feature_state_value'];
+ $flag->environment = $state['environment']['id'];
+ $flag->identity = $state['identity'];
+ $flag->feature_segment = $state['feature_segment'];
+ $flag->enabled = $state['enabled'];
+ $global[] = $flag;
+ }
+
+ $cache->set('Global', $global);
return response('');
}
}
diff --git a/src/Jobs/SyncUser.php b/src/Jobs/SyncUser.php
index 26077b0..a067272 100644
--- a/src/Jobs/SyncUser.php
+++ b/src/Jobs/SyncUser.php
@@ -1,6 +1,8 @@
user = $user;
}
@@ -31,12 +37,6 @@ public function __construct(Authenticatable $user)
*/
public function handle()
{
- //Our Trait exists
- if (!method_exists($this->user, 'getFlagsmith')) {
- return;
- }
-
- $this->user->skipFeatureCache(true);
- $this->user->getFeatures();
+ $this->user->skipFlagCache(true)->getFlags();
}
}
diff --git a/src/Listeners/UserLogin.php b/src/Listeners/UserLogin.php
index 40041f0..94f6975 100644
--- a/src/Listeners/UserLogin.php
+++ b/src/Listeners/UserLogin.php
@@ -1,6 +1,8 @@
user, 'getFlagsmith')) {
- //TODO: should we log this?
+ $user = $event->user;
+
+ if (!($user instanceof UserFlags)) {
return;
}
$queue = config('flagsmith.identity.queue');
- if (is_null($queue) || !$event->user->featuresInCache()) {
- SyncUser::dispatchSync($event->user);
+ if ($queue === null) {
+ return;
+ }
+
+ $cache = $user->getFlagsmith()->getCache();
+ if ($cache === null) {
+ return;
+ }
+
+ //Doesn't exist so get it now
+ if (!$cache->has('Identity.' . $user->getFlagIdentityId())) {
+ SyncUser::dispatchSync($user);
} else {
- SyncUser::dispatch($event->user)->onQueue($queue);
+ SyncUser::dispatch($user)->onQueue($queue);
}
}
}
diff --git a/src/Models/Flag.php b/src/Models/Flag.php
new file mode 100644
index 0000000..35812f5
--- /dev/null
+++ b/src/Models/Flag.php
@@ -0,0 +1,20 @@
+forwardCallTo($this->flagsmith, $name, $arguments);
+ }
+}
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
index ce25c12..6b9b938 100644
--- a/src/ServiceProvider.php
+++ b/src/ServiceProvider.php
@@ -1,12 +1,14 @@
make(
- \Illuminate\Contracts\Cache\Factory::class
+ \Illuminate\Contracts\Cache\Factory::class,
);
$cacheProvider = $cacheFactory->store(
- $store === 'default' ? null : $store
+ $store === 'default' ? null : $store,
);
return (new Flagsmith(
config('flagsmith.key'),
- config('flagsmith.host')
+ config('flagsmith.host'),
))
->withTimeToLive(config('flagsmith.cache.ttl'))
->withCachePrefix(config('flagsmith.cache.prefix'))
@@ -49,7 +51,7 @@ public function boot()
[
self::FLAGSMITH_CONFIG_PATH => config_path('flagsmith.php'),
],
- ['flagsmith']
+ ['flagsmith'],
);
$this->loadRoutesFrom(dirname(__DIR__) . '/routes/flagsmith.php');
diff --git a/testbench.yaml b/testbench.yaml
new file mode 100644
index 0000000..0f0ff04
--- /dev/null
+++ b/testbench.yaml
@@ -0,0 +1,20 @@
+providers:
+ - Clearlyip\LaravelFlagsmith\ServiceProvider
+
+migrations:
+ - workbench/database/migrations
+
+seeders:
+ - Workbench\Database\Seeders\DatabaseSeeder
+
+workbench:
+ start: '/'
+ install: true
+ discovers:
+ web: true
+ api: false
+ commands: false
+ views: false
+ build: []
+ assets: []
+ sync: []
diff --git a/tests/App.php b/tests/App.php
new file mode 100644
index 0000000..c081fec
--- /dev/null
+++ b/tests/App.php
@@ -0,0 +1,17 @@
+ fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $this->assertInstanceOf(Flagsmith::class, $user->getFlagsmith());
+ }
+
+ public function testGetFlags()
+ {
+ $flagsmith = \Mockery::mock(Flagsmith::class);
+ $this->app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.identity.identifier', 'id');
+ Config::set('flagsmith.identity.traits', ['email']);
+ $user = User::create([
+ 'name' => fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getIdentityFlags')
+ ->once()
+ ->withArgs(
+ fn($identifier, $traits) => $identifier ===
+ (string) $user->id && $traits->email === $user->email,
+ )
+ ->andReturn(new Flags());
+
+ $user->getFlags();
+ }
+
+ public function testSkipFlagCache()
+ {
+ $user = User::create([
+ 'name' => fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $this->assertFalse($user->getFlagsmith()->skipCache());
+
+ $user->skipFlagCache();
+
+ $this->assertTrue($user->getFlagsmith()->skipCache());
+ }
+
+ public function testIsFlagEnabled()
+ {
+ $flagsmith = \Mockery::mock(Flagsmith::class);
+ $this->app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.identity.identifier', 'id');
+ $user = User::create([
+ 'name' => fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $FlagModelsList = new FlagModelsList([
+ 'bar' => (new Flag())->withFeatureName('bar')->withEnabled(true),
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getIdentityFlags')
+ ->times(3)
+ ->withArgs(
+ fn($identifier, $traits) => $identifier ===
+ (string) $user->id && $traits->email === $user->email,
+ )
+ ->andReturn((new Flags())->withFlags($FlagModelsList));
+
+ $this->assertFalse($user->isFlagEnabled('foo'));
+ $this->assertTrue($user->isFlagEnabled('foo', true));
+
+ $this->assertTrue($user->isFlagEnabled('bar'));
+ }
+
+ public function testGetFlagValue()
+ {
+ $flagsmith = \Mockery::mock(Flagsmith::class);
+ $this->app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.identity.identifier', 'id');
+ $user = User::create([
+ 'name' => fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $FlagModelsList = new FlagModelsList([
+ 'bar' => (new Flag())
+ ->withFeatureName('bar')
+ ->withEnabled(true)
+ ->withValue('foo'),
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getIdentityFlags')
+ ->times(3)
+ ->withArgs(
+ fn($identifier, $traits) => $identifier ===
+ (string) $user->id && $traits->email === $user->email,
+ )
+ ->andReturn((new Flags())->withFlags($FlagModelsList));
+
+ $this->assertNull($user->getFlagValue('foo'));
+ $this->assertEquals('bar', $user->getFlagValue('foo', 'bar'));
+
+ $this->assertEquals($user->getFlagValue('bar'), 'foo');
+ }
+}
diff --git a/tests/Feature/SyncUserTest.php b/tests/Feature/SyncUserTest.php
new file mode 100644
index 0000000..60836c0
--- /dev/null
+++ b/tests/Feature/SyncUserTest.php
@@ -0,0 +1,46 @@
+app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.identity.identifier', 'id');
+ Config::set('flagsmith.identity.traits', ['email']);
+ $user = User::create([
+ 'name' => fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getIdentityFlags')
+ ->once()
+ ->withArgs(
+ fn($identifier, $traits) => $identifier ===
+ (string) $user->id && $traits->email === $user->email,
+ )
+ ->andReturn(new Flags());
+
+ $flagsmith
+ ->shouldReceive('withSkipCache')
+ ->once()
+ ->andReturnSelf();
+
+ (new SyncUser($user))->handle();
+ }
+}
diff --git a/tests/Feature/UserLoginListenerTest.php b/tests/Feature/UserLoginListenerTest.php
new file mode 100644
index 0000000..36bb6ea
--- /dev/null
+++ b/tests/Feature/UserLoginListenerTest.php
@@ -0,0 +1,49 @@
+app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.identity.queue', 'default');
+ Config::set('flagsmith.identity.identifier', 'id');
+ Config::set('flagsmith.identity.traits', ['email']);
+ $user = User::create([
+ 'name' => fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getCache')
+ ->once()
+ ->andReturn(
+ new Cache(
+ app(\Illuminate\Contracts\Cache\Factory::class)->store(),
+ 'foo',
+ ),
+ );
+
+ $login = new \Illuminate\Auth\Events\Login('foo', $user, false);
+
+ $userLogin = new UserLogin();
+ $userLogin->handle($login);
+
+ Bus::assertDispatched(SyncUser::class);
+ }
+}
diff --git a/tests/Feature/WebhooksTest.php b/tests/Feature/WebhooksTest.php
new file mode 100644
index 0000000..d094c0c
--- /dev/null
+++ b/tests/Feature/WebhooksTest.php
@@ -0,0 +1,416 @@
+ fake()->name(),
+ 'email' => fake()->email(),
+ 'password' => fake()->password(),
+ ]);
+
+ $trait = new stdClass();
+ $trait->id = 5638;
+ $trait->trait_key = 'email';
+ $trait->trait_value = $user->email;
+
+ $flag = new stdClass();
+ $flag->id = 12;
+ $flag->feature = new stdClass();
+ $flag->feature->id = 7168;
+ $flag->feature->name = 'butter_bar';
+ $flag->feature->created_date = '2021-02-10T20:03:43.348556Z';
+ $flag->feature->description =
+ 'Show html in a butter bar for certain users';
+ $flag->feature->initial_value = null;
+ $flag->feature->default_enabled = false;
+ $flag->feature->type = 'CONFIG';
+ $flag->feature_state_value =
+ 'You are using the develop environment.';
+ $flag->environment = 23;
+ $flag->identity = null;
+ $flag->feature_segment = null;
+ $flag->enabled = false;
+
+ $cachedIdentityFlagsApiResponse = new stdClass();
+ $cachedIdentityFlagsApiResponse->traits = [$trait];
+ $cachedIdentityFlagsApiResponse->flags = [$flag];
+
+ Config::set('flagsmith.identity.queue', 'default');
+ Config::set('flagsmith.identity.identifier', 'id');
+ Config::set('flagsmith.identity.traits', ['email']);
+
+ $flagsmith = \Mockery::mock(Flagsmith::class);
+ $this->app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.webhooks.feature.route', 'webhook');
+
+ Route::middleware(
+ config('flagsmith.webhooks.feature.middleware', []),
+ )->post(config('flagsmith.webhooks.feature.route'), [
+ Webhooks::class,
+ 'feature',
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getCache')
+ ->times(3)
+ ->andReturn(
+ new Cache(
+ app(\Illuminate\Contracts\Cache\Factory::class)->store(),
+ 'flagsmith',
+ ),
+ );
+
+ $flagsmith
+ ->shouldReceive('hasCache')
+ ->times(2)
+ ->andReturn(true);
+
+ $flagsmithCache = $flagsmith->getCache();
+ $flagsmithCache->set(
+ 'Identity.' . $user->id,
+ $cachedIdentityFlagsApiResponse,
+ );
+
+ $res = $flagsmithCache->get('Identity.' . $user->id);
+ $this->assertEquals($res->traits[0]->trait_key, 'email');
+ $this->assertEquals($res->traits[0]->trait_value, $user->email);
+ $this->assertFalse($res->flags[0]->enabled);
+
+ $response = $this->post('webhook', [
+ 'data' => [
+ 'changed_by' => 'Ben Rometsch',
+ 'new_state' => [
+ 'enabled' => true,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7168,
+ 'initial_value' => null,
+ 'name' => 'butter_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => '1',
+ ],
+ 'previous_state' => [
+ 'enabled' => false,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7168,
+ 'initial_value' => null,
+ 'name' => 'butter_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => '1',
+ ],
+ 'timestamp' => '2021-06-18T07:50:26.595298Z',
+ ],
+ 'event_type' => 'FLAG_UPDATED',
+ ]);
+
+ $response->assertOk();
+
+ $res = $flagsmithCache->get('Identity.' . $user->id);
+ $this->assertEquals($res->traits[0]->trait_key, 'email');
+ $this->assertEquals($res->traits[0]->trait_value, $user->email);
+ $this->assertTrue($res->flags[0]->enabled);
+
+ $response = $this->post('webhook', [
+ 'data' => [
+ 'changed_by' => 'Ben Rometsch',
+ 'new_state' => [
+ 'enabled' => true,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7169,
+ 'initial_value' => null,
+ 'name' => 'spinach_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => '1',
+ ],
+ 'previous_state' => [
+ 'enabled' => false,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7169,
+ 'initial_value' => null,
+ 'name' => 'spinach_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => '1',
+ ],
+ 'timestamp' => '2021-06-18T07:50:26.595298Z',
+ ],
+ 'event_type' => 'FLAG_UPDATED',
+ ]);
+
+ $response->assertOk();
+
+ $res = $flagsmithCache->get('Identity.' . $user->id);
+ $this->assertEquals($res->flags[1]->feature->name, 'spinach_bar');
+ }
+
+ public function testGlobalWebhook()
+ {
+ $cachedGlobalFlagsApiResponse = [
+ 0 => [
+ 'id' => 12,
+ 'feature' => [
+ 'id' => 7168,
+ 'name' => 'butter_bar',
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'initial_value' => null,
+ 'default_enabled' => false,
+ 'type' => 'CONFIG',
+ ],
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'environment' => 23,
+ 'identity' => null,
+ 'feature_segment' => null,
+ 'enabled' => false,
+ ],
+ ];
+
+ $flagsmith = \Mockery::mock(Flagsmith::class);
+ $this->app->instance(Flagsmith::class, $flagsmith);
+
+ Config::set('flagsmith.webhooks.feature.route', 'webhook');
+
+ Route::middleware(
+ config('flagsmith.webhooks.feature.middleware', []),
+ )->post(config('flagsmith.webhooks.feature.route'), [
+ Webhooks::class,
+ 'feature',
+ ]);
+
+ $flagsmith
+ ->shouldReceive('getCache')
+ ->times(3)
+ ->andReturn(
+ new Cache(
+ app(\Illuminate\Contracts\Cache\Factory::class)->store(),
+ 'flagsmith',
+ ),
+ );
+
+ $flagsmith
+ ->shouldReceive('hasCache')
+ ->times(2)
+ ->andReturn(true);
+
+ $flagsmithCache = $flagsmith->getCache();
+ $flagsmithCache->set(
+ 'Global',
+ json_decode(json_encode($cachedGlobalFlagsApiResponse)),
+ );
+
+ $res = $flagsmithCache->get('Global');
+ $this->assertFalse($res[0]->enabled);
+
+ $response = $this->post('webhook', [
+ 'data' => [
+ 'changed_by' => 'Ben Rometsch',
+ 'new_state' => [
+ 'enabled' => true,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7168,
+ 'initial_value' => null,
+ 'name' => 'butter_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => null,
+ ],
+ 'previous_state' => [
+ 'enabled' => false,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7168,
+ 'initial_value' => null,
+ 'name' => 'butter_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => null,
+ ],
+ 'timestamp' => '2021-06-18T07:50:26.595298Z',
+ ],
+ 'event_type' => 'FLAG_UPDATED',
+ ]);
+
+ $response->assertOk();
+
+ $res = $flagsmithCache->get('Global');
+ $this->assertTrue($res[0]->enabled);
+
+ $response = $this->post('webhook', [
+ 'data' => [
+ 'changed_by' => 'Ben Rometsch',
+ 'new_state' => [
+ 'enabled' => true,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7169,
+ 'initial_value' => null,
+ 'name' => 'spinach_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => null,
+ ],
+ 'previous_state' => [
+ 'enabled' => false,
+ 'environment' => [
+ 'id' => 23,
+ 'name' => 'Development',
+ ],
+ 'feature' => [
+ 'created_date' => '2021-02-10T20:03:43.348556Z',
+ 'default_enabled' => false,
+ 'description' =>
+ 'Show html in a butter bar for certain users',
+ 'id' => 7169,
+ 'initial_value' => null,
+ 'name' => 'spinach_bar',
+ 'project' => [
+ 'id' => 12,
+ 'name' => 'Flagsmith Website',
+ ],
+ 'type' => 'CONFIG',
+ ],
+ 'feature_segment' => null,
+ 'feature_state_value' =>
+ 'You are using the develop environment.',
+ 'identity' => null,
+ 'identity_identifier' => null,
+ ],
+ 'timestamp' => '2021-06-18T07:50:26.595298Z',
+ ],
+ 'event_type' => 'FLAG_UPDATED',
+ ]);
+
+ $response->assertOk();
+
+ $res = $flagsmithCache->get('Global');
+ $this->assertEquals($res[1]->feature->name, 'spinach_bar');
+ }
+}
diff --git a/tests/Models/User.php b/tests/Models/User.php
new file mode 100644
index 0000000..3e885a2
--- /dev/null
+++ b/tests/Models/User.php
@@ -0,0 +1,41 @@
+
+ */
+ protected $fillable = ['name', 'email', 'password'];
+
+ /**
+ * The attributes that should be hidden for serialization.
+ *
+ * @var array
+ */
+ protected $hidden = ['password', 'remember_token'];
+
+ /**
+ * Get the attributes that should be cast.
+ *
+ * @return array
+ */
+ protected function casts(): array
+ {
+ return [
+ 'email_verified_at' => 'datetime',
+ 'password' => 'hashed',
+ ];
+ }
+}
diff --git a/workbench/app/Models/.gitkeep b/workbench/app/Models/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php
new file mode 100644
index 0000000..e8cec9c
--- /dev/null
+++ b/workbench/app/Providers/WorkbenchServiceProvider.php
@@ -0,0 +1,24 @@
+get('/user', function (Request $request) {
+// return $request->user();
+// });
diff --git a/workbench/routes/console.php b/workbench/routes/console.php
new file mode 100644
index 0000000..3c0324c
--- /dev/null
+++ b/workbench/routes/console.php
@@ -0,0 +1,19 @@
+comment(Inspiring::quote());
+// })->purpose('Display an inspiring quote');
diff --git a/workbench/routes/web.php b/workbench/routes/web.php
new file mode 100644
index 0000000..d259f33
--- /dev/null
+++ b/workbench/routes/web.php
@@ -0,0 +1,18 @@
+