Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time

Upgrade guide

This document provides guidance for upgrading between major versions of Lighthouse.

General tips

The configuration options often change between major versions. Compare your lighthouse.php against the latest default configuration.

v5 to v6

messages on @rules and @rulesForArray

Lighthouse previously allowed passing a map with arbitrary keys as the messages argument on @rules and @rulesForArray. Such a construct is impossible to define within the directive definition and leads to static validation errors.

    apply: ["max:280"],
-   messages: {
-       max: "Tweets have a limit of 280 characters"
-   }
+   messages: [
+       {
+           rule: "max"
+           message: "Tweets have a limit of 280 characters"
+       }
+   ]

Use @globalId over @delete(globalId: true)

The @delete, @forceDelete, @restore and @upsert directives no longer offer the globalId argument. Use @globalId on the argument instead.

type Mutation {
-   deleteUser(id: ID!): User! @delete(globalId: true)
+   deleteUser(id: ID! @globalId): User! @delete

Specify @guard(with: "api") as @guard(with: ["api"])

Due to Lighthouse's ongoing effort to provide static schema validation, the with argument of @guard must now be provided as a list of strings.

type Mutation {
-   somethingSensitive: Boolean @guard(with: "api")
+   somethingSensitive: Boolean @guard(with: ["api"])

Use subscriptions response format version 2

The previous version 1 contained a redundant key channels and is no longer supported.

  "data": {...},
  "extensions": {
    "lighthouse_subscriptions": {
-     "version": 1,
+     "version": 2,
      "channel": "channel-name"
-     "channels": {
-       "subscriptionName": "channel-name"
-     },

It is recommended to switch to version 2 before upgrading Lighthouse to give clients a smooth transition period.

Nullability of pagination results

Generated result types of paginated lists are now always marked as non-nullable. The setting non_null_pagination_results was removed and now always behaves as if it were true.

This is generally more convenient for clients, but will cause validation errors to bubble further up in the result.

Include field cost in @complexity calculation

Previous to v6, the default query complexity calculation of fields with @complexity did not include the cost of the field itself - other than the default without the directive. In the future, a value of 1 will be added to represent the complexity more accurately.

This change will increase the complexity of queries on fields using @complexity without a custom complexity resolver. If you configured security.max_query_complexity, complex queries that previously passed might now fail.

Passing of BenSampo\Enum\Enum instances to ArgBuilderDirective::handleBuilder()

Previous to v6, Lighthouse would extract the internal $value from instances of BenSampo\Enum\Enum before passing it to ArgBuilderDirective::handleBuilder() if the setting unbox_bensampo_enum_enum_instances was true.

This is generally unnecessary, because Laravel automagically calls the Enum's __toString() method when using it in a query. This might affect users who use an ArgBuilderDirective that delegates to a method that relies on an internal value being passed.

type Query {
    withEnum(byType: AOrB @scope): WithEnum @find
// WithEnum.php
public function scopeByType(Builder $builder, int $aOrB): Builder
    return $builder->where('type', $aOrB);

In the future, Lighthouse will pass the actual Enum instance along. You can opt in to the new behaviour before upgrading by setting unbox_bensampo_enum_enum_instances to false.

public function scopeByType(Builder $builder, AOrB $aOrB): Builder

Replace Nuwave\Lighthouse\GraphQL::executeQuery() usage

Use parseAndExecuteQuery() for executing a string query or executeParsedQuery() for executing already parsed DocumentNode.

v4 to v5

Update PHP, Laravel and PHPUnit

The following versions are now the minimal required versions:

  • PHP 7.2
  • Laravel 5.6
  • PHPUnit 7

Final schema may change

Parts of the final schema are automatically generated by Lighthouse. Clients that depend on specific fields or type names may have to adapt. The recommended process for finding breaking changes is:

  1. Print your schema before upgrading: php artisan lighthouse:print-schema > old.graphql
  2. Upgrade, then re-print your schema: php artisan lighthouse:print-schema > new.graphql
  3. Use graphql-inspector to compare your changes: graphql-inspector diff old.graphql new.graphql

Rename resolve to __invoke

Field resolver classes now only support the method name __invoke, using the name resolve no longer works.

namespace App\GraphQL\Queries;

class SomeField
-   public function resolve(...
+   public function __invoke(...

Replace @middleware with @guard and specialized FieldMiddleware

The @middleware directive has been removed, as it violates the boundary between HTTP and GraphQL request handling.

Authentication is one of most common use cases for @middleware. You can now use the @guard directive on selected fields.

type Query {
-   profile: User! @middleware(checks: ["auth"])
+   profile: User! @guard

Note that @guard does not log in users. To ensure the user is logged in, add the AttemptAuthenticate middleware to your lighthouse.php middleware config, see the default config for an example.

Other functionality can be replaced by a custom FieldMiddleware directive. Just like Laravel Middleware, it can wrap around individual field resolvers.

Directives must have an SDL definition

The interface \Nuwave\Lighthouse\Support\Contracts\Directive now has the same functionality as the removed \Nuwave\Lighthouse\Support\Contracts\DefinedDirective. If you previously implemented DefinedDirective, remove it from your directives:

-use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;

-class TrimDirective extends BaseDirective implements ArgTransformerDirective, DefinedDirective
+class TrimDirective extends BaseDirective implements ArgTransformerDirective

Instead of just providing the name of the directive, all directives must now return an SDL definition that formally describes them.

-    public function name()
-    {
-        return 'trim';
-    }

+    /**
+     * Formal directive specification in schema definition language (SDL).
+     *
+     * @return string
+     */
+    public static function definition(): string
+    {
+        return /** @lang GraphQL */ <<<'GRAPHQL'
+A description of what this directive does.
+directive @trim(
+    """
+    Directives can have arguments to parameterize them.
+    """
+    someArg: String
+    }

@orderBy argument renamed to column

The argument to specify the column to order by when using @orderBy was renamed to column to match the @whereConditions directive.

Client queries will have to be changed like this:

    posts (
        orderBy: [
-               field: POSTED_AT
+               column: POSTED_AT
                order: ASC
    ) {

If you absolutely cannot break your clients, you can re-implement @orderBy in your project - it is a relatively simple ArgManipulator directive.

@modelClass and @model changed

The @model directive was repurposed to take the place of @modelClass. As a replacement for the current functionality of @model, the new @node directive was added, see for details.

You can adapt to this change in two refactoring steps that must be done in order:

  1. Rename all usages of @model to @node, e.g.:

    -type User @model {
    +type User @node {
        id: ID! @globalId
  2. Rename all usages of @modelClass to @model, e.g.

    -type PaginatedPost @modelClass(class: "\\App\\Post") {
    +type PaginatedPost @model(class: "\\App\\Post") {
        id: ID!

Replace @bcrypt with @hash

The new @hash directive is also used for password hashing, but respects the configuration settings of your Laravel project.

type Mutation {
        name: String!
-       password: String! @bcrypt
+       password: String! @hash
    ): User!

@method passes down just ordered arguments

Instead of passing down the usual resolver arguments, the @method directive will now pass just the arguments given to a field. This behaviour could previously be enabled through the passOrdered option, which is now removed.

type User {
  purchasedItemsCount(year: Int!, includeReturns: Boolean): Int @method

The method will have to change like this:

-public function purchasedItemsCount($root, array $args)
+public function purchasedItemsCount(int $year, ?bool $includeReturns)

Implement ArgDirective or ArgDirectiveForArray explicitly

This affects custom directives that implemented one of the following interfaces:

  • \Nuwave\Lighthouse\Support\Contracts\ArgDirectiveForArray
  • \Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective
  • \Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective

Whereas those interfaces previously extended \Nuwave\Lighthouse\Support\Contracts\ArgDirective, you now have to choose if you want them to apply to entire lists of arguments, elements within that list, or both. Change them as follows to make them behave like in v4:

+use Nuwave\Lighthouse\Support\Contracts\ArgDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;

-class MyCustomArgDirective extends BaseDirective implements ArgTransformerDirective, DefinedDirective
+class MyCustomArgDirective extends BaseDirective implements ArgTransformerDirective, DefinedDirective, ArgDirective

ArgDirective run in distinct phases

The application of directives that implement the ArgDirective interface is split into three distinct phases:

  • Sanitize: Clean the input, e.g. trim whitespace. Directives can hook into this phase by implementing ArgSanitizerDirective.
  • Validate: Ensure the input conforms to the expectations, e.g. check a valid email is given
  • Transform: Change the input before processing it further, e.g. hashing passwords. Directives can hook into this phase by implementing ArgTransformerDirective

Replace custom validation directives with validator classes

The ValidationDirective abstract class was removed in favour of validator classes. They represent a more lightweight way and flexible way to reuse complex validation rules, not only on fields but also on input objects.

To convert an existing custom validation directive to a validator class, change it as follows:


-namespace App\GraphQL\Directives;
+namespace App\GraphQL\Validators;

use Illuminate\Validation\Rule;
-use Nuwave\Lighthouse\Schema\Directives\ValidationDirective;
+use Nuwave\Lighthouse\Validation\Validator;

-class UpdateUserValidationDirective extends ValidationDirective
+class UpdateUserValidator extends Validator
     * @return array<string, array<mixed>>
    public function rules(): array
        return [
            'id' => ['required'],
-            'name' => ['sometimes', Rule::unique('users', 'name')->ignore($this->args['id'], 'id')],
+            'name' => ['sometimes', Rule::unique('users', 'name')->ignore($this->arg('id'), 'id')],

Instead of directly using this class as a directive, place the @validator directive on your field.

type Mutation {
- updateUser(id: ID, name: String): User @update @updateUserValidation
+ updateUser(id: ID, name: String): User @update @validator

Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionEvent is no longer fired

The event is no longer fired, and the event class was removed. Lighthouse now uses a queued job instead.

If you manually fired the event, replace it by queuing a Nuwave\Lighthouse\Subscriptions\BroadcastSubscriptionJob or a call to Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions::queueBroadcast().

In case you depend on an event being fired whenever a subscription is queued, you can bind your own implementation of Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions.

TypeRegistry does not register duplicates by default

Calling register() on the \Nuwave\Lighthouse\Schema\TypeRegistry now throws when passing a type that was already registered, as this most likely is an error.

If you want to previous behaviour of overwriting existing types, use overwrite() instead.

$typeRegistry = app(\Nuwave\Lighthouse\Schema\TypeRegistry::class);

Mass assignment protection is disabled by default

Since GraphQL constrains allowed inputs by design, mass assignment protection is not needed. By default, Lighthouse will use forceFill() when populating a model with arguments in mutation directives. This allows you to use mass assignment protection for other cases where it is actually useful.

If you need to revert to the old behavior of using fill(), you can change your lighthouse.php:

-   'force_fill' => true,
+   'force_fill' => false,

Replace ErrorBuffer with ErrorPool

Collecting partial errors is now done through the singleton \Nuwave\Lighthouse\Execution\ErrorPool instead of \Nuwave\Lighthouse\Execution\ErrorBuffer:

try {
    // Something that might fail but still allows for a partial result
} catch (\Throwable $error) {
    $errorPool = app(\Nuwave\Lighthouse\Execution\ErrorPool::class);

return $result;

Use native TestResponse::json()

The TestResponse::jsonGet() mixin was removed in favor of the ->json() method, natively supported by Laravel starting from version 5.6.

$response = $this->graphQL(...);

Use GraphQL\Language\Parser instead of Nuwave\Lighthouse\Schema\AST\PartialParser

The native parser from webonyx/graphql-php now supports partial parsing.

-use Nuwave\Lighthouse\Schema\AST\PartialParser;
+use GraphQL\Language\Parser;

Most methods work the same:

-PartialParser::directive(/** @lang GraphQL */ '@deferrable')
+Parser::constDirective(/** @lang GraphQL */ '@deferrable')

A few are different:


-PartialParser::inputValueDefinitions([$foo, $bar]);

Add method defaultHasOperator to \Nuwave\Lighthouse\WhereConditions\Operator

Since the addition of the HAS input in whereCondition mechanics, there has to be a default operator for the HAS input.

If you implement your own custom operator, implement defaultHasOperator. For example, this is the implementation of the default \Nuwave\Lighthouse\WhereConditions\SQLOperator:

public function defaultHasOperator(): string
    return 'GTE';

Change ErrorHandler method handle()

If you implemented your own error handler, change it like this:

use Nuwave\Lighthouse\Execution\ErrorHandler;

class ExtensionErrorHandler implements ErrorHandler
-   public static function handle(Error $error, Closure $next): array
+   public function __invoke(?Error $error, Closure $next): ?array

You can now discard errors by returning null from the handler.

Upgrade to mll-lab/graphql-php-scalars v4

If you use complex where condition directives, such as @whereConditions, upgrade mll-lab/graphql-php-scalars to v4:

composer require mll-lab/graphql-php-scalars:^4