Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: readme #9

Merged
merged 1 commit into from
Jul 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 160 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,204 @@
# Foggle

A feature flagging package for Laravel, heavily inspired by Pennant.
A feature flagging package for Laravel, built with DX in mind.

## Usage
> [!NOTE]
> Some features are not yet implemented:
> - Feature resolutions purge command.
> - Pre-resolution hook for global flagging.

> [!WARNING]
> Note that this usage section is a proposed spec, the implementation is a work in progress.
## Installation

#### Defining features
Install Foggle into your project using composer:

Features are defined as classes. The package will load any feature defined in the `App\Features\` namespace by default, but can be customized.
```shell
composer require youcan-shop/foggle
```

You should then publish yhe configuration files using the following artisan command:

```shell
php artisan vendor:publish --provider="YouCanShop\Foggle\FoggleServiceProvider"
```

## Configuration

After publishing, the configuration file will be located at `config/foggle.php`. This is where you configure your storage providers and context resolvers.
Foggle allows you to store the resolution of your feature flags in a vast array (haha) of data stores, or in an in-memory `array` driver.

## Feature Definition

To define a feature, you can use the `define` method of the `foggle()` helper. You will need to provide a name of the feature, as well as a closure that resolves the initial value.

Usually, a feature should be defined in a dedicated service provider. The closure will receive the `context` for the resolution as an argument, which is most commonly your application's `User` model.

```php
namespace App\Features;
<?php

namespace App\Providers;

class Themes {
public string $name = 'themes';
use App\Models\User;
use Illuminate\Support\ServiceProvider;

public function __construct(private readonly Config $config)
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// the feature is instantiated through the container
// meaning it can inject any dependencies it needs
foggle()->define('themes', fn (User $user) => match (true) {
$user->isTeamMember() => true,
$user->isTestUser() => false,
default => false,
});
}
}
```
The first time the `themes` feature is resolved for a given context (user), the result will be stored by your configured driver. The next time it is checked against the same context, the closure will not be invoked, and the value will be retrieved from the storage.

// The feature defines its context here, can be anything
// The return type is also customizable
## Class Based Features & Discovery

public function resolve(Seller $seller): bool
{
if ($seller->isInternalTeamMember()) {
return true;
Foggle allows you to define class based features. These classes can be automatically discovered and registered into the feature manager. By default, the auto-discovery path is `app/Features` but this can be changed from the config file.

When writing a feature class, you need to define a `resolve` or `__invoke` method, which will be called to resolve the feature's initial value. In this case, the class' FQN will be used as a feature name, but you can override it by defining a public `name` method on your feature class.

```php
<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewApi
{
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}
```

// Note that at this iteration, the package will not manage configuration values for each feature.
## Checking Features

$flaggedSellers = explode($this->config->get('features.themes.seller'), ', ');
To evaluate a feature's value, you may use the `active` method on the `foggle()` helper.

return in_array($seller->getId(), $flaggedSellers);
}
```php
<?php

function are_themes_active() {
return foggle()->active('themes');
}
```

The minimum requirements for a feature class are:
- A public `resolve()` method that returns a mixed value.
- A public string `$name` property that is used to identify the feature.
If you need to check a feature against a specific context, you can prepend the feature resolution call call with a `for` method like so:

These cannot be enforced through an interface, and will be validated in the registration phase of the applicaton.
```php
<?php

#### Resolving features
function are_themes_inactive(User $user) {
return foggle()->for($user)->inactive('themes');
}
```

Features are lazily resolved by default. To check whether a feature is active, you can use the Foggle feature manager like so:

```php
// Returns a boolean
make(Foggle::class)->for($seller)->active('themes');
## In-Memory Cache

When resolving a feature, Foggle will create an in-memory cache of the result. If you are using the `redis` driver for example, this means that re-checking hte same feature within the lifetime of a single request will not trigger additional Redis queries.

// Returns the raw resolution
make(Foggle::class)->for($seller)->value('themes');
If you need to manually flush the in-memory cache, you can use the `cFlush` method on the `foggle()` helper.
Note that when running in console, in-memory cache is flushed every time a job is processed to ensure long-running worker processes will always have the latest values.

// you can alternatively use the helper function
foggle()->for($seller)->active('themes');
```php
foggle()->cFlush()
```

Note that `values()` will always return the result of the feature's `resolve()` method, but `active()` will return true if the value evaluates as truthy.
## Context

### Resolvers

It is also possible to use a middleware to check a feature before the request reaches a controller:
As mentioned before, Foggle allows you to check your features against any context using the `for` method on `foggle()`. However, if you would like to omit the `for` every time you check a feature, you can configure custom context resolvers in the config file.

You can do so by creating a class that implements `ContextResolver` like so:

```php
Route::name('themes.index')->get('/themes', ThemesIndexController::class)->middleware(['foggle:themes,themes.install']);
```
<?php

Depending on the configured driver, Foggle will also persist the result of every resolution and re-use it in future requests. This can be particularly useful when defining features that don't depend on a state, for example the themes feature would have a 50% chance of activating at random, instead of persisting that state manually and checking every time the feature resolves, Foggle does that for you.
namespace App\ContextResolvers;

use App\Services\StoreService;
use YouCanShop\Foggle\Contracts\ContextResolver;

class StoreResolver implements ContextResolver {
public function __construct(private readonly StoreService $storeService)
{
}

#### Context resolvers
public function resolve(): ?Store
{
return $this->storeService->getCurrentStore();
}
}
```

Instead of providing the context each time using `for()`, you can define custom context resolvers that are used when a context is not explicitly defined. For example, to use the current authenticated seller for any feature that uses a Seller entity as the context, you can do the following:
You should then bind this class to the type of context it resolves, which is `Store` in this case, in the config file:

```php
// AppServiceProvider.php
<?php

return [
// ...

'context_resolvers' => [
Store::class => StoreResolver::class,
]

foggle()->resolveContextUsing(Seller::class, fn () => auth()->user());
];
```

### Defining A Feature's Context Type

By default, if you do not provide context to a feature, it will not try to use a context resolver and will default to null. You can tell Foggle which resolver to use in the feature definition in one of two ways:
- When using class based features, you must define a public `$contextType` property containing the context's class name.
- When using closure based features, or `FogGen` generations, you can declare your context type as a 3rd parameter to the `define` method on `foggle()`.

### Scope Identifiers

Foggle's built-in drivers will store your context alongside the resolution value in their stores. However, serializing some contexts as-is can be heavy on some data stores (e.g. Redis) which is why every context that isn't a string must implement the `Foggable` interface.

```php
<?php

namespace App\Models;

use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;

class User extends Model implements Foggable
{
public function foggleId(): string
{
return $this->id;
}
}
```

## Feature generation

Some features are often too simple to warrant an entire class or a repeated closure on each definition, which is why Foggle provides `FogGen` class that generates common feature closures.

### FogGen::inconfig()

The `inconfig` method takes a config path and generates a feature that checks if the given context's identifier is included in the provided config's value. If the config value is a string, `inconfig` attempts to explode it into an array using the `,` separator by default, which can be changed using its optional 2nd param.

```php
<?php

foggle()->define('billing-v2', FogGen::inconfig('features.billing-v2.stores', ','), Store::class);

```

In this case, it will check if the store's `id` attribute exists within the `features.billing-v2.stores` value.
Loading