定制 hmennen90/laravel-graphql 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

hmennen90/laravel-graphql

Composer 安装命令:

composer require hmennen90/laravel-graphql

包简介

A hand-written GraphQL engine with first-class Laravel integration (code-first + SDL).

README 文档

README

CI Latest Version PHP Version Total Downloads License: MIT

A hand-written GraphQL engine with first-class Laravel integration — no dependency on webonyx/graphql-php. Define your schema code-first (PHP), schema-first (SDL), attribute-driven, or mix all three; they compile to one internal schema.

Status: in active development. APIs may change before the first stable release.

Key differentiator — single source of truth

Unlike SDL-first stacks, you do not implement a type twice. There is no SDL type mirroring your Eloquent model plus a separate type/transformer class, and no directive DSL to keep in sync. Declare a type once; resolvers are plain PHP callables that read your models directly.

Features

  • Own lexer, parser, AST, type system, validator and executor (spec-driven).
  • Hybrid schema: code-first, SDL, and PHP attributes — one internal schema.
  • Built-in scalars (Int, Float, String, Boolean, ID) + custom scalars.
  • Objects, interfaces, unions, enums, input objects, lists, non-null.
  • Comprehensive validation, introspection (GraphiQL/Apollo tooling), @oneOf, @specifiedBy.
  • Custom directives (runtime middleware and build-time SDL), SDL type extensions.
  • Eloquent directive layer (@all, @find, @paginate, @hasMany, @whereConditions, @orderBy, @create/@update/@delete, @search, …) — available as SDL directives and equivalent PHP attributes (#[All], #[Paginate]).
  • Argument sanitisers & validation: @trim, @hash, @globalId, @rules, @validator.
  • DataLoader (N+1 batching), query depth/complexity limits.
  • Laravel: HTTP endpoint, batching, middleware/auth, error masking, GraphiQL, file uploads, Automatic Persisted Queries, @cacheControl HTTP caching, Relay pagination, subscriptions (broadcasting + graphql-ws), Apollo Federation.
  • PHP 8.4, PHPStan level 10, tested with orchestra/testbench.

Requirements

  • PHP ^8.4
  • Laravel 11, 12 or 13 (for the Laravel integration; the engine itself is framework-agnostic)

Installation

composer require hmennen90/laravel-graphql

Publish the config (and optionally a starter schema):

php artisan vendor:publish --tag=graphql-config

Quick start (Laravel)

Create a schema provider:

<?php

namespace App\GraphQL;

use App\Models\User;
use Hmennen90\GraphQL\Contracts\ProvidesSchema;
use Hmennen90\GraphQL\Engine\Schema\Schema;
use Hmennen90\GraphQL\Engine\Schema\SchemaConfig;
use Hmennen90\GraphQL\Engine\Type\Definition\Argument;
use Hmennen90\GraphQL\Engine\Type\Definition\FieldDefinition;
use Hmennen90\GraphQL\Engine\Type\Definition\ObjectType;
use Hmennen90\GraphQL\Engine\Type\Definition\Type;

final class AppSchema implements ProvidesSchema
{
    public function schema(): Schema
    {
        $user = new ObjectType('User', [
            FieldDefinition::make('id', Type::nonNull(Type::id())),
            FieldDefinition::make('name', Type::string()),
            FieldDefinition::make('email', Type::string()),
        ]);

        $query = new ObjectType('Query', [
            FieldDefinition::make(
                'user',
                $user,
                args: [Argument::make('id', Type::nonNull(Type::id()))],
                resolve: fn ($root, array $args) => User::find($args['id']),
            ),
        ]);

        return new Schema(new SchemaConfig(query: $query));
    }
}

Register it in config/graphql.php:

'schema' => [
    'factory' => App\GraphQL\AppSchema::class,
],

Query the endpoint:

curl -X POST http://localhost/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ user(id: \"1\") { id name } }"}'
{ "data": { "user": { "id": "1", "name": "Ada" } } }

Defining a schema

Code-first

use Hmennen90\GraphQL\Engine\Type\Definition\{Argument, EnumType, EnumValueDefinition,
    FieldDefinition, InterfaceType, ObjectType, Type, UnionType};

$status = new EnumType('Status', [
    new EnumValueDefinition('ACTIVE'),
    new EnumValueDefinition('ARCHIVED', deprecationReason: 'Use ACTIVE'),
]);

$user = new ObjectType('User', [
    FieldDefinition::make('id', Type::nonNull(Type::id())),
    FieldDefinition::make('name', Type::string()),
    FieldDefinition::make('status', Type::nonNull($status)),
    // Lists & non-null wrappers:
    FieldDefinition::make('tags', Type::listOf(Type::nonNull(Type::string()))),
]);

Recursive/cyclic types are fine — pass a closure for lazily-resolved fields:

$node = null;
$node = new ObjectType('Node', fn (): array => [
    FieldDefinition::make('id', Type::nonNull(Type::id())),
    FieldDefinition::make('parent', $node),
]);

Schema-first (SDL)

use Hmennen90\GraphQL\Engine\Building\SchemaFirst\SchemaBuilder;

$sdl = <<<'GRAPHQL'
type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  name: String
}
GRAPHQL;

