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 + + + + + + + + + + + + + + + + src + + + + + + + + + + + + + + + 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 @@ +