Skip to content

Commit

Permalink
Merge branch 'master' into enable-reporting-client-safe-exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
remipelhate authored Jan 10, 2025
2 parents 1357c4e + 2f0cd99 commit 6e58641
Show file tree
Hide file tree
Showing 18 changed files with 2,007 additions and 15 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ You can find and compare releases at the [GitHub release page](https://github.co

- Make reporting of client-safe errors configurable https://github.com/nuwave/lighthouse/issues/2647

## v6.48.0

### Added

- Add `@bind` directive as a GraphQL analogue for Laravel's Route Model Binding https://github.com/nuwave/lighthouse/pull/2645

## v6.47.1

### Fixed

- Cast model count to `int` in `CountModelsLoader` https://github.com/nuwave/lighthouse/pull/2646

## v6.47.0

### Added
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"Nuwave\\Lighthouse\\LighthouseServiceProvider",
"Nuwave\\Lighthouse\\Async\\AsyncServiceProvider",
"Nuwave\\Lighthouse\\Auth\\AuthServiceProvider",
"Nuwave\\Lighthouse\\Bind\\BindServiceProvider",
"Nuwave\\Lighthouse\\Cache\\CacheServiceProvider",
"Nuwave\\Lighthouse\\GlobalId\\GlobalIdServiceProvider",
"Nuwave\\Lighthouse\\OrderBy\\OrderByServiceProvider",
Expand Down
151 changes: 151 additions & 0 deletions docs/6/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,157 @@ type RoleEdge {
}
```

## @bind

```graphql
"""
Replace argument values with the corresponding model (or some other value) before passing them to the resolver.
For example, instead of injecting a user's ID, you can inject the entire User model instance that matches the given ID.
This eliminates the need to manually query for the instance inside the resolver.
This works analogues to [Laravel's Route Model Binding](https://laravel.com/docs/routing#route-model-binding).
"""
directive @bind(
"""
Specify the fully qualified class name of the binding to use.
This can be either an Eloquent model, or a class that defines a method `__invoke` that resolves the value.
"""
class: String!

"""
Specify the column name of a unique identifier to use when binding Eloquent models.
By default, "id" is used as the primary key column.
"""
column: String! = "id"

"""
Specify the relations to eager-load when binding Eloquent models.
"""
with: [String!]! = []

"""
Specify whether the binding should be considered required.
When set to `true`, a validation error will be thrown if the value (or any of the list values) can not be resolved.
The field resolver will not be invoked in this case.
When set to `false`, argument values that can not be resolved will be passed to the resolver as `null`.
When the argument is a list, individual values that can not be resolved will be filtered out.
"""
required: Boolean! = true
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
```

### Basic usage

```graphql
type Mutation {
addUserToCompany(
user: ID! @bind(class: "App\\Models\\User")
company: ID! @bind(class: "App\\Models\\Company")
): Boolean!
}
```

```php
namespace App\GraphQL\Mutations;

final class AddUserToCompany
{
/**
* @param array{
* user: \App\Models\User,
* company: \App\Models\Company,
* } $args
*/
public function __invoke(mixed $root, array $args): bool
{
$user = $args['user'];
$user->associate($args['company']);

return $user->save();
}
}
```

### Binding instances that are not Eloquent models

To bind instances that are not Eloquent models, callable classes can be used instead:

```graphql
type Mutation {
updateCompanyInfo(
company: ID! @bind(class: "App\\Http\\GraphQL\\Bindings\\CompanyBinding")
): Boolean!
}
```

```php
namespace App\GraphQL\Bindings;

use App\External\Company;
use App\External\CompanyRepository;
use Nuwave\Lighthouse\Bind\BindDefinition;

final class CompanyBinding
{
public function __construct(
private CompanyRepository $companyRepository,
) {}

public function __invoke(string $value, BindDefinition $definition): ?Company
{
if ($definition->required) {
return $this->companyRepository->findOrFail($value);
}

return $this->companyRepository->find($value);
}
}
```

### Binding a collection of instances

When the `@bind` directive is defined on an argument or input field with an array value,
it can be used to resolve a collection of instances.

```graphql
type Mutation {
addUsersToCompany(
users: [ID!]! @bind(class: "App\\Models\\User")
company: ID! @bind(class: "App\\Models\\Company")
): [User!]!
}
```

```php
namespace App\GraphQL\Mutations;

use App\Models\User;

final class AddUsersToCompany
{
/**
* @param array{
* users: \Illuminate\Database\Eloquent\Collection<\App\Models\User>,
* company: \App\Models\Company,
* } $args
*
* @return \Illuminate\Database\Eloquent\Collection<\App\Models\User>
*/
public function __invoke(mixed $root, array $args): Collection
{
return $args['users']
->map(function (User $user) use ($args): ?User {
$user->associate($args['company']);

return $user->save()
? $user
: null;
})
->filter();
}
}
```

## @broadcast

```graphql
Expand Down
151 changes: 151 additions & 0 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,157 @@ type RoleEdge {
}
```

## @bind

```graphql
"""
Replace argument values with the corresponding model (or some other value) before passing them to the resolver.
For example, instead of injecting a user's ID, you can inject the entire User model instance that matches the given ID.
This eliminates the need to manually query for the instance inside the resolver.
This works analogues to [Laravel's Route Model Binding](https://laravel.com/docs/routing#route-model-binding).
"""
directive @bind(
"""
Specify the fully qualified class name of the binding to use.
This can be either an Eloquent model, or a class that defines a method `__invoke` that resolves the value.
"""
class: String!

"""
Specify the column name of a unique identifier to use when binding Eloquent models.
By default, "id" is used as the primary key column.
"""
column: String! = "id"

"""
Specify the relations to eager-load when binding Eloquent models.
"""
with: [String!]! = []

"""
Specify whether the binding should be considered required.
When set to `true`, a validation error will be thrown if the value (or any of the list values) can not be resolved.
The field resolver will not be invoked in this case.
When set to `false`, argument values that can not be resolved will be passed to the resolver as `null`.
When the argument is a list, individual values that can not be resolved will be filtered out.
"""
required: Boolean! = true
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
```

### Basic usage

```graphql
type Mutation {
addUserToCompany(
user: ID! @bind(class: "App\\Models\\User")
company: ID! @bind(class: "App\\Models\\Company")
): Boolean!
}
```

```php
namespace App\GraphQL\Mutations;

final class AddUserToCompany
{
/**
* @param array{
* user: \App\Models\User,
* company: \App\Models\Company,
* } $args
*/
public function __invoke(mixed $root, array $args): bool
{
$user = $args['user'];
$user->associate($args['company']);

return $user->save();
}
}
```

### Binding instances that are not Eloquent models

To bind instances that are not Eloquent models, callable classes can be used instead:

```graphql
type Mutation {
updateCompanyInfo(
company: ID! @bind(class: "App\\Http\\GraphQL\\Bindings\\CompanyBinding")
): Boolean!
}
```

```php
namespace App\GraphQL\Bindings;

use App\External\Company;
use App\External\CompanyRepository;
use Nuwave\Lighthouse\Bind\BindDefinition;

final class CompanyBinding
{
public function __construct(
private CompanyRepository $companyRepository,
) {}

public function __invoke(string $value, BindDefinition $definition): ?Company
{
if ($definition->required) {
return $this->companyRepository->findOrFail($value);
}

return $this->companyRepository->find($value);
}
}
```

### Binding a collection of instances

When the `@bind` directive is defined on an argument or input field with an array value,
it can be used to resolve a collection of instances.

```graphql
type Mutation {
addUsersToCompany(
users: [ID!]! @bind(class: "App\\Models\\User")
company: ID! @bind(class: "App\\Models\\Company")
): [User!]!
}
```

```php
namespace App\GraphQL\Mutations;

use App\Models\User;

final class AddUsersToCompany
{
/**
* @param array{
* users: \Illuminate\Database\Eloquent\Collection<\App\Models\User>,
* company: \App\Models\Company,
* } $args
*
* @return \Illuminate\Database\Eloquent\Collection<\App\Models\User>
*/
public function __invoke(mixed $root, array $args): Collection
{
return $args['users']
->map(function (User $user) use ($args): ?User {
$user->associate($args['company']);

return $user->save()
? $user
: null;
})
->filter();
}
}
```

## @broadcast

```graphql
Expand Down
54 changes: 54 additions & 0 deletions src/Bind/BindDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Bind;

use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Exceptions\DefinitionException;

/** @template-covariant TClass of object */
class BindDefinition
{
public function __construct(
/** @var class-string<TClass> */
public string $class,
public string $column,
/** @var array<string> */
public array $with,
public bool $required,
) {}

public function validate(
InputValueDefinitionNode $definitionNode,
FieldDefinitionNode|InputObjectTypeDefinitionNode $parentNode,
): void {
$nodeName = $definitionNode->name->value;
$parentNodeName = $parentNode->name->value;

if (! class_exists($this->class)) {
throw new DefinitionException(
"@bind argument `class` defined on `{$parentNodeName}.{$nodeName}` must be an existing class, received `{$this->class}`.",
);
}

if ($this->isModelBinding()) {
return;
}

if (method_exists($this->class, '__invoke')) {
return;
}

$modelClass = Model::class;
throw new DefinitionException(
"@bind argument `class` defined on `{$parentNodeName}.{$nodeName}` must extend {$modelClass} or define the method `__invoke`, but `{$this->class}` does neither.",
);
}

public function isModelBinding(): bool
{
return is_subclass_of($this->class, Model::class);
}
}
Loading

0 comments on commit 6e58641

Please sign in to comment.