$schema = SchemaBuilder::fromSdl($sdl, resolvers: [
    'Query' => [
        'user' => fn ($root, array $args) => User::find($args['id']),
    ],
]);

Fields without a resolver fall back to reading array keys, object properties or getters.

Attribute-driven

use Hmennen90\GraphQL\Engine\Building\CodeFirst\Attributes\GraphQLField;
use Hmennen90\GraphQL\Engine\Building\CodeFirst\Attributes\GraphQLType;
use Hmennen90\GraphQL\Engine\Building\CodeFirst\AttributeSchemaBuilder;
use Hmennen90\GraphQL\Engine\Schema\Schema;
use Hmennen90\GraphQL\Engine\Schema\SchemaConfig;

#[GraphQLType(name: 'Query')]
final class QueryType
{
    #[GraphQLField(type: 'String!')]
    public function hello(): string
    {
        return 'world';
    }

    #[GraphQLField(type: '[User!]!')]
    public function users(): array
    {
        return User::all()->all();
    }
}

$types = (new AttributeSchemaBuilder())->build([QueryType::class]);
$schema = new Schema(new SchemaConfig(query: $types['Query']));

SDL type extensions

type Query { hello: String! }
extend type Query { world: String! }

Eloquent directives (CRUD without resolvers)

Build a full CRUD API over Eloquent declaratively — the model stays the single source of truth, and directives derive queries, columns and relations from it.

type Query {
  users: [User!]! @all
  user(id: ID!): User @find
  posts: [Post!]! @paginate @whereConditions(columns: ["title"]) @orderBy(columns: ["id"])
}

type Mutation {
  createUser(name: String!, email: String!): User @create
  updateUser(id: ID!, name: String): User @update
  deleteUser(id: ID!): User @delete
}

type User {
  id: ID!
  name: String
  posts: [Post!]! @hasMany
  postsCount: Int @count(relation: "posts")
}

Everything is also available as PHP attributes that dispatch to the exact same implementations — pick SDL or code-first per taste, with no duplicated logic:

use Hmennen90\GraphQL\Attributes\{All, Paginate, Guard};

#[GraphQLType(name: 'Query')]
final class QueryType
{
    #[GraphQLField(type: '[User!]!')]
    #[All(model: User::class)]
    public function users(): array { return []; }

    #[GraphQLField(type: '[Post!]!')]
    #[Paginate(type: 'CONNECTION')]
    #[Guard]
    public function posts(): array { return []; }
}

Reading (@all/@find/@first/@paginate), relations (@hasMany/@hasOne/@belongsTo/@belongsToMany/@morph*/@count), filtering & sorting (@whereConditions/@orderBy), mutations incl. nested (@create/@update/@delete/@upsert), auth/utility (@guard/@inject/@field/@rename) and Laravel Scout (@search) are all supported. See Eloquent directives for the full reference.

Apollo Federation

Expose any schema as a federated subgraph:

use Hmennen90\GraphQL\Federation\Federation;

$subgraph = Federation::subgraph($schema, [
    'User' => [
        'model'   => \App\Models\User::class,
        'resolve' => fn (array $ref) => \App\Models\User::find($ref['id']),
    ],
]);

This adds _service { sdl }, _entities(representations:) and the _Any/_Service/ _Entity types, wiring one reference resolver per entity type.

Generating types from your Laravel app

The strongest form of "single source of truth": derive GraphQL types from the artifacts you already maintain — Eloquent models, FormRequest rules and JSON responses — instead of re-declaring their shape.

use Hmennen90\GraphQL\Generation\ModelTypeGenerator;
use Hmennen90\GraphQL\Generation\ValidationInputGenerator;
use Hmennen90\GraphQL\Generation\ResponseTypeGenerator;

// 1. Object type from an Eloquent model (primary key, fillable, casts, timestamps)
$userType = (new ModelTypeGenerator())->fromModel(\App\Models\User::class);
//   -> type User { id: ID!  name: String  active: Boolean  meta: JSON  created_at: String ... }

