monkeyscloud / monkeyslegion-graphql
Code-first GraphQL server for the MonkeysLegion framework — PHP 8.4 attributes, PSR-15, DataLoader, subscriptions, and security out of the box.
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/monkeyscloud/monkeyslegion-graphql
Requires
- php: ^8.4
- monkeyscloud/monkeyslegion-cli: ^1.0
- monkeyscloud/monkeyslegion-core: ^1.0
- monkeyscloud/monkeyslegion-di: ^1.0
- monkeyscloud/monkeyslegion-mlc: ^1.0
- monkeyscloud/monkeyslegion-router: ^1.0
- nyholm/psr7: ^1.8
- psr/container: ^2.0
- psr/http-message: ^2.0
- psr/http-server-middleware: ^1.0
- psr/simple-cache: ^3.0
- webonyx/graphql-php: ^15.30
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
- squizlabs/php_codesniffer: ^3.11
Suggests
- ext-redis: For Redis-backed PubSub subscriptions
- monkeyscloud/monkeyslegion-auth: For JWT-based resolver authorization and WS authentication
- monkeyscloud/monkeyslegion-cache: For PSR-16 schema caching
- monkeyscloud/monkeyslegion-entity: For entity-to-type auto-mapping
- monkeyscloud/monkeyslegion-files: For file upload handling
- monkeyscloud/monkeyslegion-logger: For structured logging in GraphQL provider
- monkeyscloud/monkeyslegion-query: For repository-backed resolvers
- monkeyscloud/monkeyslegion-queue: For async mutation side-effects
This package is auto-updated.
Last update: 2026-02-11 03:47:23 UTC
README
Code-first GraphQL server for the MonkeysLegion framework — PHP 8.4 attributes, PSR-15, DataLoader, subscriptions, and security out of the box.
Requirements
- PHP 8.4+
webonyx/graphql-php^15.30
Installation
composer require monkeyscloud/monkeyslegion-graphql
The GraphQLProvider is auto-registered via composer.json extra.
Quick Start
1. Define a Type
use MonkeysLegion\GraphQL\Attribute\{Type, Field}; #[Type(description: 'A user')] final class UserType { #[Field] public function id(User $root): int { return $root->id; } #[Field] public function name(User $root): string { return $root->name; } #[Field(description: 'Email address')] public function email(User $root): string { return $root->email; } }
2. Define a Query
use MonkeysLegion\GraphQL\Attribute\{Query, Arg}; use MonkeysLegion\GraphQL\Context\GraphQLContext; #[Query(name: 'user', description: 'Get user by ID')] final class GetUserQuery { public function __construct(private UserRepository $users) {} public function __invoke( mixed $root, #[Arg(description: 'User ID')] int $id, GraphQLContext $context, ): ?User { return $this->users->find($id); } }
3. Define a Mutation
use MonkeysLegion\GraphQL\Attribute\{Mutation, Arg}; #[Mutation(name: 'createUser', description: 'Create a new user')] final class CreateUserMutation { public function __construct(private UserRepository $users) {} public function __invoke( mixed $root, #[Arg] string $name, #[Arg] string $email, ): User { return $this->users->create($name, $email); } }
4. Configure
# config/graphql.mlc graphql: endpoint: /graphql scan: directories: - app/GraphQL security: max_depth: 10 max_complexity: 200
Features
Attributes
| Attribute | Target | Purpose |
|---|---|---|
#[Type] |
Class | GraphQL object type |
#[Field] |
Method/Property | Object type field |
#[Query] |
Class | Root query field |
#[Mutation] |
Class | Root mutation field |
#[Subscription] |
Class | Subscription field |
#[Arg] |
Parameter | Argument metadata |
#[InputType] |
Class | Input object type |
#[Enum] |
Backed enum | Enum type |
#[InterfaceType] |
Class/Interface | Interface type |
#[UnionType] |
Class | Union type |
#[Middleware] |
Class/Method | Per-field middleware |
Custom Scalars
DateTime— ISO 8601 serializationJSON— Arbitrary JSON passthroughEmail— Email format validationURL— URL format validationUpload— Multipart file upload
Security
graphql: security: max_depth: 10 # Query depth limiting max_complexity: 200 # Field cost analysis introspection: false # Disable introspection in production persisted_queries: true # APQ with SHA256 rate_limit: enabled: true max_requests: 100 window_seconds: 60
DataLoader (N+1 Prevention)
use MonkeysLegion\GraphQL\Loader\DataLoader; final class UserLoader extends DataLoader { public function __construct(private UserRepository $users) {} protected function batchLoad(array $keys): array { $users = $this->users->findByIds($keys); return array_map( fn(int $id) => $users[$id] ?? null, $keys, ); } }
Relay Pagination
use MonkeysLegion\GraphQL\Type\ConnectionType; // Automatically creates UserConnection, UserEdge, PageInfo types $connectionType = ConnectionType::create('User', $userType);
Subscriptions
use MonkeysLegion\GraphQL\Attribute\Subscription; #[Subscription(name: 'messageAdded', description: 'New message')] final class MessageAddedSubscription { public function __invoke(mixed $root): Message { return $root; } }
Supports graphql-ws protocol with in-memory and Redis PubSub backends.
File Uploads
Follows the GraphQL multipart request spec:
#[Mutation(name: 'uploadFile')] final class UploadFileMutation { public function __invoke( mixed $root, #[Arg] UploadedFileInterface $file, ): string { $file->moveTo('/uploads/' . $file->getClientFilename()); return $file->getClientFilename(); } }
CLI Commands
| Command | Description |
|---|---|
php ml graphql:schema:dump |
Dump schema as SDL |
php ml graphql:schema:validate |
Validate schema |
php ml graphql:cache:warm |
Warm schema cache |
php ml graphql:cache:clear |
Clear schema cache |
php ml graphql:introspect |
Dump introspection JSON |
Entity Integration
When monkeyslegion-entity is installed, you can auto-map your entities to GraphQL types without writing boilerplate type classes.
Entity Example
use MonkeysLegion\Entity\Attribute\Entity; use MonkeysLegion\Entity\Attribute\Id; use MonkeysLegion\Entity\Attribute\Column; #[Entity(table: 'products')] class Product { #[Id] public int $id; #[Column(type: 'varchar', length: 255)] public string $name; #[Column(type: 'text')] public string $description; #[Column(type: 'decimal')] public float $price; #[Column(type: 'boolean')] public bool $active; #[Column(type: 'datetime')] public \DateTimeImmutable $createdAt; }
Auto-Map Entities to GraphQL Types
use MonkeysLegion\GraphQL\Scanner\EntityTypeMapper; $mapper = new EntityTypeMapper(); // Maps all typed properties → GraphQL fields automatically: // int → Int! float → Float! // string → String! bool → Boolean! // DateTime* → DateTime! (custom scalar) // ?string → String (nullable) $typeConfig = $mapper->map(Product::class); // Returns: ['name' => 'Product', 'fields' => ['id' => ..., 'name' => ..., ...]] // Map multiple entities at once $types = $mapper->mapAll([Product::class, Category::class, Order::class]);
Auto-Generated CRUD Resolvers
Use EntityResolver to expose entities without manual resolver classes:
use MonkeysLegion\GraphQL\Resolver\EntityResolver; use MonkeysLegion\GraphQL\Type\ConnectionType; // Single entity by ID // query { product(id: 42) { name price } } $findProduct = EntityResolver::findById( Product::class, ProductRepository::class, // optional — defaults to Product::class . 'Repository' ); // List all entities // query { products { name price active } } $listProducts = EntityResolver::findAll(Product::class); // Relay-style pagination with cursors // query { products(first: 10, after: "Y3Vyc29yOjk=") { // edges { node { name } cursor } // pageInfo { hasNextPage endCursor } // totalCount // }} $paginatedProducts = EntityResolver::connection(Product::class);
Full Example: Entity-Backed Schema
use MonkeysLegion\GraphQL\Attribute\{Type, Field, Query, Arg}; use MonkeysLegion\GraphQL\Context\GraphQLContext; // 1. Define the GraphQL type wrapping the entity #[Type(description: 'A product in the catalog')] final class ProductType { #[Field] public function id(Product $root): int { return $root->id; } #[Field] public function name(Product $root): string { return $root->name; } #[Field] public function price(Product $root): float { return $root->price; } #[Field(description: 'Active in store?')] public function active(Product $root): bool { return $root->active; } #[Field(description: 'ISO 8601')] public function createdAt(Product $root): string { return $root->createdAt->format('c'); } } // 2. Query resolver using the repository from DI #[Query(name: 'product', description: 'Find product by ID')] final class GetProductQuery { public function __construct(private ProductRepository $products) {} public function __invoke( mixed $root, #[Arg(description: 'Product ID')] int $id, GraphQLContext $context, ): ?Product { return $this->products->find($id); } } // 3. List with filtering #[Query(name: 'products', description: 'List products')] final class ListProductsQuery { public function __construct(private ProductRepository $products) {} public function __invoke( mixed $root, #[Arg(nullable: true)] ?bool $active, #[Arg(nullable: true, defaultValue: 20)] int $limit, ): array { if ($active !== null) { return $this->products->findByActive($active, $limit); } return $this->products->findAll($limit); } }
Route Registration
GraphQLProvider automatically registers routes with monkeyslegion-router when the application boots.
Default Routes
| Method | Path | Handler | Description |
|---|---|---|---|
POST |
/graphql |
GraphQLMiddleware |
Queries & mutations |
GET |
/graphql |
GraphQLMiddleware |
Simple GET queries |
GET |
/graphiql |
GraphiQLMiddleware |
Interactive IDE (dev) |
Configuration
# config/graphql.mlc graphql: endpoint: /graphql # Change the endpoint path graphiql_enabled: true # Disable GraphiQL in production graphiql_endpoint: /graphiql # Custom GraphiQL path debug: false # Enable for detailed error traces scan_dirs: - app/GraphQL # Where to find Type/Query/Mutation classes scan_namespace: App\GraphQL # PSR-4 namespace for scanned classes security: max_depth: 10 max_complexity: 200 introspection: true # Disable in production persisted_queries: false rate_limit: max_requests: 100 window_seconds: 60 cache: enabled: false ttl: 3600 # Schema cache TTL in seconds subscriptions: enabled: false driver: memory # 'memory' or 'redis' host: 0.0.0.0 port: 6001 redis_dsn: redis://127.0.0.1:6379
How Route Registration Works
The GraphQLProvider::register() method is called automatically during application bootstrap (via the monkeyslegion extra in composer.json). Here's what happens:
// This happens automatically — no manual setup needed. // The provider: // 1. Reads config/graphql.mlc // 2. Registers all GraphQL services in the DI container // 3. Registers routes with MonkeysLegion\Router\Router // Routes are registered as closures that delegate to PSR-15 middleware: $router->post('/graphql', $graphqlHandler, 'graphql'); $router->get('/graphql', $graphqlHandler, 'graphql.get'); $router->get('/graphiql', $graphiqlHandler, 'graphiql'); // if enabled
Custom Route Middleware
Stack your own middleware (auth, CORS, rate-limiting) alongside GraphQL:
use MonkeysLegion\GraphQL\Attribute\Middleware; // Per-resolver middleware #[Middleware('App\Middleware\AuthMiddleware')] #[Middleware('App\Middleware\RateLimitMiddleware')] #[Query(name: 'adminUsers')] final class AdminUsersQuery { public function __invoke(mixed $root, GraphQLContext $context): array { // Only reached if auth + rate-limit pass return $context->container->get(UserRepository::class)->findAdmins(); } }
Testing the Endpoint
# Simple query curl -X POST http://localhost:8080/graphql \ -H 'Content-Type: application/json' \ -d '{"query": "{ product(id: 1) { name price } }"}' # Mutation curl -X POST http://localhost:8080/graphql \ -H 'Content-Type: application/json' \ -d '{"query": "mutation { createProduct(name: \"Widget\", price: 9.99) { id name } }"}' # GET request (simple queries only) curl 'http://localhost:8080/graphql?query=\{products\{name\}\}' # Open GraphiQL IDE in browser open http://localhost:8080/graphiql
Facade
use MonkeysLegion\GraphQL\GraphQL; // Execute a query programmatically $result = GraphQL::execute('{ user(id: 1) { name } }'); // Publish a subscription event GraphQL::publish('messageAdded', $message); // Get the built schema $schema = GraphQL::schema();
License
MIT — see LICENSE for details.