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

Add @feature directive to conditionally add a field based on a feature state (using Laravel Pennant) #2442

Merged
merged 29 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0ebfe10
Suggest laravel/pennant in order to use @feature directive
remipelhate Sep 1, 2023
da6f4d8
Register Pennant directive namespace
remipelhate Sep 1, 2023
797ba08
Add @feature directive
remipelhate Sep 1, 2023
3dec4a0
Add @feature directive to docs
remipelhate Sep 1, 2023
1b7bed3
Improve directive description
remipelhate Sep 1, 2023
a9b2764
Update changelog
remipelhate Sep 1, 2023
f45b452
Update composer.json
remipelhate Sep 1, 2023
e9bdc84
Improve directive description
remipelhate Sep 1, 2023
d3f60e7
Ensure name argument on @feature is not nullable
remipelhate Sep 1, 2023
83a545a
Ensure when argument on @feature is not nullable
remipelhate Sep 1, 2023
d04532e
Improve definition error message on `when` argument
remipelhate Sep 1, 2023
3080ca2
Define schema for each tests separately
remipelhate Sep 1, 2023
9f87761
Remove `query` from graphQL request in FeatureDirectiveTest
remipelhate Sep 1, 2023
95b2263
Extract field value of mocked resolver to a variable
remipelhate Sep 1, 2023
f135855
Update @feature section in the api reference to match the directive d…
remipelhate Sep 1, 2023
18b07c4
Update feature toggles documentation
remipelhate Sep 2, 2023
4863e31
Improve docs
remipelhate Sep 4, 2023
a8191f8
Merge branch 'master' into feature-directive
spawnia Sep 6, 2023
5e2c43b
Omit pennant installation
spawnia Sep 6, 2023
e33f6e9
skip test
spawnia Sep 6, 2023
769a6ed
fix if
spawnia Sep 6, 2023
31f0bd2
switch benchmarks to Laravel 10
spawnia Sep 6, 2023
1e786ed
depend on Laravel 10
spawnia Sep 6, 2023
428a055
fix stan
spawnia Sep 6, 2023
dc0f2b0
revert to coverage
spawnia Sep 6, 2023
142c2b6
rev
spawnia Sep 6, 2023
06592d1
Laravel 10
spawnia Sep 6, 2023
b69b567
Laravel 10
spawnia Sep 6, 2023
57a7caa
fix bench and test
spawnia Sep 6, 2023
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
14 changes: 10 additions & 4 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ jobs:
- name: "Remove conflicting dependencies that are not needed here"
run: composer remove --dev --no-update phpbench/phpbench rector/rector

- if: matrix.laravel-version != '^10'
run: composer remove --dev --no-update laravel/pennant

- run: >
composer require
illuminate/contracts:${{ matrix.laravel-version }}
Expand Down Expand Up @@ -124,6 +127,9 @@ jobs:
- name: "Remove conflicting dependencies that are not needed here"
run: composer remove --dev --no-update nunomaduro/larastan phpstan/phpstan-mockery phpbench/phpbench rector/rector

- if: matrix.laravel-version != '^10'
run: composer remove --dev --no-update laravel/pennant

- run: >
composer require
illuminate/contracts:${{ matrix.laravel-version }}
Expand All @@ -141,9 +147,9 @@ jobs:
strategy:
matrix:
php-version:
- 8.2
- "8.2"
laravel-version:
- ^9
- "^10"

services:
mysql:
Expand Down Expand Up @@ -190,9 +196,9 @@ jobs:
strategy:
matrix:
php-version:
- 8.2
- "8.2"
laravel-version:
- ^9
- "^10"

steps:
- uses: actions/checkout@v3
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Added