// 2. Input type from a FormRequest's validation rules ("required" -> non-null)
$createUserInput = (new ValidationInputGenerator())
    ->fromRequest(\App\Http\Requests\StoreUserRequest::class, 'CreateUserInput');
//   from ['name' => 'required|string', 'age' => 'integer'] -> input CreateUserInput { name: String!  age: Int }

// ...or straight from a rules array:
$filterInput = (new ValidationInputGenerator())->fromRules([
    'term' => 'required|string',
    'limit' => 'integer',
], 'FilterInput');

// 3. Object type inferred from a JSON resource / response shape
$sample = (new \App\Http\Resources\UserResource($user))->toArray(request());
$userResourceType = (new ResponseTypeGenerator())->fromArray($sample, 'UserResource');

Compose the generated types into a schema like any hand-built type:

use Hmennen90\GraphQL\Engine\Schema\Schema;
use Hmennen90\GraphQL\Engine\Schema\SchemaConfig;
use Hmennen90\GraphQL\Engine\Type\Definition\{Argument, FieldDefinition, ObjectType, Type};

$query = new ObjectType('Query', [
    FieldDefinition::make('user', $userType,
        args: [Argument::make('id', Type::nonNull(Type::id()))],
        resolve: fn ($root, array $args) => \App\Models\User::find($args['id'])),
]);

$mutation = new ObjectType('Mutation', [
    FieldDefinition::make('createUser', $userType,
        args: [Argument::make('input', Type::nonNull($createUserInput))],
        resolve: fn ($root, array $args) => \App\Models\User::create($args['input'])),
]);

$schema = new Schema(new SchemaConfig(query: $query, mutation: $mutation, types: [$userResourceType]));

Mapping notes: model casts and rule tokens map to the built-in scalars; array/json casts and array rules use a bundled JSON scalar. Nested resource arrays become nested object types. Generators produce a starting point you can refine — add relations, hide fields, or wrap the returned types as needed.

Executing standalone (without Laravel)

The engine has no framework dependency:

use Hmennen90\GraphQL\Engine\Executor\Executor;
use Hmennen90\GraphQL\Engine\Language\Parser;

$result = Executor::execute($schema, Parser::parse('{ user(id: "1") { id name } }'));

$result->toArray(); // ['data' => ['user' => ['id' => '1', 'name' => 'Ada']]]

Or validate first:

use Hmennen90\GraphQL\Engine\Validation\DocumentValidator;

$errors = DocumentValidator::validate($schema, Parser::parse($query), [
    'maxDepth' => 10,
    'maxComplexity' => 100,
]);

Laravel integration

Configuration

config/graphql.php controls the endpoint, GraphiQL, schema source, batching, error handling, security limits, persisted queries, cache-control and subscriptions.

The GraphQL facade

use Hmennen90\GraphQL\Facades\GraphQL;

$result = GraphQL::execute('{ user(id: "1") { name } }');
$schema = GraphQL::schema();

Authorization

Inside a resolver, use the request Context:

use Hmennen90\GraphQL\Execution\Context;

FieldDefinition::make('secret', Type::string(), resolve: function ($root, array $args, $context) {
    if ($context instanceof Context) {
        $context->authorize('view-secret'); // throws AuthorizationError on deny
    }

    return 'classified';
});

Or declaratively in SDL with the @can directive:

use Hmennen90\GraphQL\Directives\CanDirective;

$schema = SchemaBuilder::fromSdl(<<<'GRAPHQL'
    directive @can(ability: String!) on FIELD_DEFINITION
    type Query {
      secret: String @can(ability: "view-secret")
    }
GRAPHQL, resolvers: [/* ... */], schemaDirectives: ['can' => new CanDirective()]);

Argument validation

Use Laravel's validator inside a resolver; a thrown ValidationException surfaces under errors[].extensions.validation:

FieldDefinition::make('register', $user, args: [
    Argument::make('email', Type::nonNull(Type::string())),
], resolve: function ($root, array $args) {
    validator($args, ['email' => 'required|email'])->validate();

    return User::create($args);
});

Or declaratively in SDL with the built-in directives — @rules validates a single argument, @validator binds a dedicated validator class, and the sanitisers run before the resolver:

type Mutation {
  register(
    email: String @rules(apply: ["required", "email"])
    name: String @trim
    password: String @hash
  ): User @create

  updatePost(id: ID @globalId, title: String): Post
    @update
    @validator(class: "App\\GraphQL\\Validators\\UpdatePostValidator")
}
  • @rules(apply: [...]) — validate one argument with Laravel rules.
  • @validator(class:) — validate all arguments via a class exposing rules().
  • @trim — strip surrounding whitespace; @hash — bcrypt a value; @globalId — decode a Relay global id down to its raw key.

Error masking

With graphql.debug = false, internal exception messages are masked to "Internal server error". Client-safe exceptions (authorization, authentication, validation) pass through and are categorised under extensions.category.

File uploads

Add the Upload scalar and send a GraphQL multipart request:

use Hmennen90\GraphQL\Support\UploadType;

$upload = UploadType::make();

FieldDefinition::make('import', Type::string(), args: [
    Argument::make('file', Type::nonNull($upload)),
], resolve: fn ($root, array $args) => $args['file']->getClientOriginalName());

Uploaded files arrive as Illuminate\Http\UploadedFile instances.

Automatic Persisted Queries (APQ)

// config/graphql.php
'persisted_queries' => ['enabled' => true],

Clients send extensions.persistedQuery.sha256Hash; the first request registers the query, later requests may send the hash alone.

HTTP caching (@cacheControl)

// config/graphql.php
'cache_control' => ['enabled' => true],
directive @cacheControl(maxAge: Int, scope: String) on FIELD_DEFINITION

type Query {
  articles: [Article!]! @cacheControl(maxAge: 60, scope: "PUBLIC")
}

The endpoint emits a Cache-Control header from the minimum maxAge of the selected fields. Combined with APQ (GET + hash), this enables CDN/HTTP caching.

Subscriptions

Enable and broadcast events:

// config/graphql.php
'subscriptions' => ['enabled' => true],
use Hmennen90\GraphQL\Subscriptions\SubscriptionManager;

app(SubscriptionManager::class)->broadcast('postAdded', $post);

Clients subscribe via Laravel Echo (broadcasting) or a graphql-ws client. To run the bundled graphql-ws server (requires the Swoole extension):

php artisan graphql:subscriptions:serve --port=9501

Performance

DataLoader (N+1 batching)

use Hmennen90\GraphQL\Engine\Executor\DataLoader;

$companies = new DataLoader(function (array $ids) {
    $byId = Company::findMany($ids)->keyBy('id');

    // must return one value per key, in the same order as $ids
    return array_map(fn ($id) => $byId->get($id), $ids);
});

FieldDefinition::make('company', $companyType,
    resolve: fn (array $user) => $companies->load($user['company_id']));

All companies requested during one query are fetched in a single batch call.

Query limits

// config/graphql.php
'security' => [
    'max_depth' => 15,
    'max_complexity' => 200,
],

Custom directives

Runtime (query) directive — wrap field resolution:

use Closure;
use Hmennen90\GraphQL\Engine\Executor\{DirectiveMiddleware, ResolveInfo};
use Hmennen90\GraphQL\Engine\Language\AST\DirectiveNode;

final class UpperDirective implements DirectiveMiddleware
{
    public function handle(DirectiveNode $node, ResolveInfo $info, Closure $resolve): mixed
    {
        $value = $resolve();

        return is_string($value) ? strtoupper($value) : $value;
    }
}

// register on the schema:
new SchemaConfig(query: $query, directiveMiddleware: ['upper' => new UpperDirective()]);

Build-time SDL directives implement SchemaDirective and are passed to SchemaBuilder::fromSdl(..., schemaDirectives: [...]) (see @can/@cacheControl).

Relay pagination

use Hmennen90\GraphQL\Support\Relay\Relay;

$connection = Relay::connectionType($userType);

FieldDefinition::make('users', $connection, args: [
    Argument::make('first', Type::int()),
    Argument::make('after', Type::string()),
], resolve: fn ($root, array $args) => Relay::connectionFromArray(User::all()->all(), $args));

Introspection & GraphiQL

Full introspection is supported. With graphql.graphiql.enabled = true, an in-browser IDE is served at /graphiql.

Testing

composer test       # PHPUnit (Unit + Feature via orchestra/testbench)
composer analyse    # PHPStan level 10

Contributing

Contributions are welcome — see CONTRIBUTING.md. Commits follow Conventional Commits; releases are cut with semantic-release. Please also read the Code of Conduct.

Security

Please report security issues privately — see SECURITY.md.

Changelog

See CHANGELOG.md (generated by semantic-release).

Comparison

A full, per-package feature comparison (vs. Lighthouse, rebing/graphql-laravel, webonyx/graphql-php) is in the documentation.

License

MIT — see LICENSE.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固