diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 03da633ad2..6e8b03e4ac 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -32,6 +32,7 @@ jobs: - "^9" - "^10" - "^11" + - "^12" composer: - name: lowest arg: "--prefer-lowest --prefer-stable" @@ -44,6 +45,10 @@ jobs: laravel-version: "^11" - php-version: "8.1" laravel-version: "^11" + - php-version: "8.0" + laravel-version: "^12" + - php-version: "8.1" + laravel-version: "^12" steps: - uses: actions/checkout@v4 @@ -63,7 +68,8 @@ 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' + - name: "Remove Pennant for Laravel 9 because it is not compatible" + if: matrix.laravel-version == '^9' run: composer remove --dev --no-update laravel/pennant - run: > @@ -94,6 +100,7 @@ jobs: - "^9" - "^10" - "^11" + - "^12" os: - ubuntu-latest composer: @@ -108,6 +115,10 @@ jobs: laravel-version: "^11" - php-version: "8.1" laravel-version: "^11" + - php-version: "8.0" + laravel-version: "^12" + - php-version: "8.1" + laravel-version: "^12" services: mysql: @@ -142,7 +153,8 @@ jobs: - name: "Remove conflicting dependencies that are not needed here" run: composer remove --dev --no-update larastan/larastan phpstan/phpstan-mockery phpbench/phpbench rector/rector - - if: matrix.laravel-version != '^10' && matrix.laravel-version != '^11' + - name: "Remove Pennant for Laravel 9 because it is not compatible" + if: matrix.laravel-version == '^9' run: composer remove --dev --no-update laravel/pennant - run: > @@ -162,7 +174,7 @@ jobs: strategy: matrix: php-version: ["8.4"] - laravel-version: ["^11"] + laravel-version: ["^12"] services: mysql: @@ -209,7 +221,7 @@ jobs: strategy: matrix: php-version: ["8.4"] - laravel-version: ["^11"] + laravel-version: ["^12"] steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5c3df94d..4011b57ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Added + +- Support Laravel 12 https://github.com/nuwave/lighthouse/pull/2665 + ## v6.53.0 ### Added diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php index f3b88c1233..74e9e4667f 100644 --- a/benchmarks/QueryBench.php +++ b/benchmarks/QueryBench.php @@ -11,7 +11,7 @@ abstract class QueryBench extends TestCase /** Cached graphQL endpoint. */ protected string $graphQLEndpoint; - public function __construct() + public function __construct() // @phpstan-ignore method.parentMethodFinalByPhpDoc (yeah this is hacky) { parent::__construct(static::class); } diff --git a/composer.json b/composer.json index 1d273823d7..0e593620bf 100644 --- a/composer.json +++ b/composer.json @@ -29,15 +29,15 @@ "php": "^8", "ext-json": "*", "haydenpierce/class-finder": "^0.4 || ^0.5", - "illuminate/auth": "^9 || ^10 || ^11", - "illuminate/bus": "^9 || ^10 || ^11", - "illuminate/contracts": "^9 || ^10 || ^11", - "illuminate/http": "^9 || ^10 || ^11", - "illuminate/pagination": "^9 || ^10 || ^11", - "illuminate/queue": "^9 || ^10 || ^11", - "illuminate/routing": "^9 || ^10 || ^11", - "illuminate/support": "^9 || ^10 || ^11", - "illuminate/validation": "^9 || ^10 || ^11", + "illuminate/auth": "^9 || ^10 || ^11 || ^12", + "illuminate/bus": "^9 || ^10 || ^11 || ^12", + "illuminate/contracts": "^9 || ^10 || ^11 || ^12", + "illuminate/http": "^9 || ^10 || ^11 || ^12", + "illuminate/pagination": "^9 || ^10 || ^11 || ^12", + "illuminate/queue": "^9 || ^10 || ^11 || ^12", + "illuminate/routing": "^9 || ^10 || ^11 || ^12", + "illuminate/support": "^9 || ^10 || ^11 || ^12", + "illuminate/validation": "^9 || ^10 || ^11 || ^12", "laragraph/utils": "^1.5 || ^2", "thecodingmachine/safe": "^1 || ^2 || ^3", "webonyx/graphql-php": "^15" @@ -45,21 +45,21 @@ "require-dev": { "algolia/algoliasearch-client-php": "^3", "bensampo/laravel-enum": "^5 || ^6", - "dms/phpunit-arraysubset-asserts": "^0.4 || ^0.5", + "dms/phpunit-arraysubset-asserts": "^0.4 || ^0.5 || dev-add-phpunit-11-support", "ergebnis/composer-normalize": "^2.2.2", "fakerphp/faker": "^1.21", "google/protobuf": "^3.21", "larastan/larastan": "^2.9.14 || ^3.0.4", - "laravel/framework": "^9 || ^10 || ^11", + "laravel/framework": "^9 || ^10 || ^11 || ^12", "laravel/legacy-factories": "^1.1.1", "laravel/pennant": "^1", - "laravel/scout": "^8 || ^9 || ^10 || ^11", + "laravel/scout": "^8 || ^9 || ^10", "mattiasgeniar/phpunit-query-count-assertions": "^1.1", "mll-lab/graphql-php-scalars": "^6", "mll-lab/php-cs-fixer-config": "^5", "mockery/mockery": "^1.5", - "nesbot/carbon": "^2.62.1", - "orchestra/testbench": "^7.50 || ^8.32 || ^9.10", + "nesbot/carbon": "^2.62.1 || ^3.8.4", + "orchestra/testbench": "^7.50 || ^8.32 || ^9.10 || ^10.1", "phpbench/phpbench": "^1.2.6", "phpstan/extension-installer": "^1", "phpstan/phpstan": "^1.12.18 || ^2", @@ -80,6 +80,12 @@ "mll-lab/laravel-graphiql": "A graphical interactive in-browser GraphQL IDE - integrated with Laravel", "pusher/pusher-php-server": "Required when using the Pusher Subscriptions driver" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/pieterocp/phpunit-arraysubset-asserts" + } + ], "minimum-stability": "dev", "prefer-stable": true, "autoload": { diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php new file mode 100644 index 0000000000..37bc24908e --- /dev/null +++ b/phpstan-bootstrap.php @@ -0,0 +1,6 @@ + false]); diff --git a/phpstan.neon b/phpstan.neon index 3b204a2060..028c1471f2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,7 @@ parameters: level: 8 # TODO level up to max + bootstrapFiles: + - phpstan-bootstrap.php stubFiles: - _ide_helper.php paths: @@ -34,7 +36,7 @@ parameters: - path: tests/database/factories message: '#Variable \$factory might not be defined#' - # Mixins are magical + # Mixins magically rebind $this to be something else and do other funky stuff - path: src/Testing/TestResponseMixin.php message: '#Method Nuwave\\Lighthouse\\Testing\\TestResponseMixin::assertGraphQLErrorMessage\(\) invoked with 1 parameter, 0 required\.#' - path: src/Testing/TestResponseMixin.php @@ -42,18 +44,24 @@ parameters: - path: src/Testing/TestResponseMixin.php message: '#Anonymous function should return Illuminate\\Testing\\TestResponse but returns .*#' + # Recognition of mixin methods is broken in some PHPStan/Larastan versions + - '#Call to an undefined method Illuminate\\Testing\\TestResponse::(assertGraphQLValidationError|assertGraphQLValidationKeys|assertGraphQLValidationPasses|assertGraphQLError|assertGraphQLErrorMessage|assertGraphQLDebugMessage|assertGraphQLErrorFree|assertGraphQLSubscriptionAuthorized|assertGraphQLSubscriptionNotAuthorized|graphQLSubscriptionMock|graphQLSubscriptionChannelName|assertGraphQLBroadcasted|assertGraphQLNotBroadcasted)\(\)\.#' + # Relation forwards calls to Builder - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder|Illuminate\\Database\\Eloquent\\Relations\\Relation|Illuminate\\Database\\Query\\Builder::(orderBy|where|whereIn|whereNotIn|whereBetween|whereJsonContains|whereNotBetween)\(\)\.#' # Laravel 11 changes a lot of generics - '#generic class (Illuminate\\Database\\Eloquent\\Builder|Laravel\\Scout\\Builder)( but)? does not specify its types#' - '#contains generic type Illuminate\\Testing\\TestResponse<.+> but class Illuminate\\Testing\\TestResponse is not generic\.#' - - '#Generic type Illuminate\\Database\\Eloquent\\Relations\\(HasOne|HasMany|BelongsToMany|MorphOne|MorphMany|MorphToMany)<.+, .+> in PHPDoc tag @return specifies 2 template types, but class Illuminate\\Database\\Eloquent\\Relations\\\1 supports only 1: TRelatedModel#' + - '#Generic type Illuminate\\Database\\Eloquent\\Relations\\(HasOne|HasMany|BelongsToMany|MorphOne|MorphMany|MorphToMany)<.+> in PHPDoc tag @return specifies \d template types, but class Illuminate\\Database\\Eloquent\\Relations\\\1 supports only \d: .+#' - '#Method .+ should return Illuminate\\Database\\Eloquent\\Relations\\(HasOne|HasMany|BelongsToMany|MorphOne|MorphMany|MorphToMany)<(.+), .+> but returns Illuminate\\Database\\Eloquent\\Relations\\\1<\2>.#' - '#Method .+ should return Illuminate\\Database\\Eloquent\\Relations\\(BelongsTo|MorphTo)<(.+), \$this\((.+)\)> but returns Illuminate\\Database\\Eloquent\\Relations\\\1<\2, \3>.#' - '#Generic type Illuminate\\Database\\Eloquent\\Relations\\(HasOneThrough|HasManyThrough)<.+, .+, .+> in PHPDoc tag @return specifies 3 template types, but class Illuminate\\Database\\Eloquent\\Relations\\\1 supports only 1: TRelatedModel#' - '#Method .+ should return Illuminate\\Database\\Eloquent\\Relations\\(HasOneThrough|HasManyThrough)<(.+), .+> but returns Illuminate\\Database\\Eloquent\\Relations\\\1<\2>.#' + # Different between PHPUnit versions + - '#PHPDoc tag @return contains generic type PHPUnit\\Framework\\MockObject\\Builder\\InvocationMocker but class PHPUnit\\Framework\\MockObject\\Builder\\InvocationMocker is not generic.#' + # This test cheats and uses reflection to make assertions - path: tests/Unit/Schema/Directives/BaseDirectiveTest.php message: '#Call to protected method getModelClass\(\) of class Nuwave\\Lighthouse\\Schema\\Directives\\BaseDirective\.#' @@ -61,6 +69,9 @@ parameters: # This is a library, so it should be extendable - '#Unsafe usage of new static.*#' + # Possible footgun, but people who extend Lighthouse classes should know what they are doing + - '#Cannot unset property .+ because it might have hooks in a subclass\.#' + # Ease transition for non-nullable properties towards native types https://github.com/phpstan/phpstan/issues/5150 - '#Property .* in isset\(\) is not nullable\.#' diff --git a/src/Console/FieldCommand.php b/src/Console/FieldCommand.php index b25b457cef..58e0bd8159 100644 --- a/src/Console/FieldCommand.php +++ b/src/Console/FieldCommand.php @@ -37,6 +37,9 @@ protected function getDefaultNamespace($rootNamespace): string protected function nameParts(): array { $name = $this->argument('name'); + if (! is_string($name)) { + throw new \InvalidArgumentException('You must specify the name for the class to generate.'); + } $parts = explode('.', $name); if (count($parts) !== 2) { diff --git a/src/Console/LighthouseGeneratorCommand.php b/src/Console/LighthouseGeneratorCommand.php index 0bad1f0b5d..edd2527a40 100644 --- a/src/Console/LighthouseGeneratorCommand.php +++ b/src/Console/LighthouseGeneratorCommand.php @@ -16,6 +16,9 @@ abstract class LighthouseGeneratorCommand extends GeneratorCommand protected function getNameInput(): string { $name = $this->argument('name'); + if (! is_string($name)) { + throw new \InvalidArgumentException('You must specify the name for the class to generate.'); + } return ucfirst(trim($name)); } diff --git a/src/GraphQL.php b/src/GraphQL.php index b5938791c2..1cf90c1ebc 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -161,9 +161,9 @@ public function executeParsedQueryRaw( $queryComplexityRule = $validationRules[QueryComplexity::class] ?? null; $queryComplexity = $queryComplexityRule instanceof QueryComplexity // TODO remove this check when updating the required version of webonyx/graphql-php - && method_exists($queryComplexityRule, 'getQueryComplexity') - ? $queryComplexityRule->getQueryComplexity() - : null; + && method_exists($queryComplexityRule, 'getQueryComplexity') // @phpstan-ignore function.alreadyNarrowedType (depends on the used library version) + ? $queryComplexityRule->getQueryComplexity() + : null; /** @var array<\Nuwave\Lighthouse\Execution\ExtensionsResponse|null> $extensionsResponses */ $extensionsResponses = (array) $this->eventDispatcher->dispatch( @@ -404,13 +404,13 @@ protected function validateCacheableRules( } if ($queryHash === null) { - return DocumentValidator::validate($schema, $query, $validationRules); + return DocumentValidator::validate($schema, $query, $validationRules); // @phpstan-ignore return.type (TODO remove ignore when requiring a newer version of webonyx/graphql-php) } $cacheConfig = $this->configRepository->get('lighthouse.validation_cache'); if (! isset($cacheConfig['enable']) || ! $cacheConfig['enable']) { - return DocumentValidator::validate($schema, $query, $validationRules); + return DocumentValidator::validate($schema, $query, $validationRules); // @phpstan-ignore return.type (TODO remove ignore when requiring a newer version of webonyx/graphql-php) } $cacheKey = "lighthouse:validation:{$schemaHash}:{$queryHash}"; @@ -429,7 +429,7 @@ protected function validateCacheableRules( // As of webonyx/graphql-php 15.14.0, GraphQL\Error\Error is not serializable. // We would have to figure out how to serialize them properly to cache them. if ($result !== []) { - return $result; + return $result; // @phpstan-ignore return.type (TODO remove ignore when requiring a newer version of webonyx/graphql-php) } $store->put($cacheKey, $result, $cacheConfig['ttl']); diff --git a/src/Schema/AST/ASTCache.php b/src/Schema/AST/ASTCache.php index 867f877f1b..12ac3ba37a 100644 --- a/src/Schema/AST/ASTCache.php +++ b/src/Schema/AST/ASTCache.php @@ -9,8 +9,8 @@ /** * @phpstan-type CacheConfig array{ - * enable: bool, - * path: string|null, + * enable: bool|int, + * path?: ?string, * } * * @phpstan-import-type SerializableDocumentAST from DocumentAST diff --git a/src/Schema/Directives/RelationDirectiveHelpers.php b/src/Schema/Directives/RelationDirectiveHelpers.php index d541f4f752..b86d0df16e 100644 --- a/src/Schema/Directives/RelationDirectiveHelpers.php +++ b/src/Schema/Directives/RelationDirectiveHelpers.php @@ -28,7 +28,7 @@ protected function relation(): string /** * @param array $args * - * @return \Closure(QueryBuilder|\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model>, ?mixed = null): void + * @return \Closure(\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model>, mixed=): void */ protected function makeBuilderDecorator(mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): \Closure { diff --git a/src/Schema/Types/Scalars/Date.php b/src/Schema/Types/Scalars/Date.php index 806eedf662..701e9ded3e 100644 --- a/src/Schema/Types/Scalars/Date.php +++ b/src/Schema/Types/Scalars/Date.php @@ -11,7 +11,7 @@ protected function format(Carbon $carbon): string return $carbon->toDateString(); } - protected function parse(mixed $value): Carbon + protected function parse(string $value): Carbon { // @phpstan-ignore-next-line We know the format to be good, so this can never return `false` return Carbon::createFromFormat('Y-m-d', $value)->startOfDay(); diff --git a/src/Schema/Types/Scalars/DateScalar.php b/src/Schema/Types/Scalars/DateScalar.php index 2b07a07c08..a42875cf7f 100644 --- a/src/Schema/Types/Scalars/DateScalar.php +++ b/src/Schema/Types/Scalars/DateScalar.php @@ -80,6 +80,10 @@ protected function tryParsingDate(mixed $value, string $exceptionClass): Illumin } } + if (! is_string($value)) { + throw new $exceptionClass('Query error: Can only parse strings.'); + } + return $this->parse($value); } catch (\Exception $exception) { throw new $exceptionClass($exception->getMessage()); @@ -92,7 +96,7 @@ abstract protected function format(IlluminateCarbon $carbon): string; /** * Try turning a client value into a Carbon instance. * - * @param mixed $value a possibly faulty client value + * @param string $value a possibly faulty client value */ - abstract protected function parse(mixed $value): IlluminateCarbon; + abstract protected function parse(string $value): IlluminateCarbon; } diff --git a/src/Schema/Types/Scalars/DateTime.php b/src/Schema/Types/Scalars/DateTime.php index 4b22ae82f3..4b3308c325 100644 --- a/src/Schema/Types/Scalars/DateTime.php +++ b/src/Schema/Types/Scalars/DateTime.php @@ -11,7 +11,7 @@ protected function format(Carbon $carbon): string return $carbon->toDateTimeString(); } - protected function parse(mixed $value): Carbon + protected function parse(string $value): Carbon { // @phpstan-ignore-next-line We know the format to be good, so this can never return `false` return Carbon::createFromFormat(Carbon::DEFAULT_TO_STRING_FORMAT, $value); diff --git a/src/Schema/Types/Scalars/DateTimeTz.php b/src/Schema/Types/Scalars/DateTimeTz.php index 34491bdf58..2b129db9bc 100644 --- a/src/Schema/Types/Scalars/DateTimeTz.php +++ b/src/Schema/Types/Scalars/DateTimeTz.php @@ -11,7 +11,7 @@ protected function format(Carbon $carbon): string return $carbon->toIso8601String(); } - protected function parse(mixed $value): Carbon + protected function parse(string $value): Carbon { // @phpstan-ignore-next-line We know the format to be good, so this can never return `false` return Carbon::createFromFormat( diff --git a/src/Schema/Types/Scalars/DateTimeUtc.php b/src/Schema/Types/Scalars/DateTimeUtc.php index 53db7c456f..64f55a2908 100644 --- a/src/Schema/Types/Scalars/DateTimeUtc.php +++ b/src/Schema/Types/Scalars/DateTimeUtc.php @@ -5,7 +5,7 @@ use Illuminate\Support\Carbon; /** - * Only works with Carbon 2. + * Works with Carbon 2.x & 3.x. */ class DateTimeUtc extends DateScalar { @@ -14,7 +14,7 @@ protected function format(Carbon $carbon): string return $carbon->toJSON(); } - protected function parse(mixed $value): Carbon + protected function parse(string $value): Carbon { // @phpstan-ignore-next-line We know the format to be good, so this can never return `false` return Carbon::createFromIsoFormat('YYYY-MM-DDTHH:mm:ss.SSSSSSZ', $value); diff --git a/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php b/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php index 418349a44d..b1a13f8b34 100644 --- a/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php +++ b/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php @@ -47,7 +47,7 @@ public function testDefaultComplexity(): void $this->assertCount(1, $events); // TODO remove this check when updating the required version of webonyx/graphql-php - if (method_exists(QueryComplexity::class, 'getQueryComplexity')) { + if (method_exists(QueryComplexity::class, 'getQueryComplexity')) { // @phpstan-ignore function.alreadyNarrowedType (depends on the used library version) $event = $events[0]; $this->assertSame(2, $event->queryComplexity); } diff --git a/tests/Unit/Subscriptions/Storage/RedisStorageManagerTest.php b/tests/Unit/Subscriptions/Storage/RedisStorageManagerTest.php index b76653f156..f0a5ed0e8f 100644 --- a/tests/Unit/Subscriptions/Storage/RedisStorageManagerTest.php +++ b/tests/Unit/Subscriptions/Storage/RedisStorageManagerTest.php @@ -25,7 +25,7 @@ final class RedisStorageManagerTest extends TestCase * @param array $firstCallArguments * @param array ...$consecutiveCallsArguments * - * @return iterable<\PHPUnit\Framework\Constraint\Callback> + * @return iterable> */ private function withConsecutive(array $firstCallArguments, array ...$consecutiveCallsArguments): iterable { diff --git a/tests/Utils/Models/Category.php b/tests/Utils/Models/Category.php index 6addd916bd..6f3a2bad7d 100644 --- a/tests/Utils/Models/Category.php +++ b/tests/Utils/Models/Category.php @@ -28,7 +28,7 @@ public function parent(): BelongsTo return $this->belongsTo(Category::class, 'parent_id'); } - /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\Post, $this> */ + /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\Post, $this, \Illuminate\Database\Eloquent\Relations\Pivot> */ public function posts(): BelongsToMany { return $this->belongsToMany(Post::class, 'category_post', 'post_id', 'category_id'); diff --git a/tests/Utils/Models/Post.php b/tests/Utils/Models/Post.php index a27497ed32..6fb42e6aea 100644 --- a/tests/Utils/Models/Post.php +++ b/tests/Utils/Models/Post.php @@ -64,7 +64,7 @@ public function activity(): MorphMany return $this->morphMany(Activity::class, 'content'); } - /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\Category, $this> */ + /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\Category, $this, \Illuminate\Database\Eloquent\Relations\Pivot> */ public function categories(): BelongsToMany { return $this->belongsToMany(Category::class, 'category_post', 'category_id', 'post_id'); diff --git a/tests/Utils/Models/Role.php b/tests/Utils/Models/Role.php index 7b4ed57194..50911fa4ca 100644 --- a/tests/Utils/Models/Role.php +++ b/tests/Utils/Models/Role.php @@ -25,11 +25,10 @@ final class Role extends Model { public $timestamps = false; - /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\User, $this> */ + /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\User, $this, \Illuminate\Database\Eloquent\Relations\Pivot> */ public function users(): BelongsToMany { - return $this - ->belongsToMany(User::class) + return $this->belongsToMany(User::class) ->withPivot('meta'); } diff --git a/tests/Utils/Models/User.php b/tests/Utils/Models/User.php index c3748d3f70..bd636ec83a 100644 --- a/tests/Utils/Models/User.php +++ b/tests/Utils/Models/User.php @@ -103,11 +103,10 @@ public function posts(): HasMany return $this->hasMany(Post::class); } - /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\Role, $this> */ + /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Utils\Models\Role, $this, \Illuminate\Database\Eloquent\Relations\Pivot> */ public function roles(): BelongsToMany { - return $this - ->belongsToMany(Role::class) + return $this->belongsToMany(Role::class) ->withPivot('meta'); }