- Add directive `@feature` to conditionally add annotated elements based on the state of a feature using [Laravel Pennant](https://github.com/laravel/pennant) https://github.com/nuwave/lighthouse/pull/2442

## v6.17.0

### Added
Expand Down
5 changes: 5 additions & 0 deletions benchmarks/QueryBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ abstract class QueryBench extends TestCase
/** Cached graphQL endpoint. */
protected string $graphQLEndpoint;

public function __construct()
{
parent::__construct(static::class);
}

public function setUp(): void
{
parent::setUp();
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"laravel/framework": "^9 || ^10",
"laravel/legacy-factories": "^1.1.1",
"laravel/lumen-framework": "^9 || ^10 || dev-master",
"laravel/pennant": "^1",
"laravel/scout": "^8 || ^9 || ^10",
"mattiasgeniar/phpunit-query-count-assertions": "^1.1",
"mll-lab/graphql-php-scalars": "^6",
Expand All @@ -72,6 +73,7 @@
},
"suggest": {
"bensampo/laravel-enum": "Convenient enum definitions that can easily be registered in your Schema",
"laravel/pennant": "Required for the @feature directive",
"laravel/scout": "Required for the @search directive",
"mll-lab/graphql-php-scalars": "Useful scalar types, required for @whereConditions",
"mll-lab/laravel-graphiql": "A graphical interactive in-browser GraphQL IDE - integrated with Laravel",
Expand Down
43 changes: 43 additions & 0 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,49 @@ type Mutation {
}
```

## @feature

```graphql
"""
Include the annotated element in the schema depending on a Laravel Pennant feature.
"""
directive @feature(
"""
The name of the feature to be checked (can be a string or class name).
"""
name: String!

"""
Specify what the state of the feature should be for the field to be included.
"""
when: FeatureState! = ACTIVE
) on FIELD_DEFINITION | OBJECT

"""
Options for the `when` argument of `@feature`.
"""
enum FeatureState {
"""
Indicates an active feature.
"""
ACTIVE

"""
Indicates an inactive feature.
"""
INACTIVE
}
```

Requires the installation of [Laravel Pennant](https://laravel.com/docs/pennant)
and manual registration of the service provider in `config/app.php`:

```php
'providers' => [
\Nuwave\Lighthouse\Pennant\PennantServiceProvider::class,
],
```

## @field

```graphql
Expand Down
37 changes: 37 additions & 0 deletions docs/master/digging-deeper/feature-toggles.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,46 @@ type Query {
}
```

## @feature

The [@feature](../api-reference/directives.md#feature) directive allows to include fields in the schema depending
on a [Laravel Pennant](https://laravel.com/docs/pennant) feature.

For example, you might want a new experimental field only to be available when the according feature is active:

```graphql
type Query {
experimentalField: String! @feature(name: "new-api")
}
```

In this case, `experimentalField` will only be included when the `new-api` feature is active.

Another example would be to only include a field when the feature is inactive:

```graphql
type Query {
deprecatedField: String! @feature(name: "new-api", when: "INACTIVE")
remipelhate marked this conversation as resolved.
Show resolved Hide resolved
}
```

When using [class based features](https://laravel.com/docs/pennant#class-based-features),
the fully qualified class name must be used as the value for the `name` argument:

```graphql
type Query {
experimentalField: String! @feature(name: "App\\Features\\NewApi")
}
```

## Interaction With Schema Cache

[@show](../api-reference/directives.md#show) and [@hide](../api-reference/directives.md#hide) work by manipulating the schema.
This means that when using their `env` option, the inclusion or exclusion of elements depends on the value
of `app()->environment()` at the time the schema is built and not update on later environment changes.
If you are pre-generating your schema cache, make sure to match the environment to your deployment target.

The same goes for [@feature](../api-reference/directives.md#feature). Whether a field is included in the schema will be
based on the state of a feature at the time the schema is built. In addition, if you are pre-generating your schema cache,
you will only be able to use features that support [nullable scopes](https://laravel.com/docs/pennant#nullable-scope),
as there won't be an authenticated user to check the feature against.
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ parameters:
excludePaths:
- tests/Utils/Models/WithoutRelationClassImport.php # Intentionally wrong
- tests/LaravelPhpdocAlignmentFixer.php # Copied from laravel/pint
# laravel/pennant requires Laravel 10
- src/Pennant
- tests/Integration/Pennant
ignoreErrors:
# PHPStan does not get it
- '#Parameter \#1 \$callback of static method Closure::fromCallable\(\) expects callable\(\): mixed, array{object, .*} given\.#'
Expand All @@ -24,6 +27,7 @@ parameters:
- path: tests/database/factories/*
message: '#Variable \$factory might not be defined#'


# Mixins are magical
- path: src/Testing/TestResponseMixin.php
message: '#Method Nuwave\\Lighthouse\\Testing\\TestResponseMixin::assertGraphQLErrorMessage\(\) invoked with 1 parameter, 0 required\.#'
Expand Down
63 changes: 63 additions & 0 deletions src/Pennant/FeatureDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Pennant;

use Laravel\Pennant\FeatureManager;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\Directives\HideDirective;

final class FeatureDirective extends HideDirective
{
public function __construct(
private FeatureManager $features,
) {
parent::__construct();
}

public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Include the annotated element in the schema depending on a Laravel Pennant feature.
"""
directive @feature(
"""
The name of the feature to be checked (can be a string or class name).
"""
name: String!

"""
Specify what the state of the feature should be for the field to be included.
"""
when: FeatureState! = ACTIVE
) on FIELD_DEFINITION | OBJECT

"""
Options for the `when` argument of `@feature`.
"""
enum FeatureState {
"""
Indicates an active feature.
"""
ACTIVE

"""
Indicates an inactive feature.
"""
INACTIVE
}
GRAPHQL;
}

protected function shouldHide(): bool
{
$feature = $this->directiveArgValue('name');
$requiredFeatureState = $this->directiveArgValue('when', 'ACTIVE');

return match ($requiredFeatureState) {
'ACTIVE' => $this->features->inactive($feature),
'INACTIVE' => $this->features->active($feature),
default => throw new DefinitionException("Expected FeatureState `ACTIVE` or `INACTIVE` for argument `when` of @{$this->name()} on {$this->nodeName()}, got `{$requiredFeatureState}`."),
};
}
}
15 changes: 15 additions & 0 deletions src/Pennant/PennantServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Pennant;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;

final class PennantServiceProvider extends ServiceProvider
{
public function boot(Dispatcher $dispatcher): void
{
$dispatcher->listen(RegisterDirectiveNamespaces::class, static fn (): string => __NAMESPACE__);
}
}
Loading
Loading