A production-ready GraphQL server integration for OroCommerce, providing efficient, type-safe API access to business data.
The Genaker GraphQL Bundle integrates the webonyx/graphql-php library with Orocommerce's security, caching, and ORM infrastructure. It exposes business entities (Products, Orders, etc.) through a modern GraphQL query interface alongside traditional REST endpoints.
GraphQL is a query language and runtime for APIs that allows clients to:
- Request exactly what they need — no over-fetching or under-fetching data
- Fetch related data in a single query — no N+1 round-trips to the server
- Self-document via introspection — built-in schema discovery for client tooling
- Evolve APIs without versioning — deprecation flags allow graceful migrations
-
Mobile & Single-Page Apps (SPAs)
GraphQL reduces bandwidth and network overhead — critical for mobile users and offline-first apps. -
Complex Data Requirements
Fetch products with related images, pricing tier, inventory, and warehouse stock in one query. -
Polyglot Clients
iOS, Android, web, and backend integrations consume the same schema without custom resource layers. -
Dashboard & Real-Time UIs
Subscriptions (future) and field-level resolvers enable live data without polling. -
Third-Party Integrations
Marketplace connectors, billing systems, and analytics platforms query exactly the fields they need. -
Microservices & Federated Architectures
Apollo Federation support (planned) enables composing multiple GraphQL services.
- Simple, read-only public APIs (REST is sufficient)
- Bulk data exports (use GraphQL bulk queries responsibly; consider batch APIs instead)
- Server-to-server internal APIs where REST conventions suffice
Before (REST):
GET /api/products/123
GET /api/products/123/images
GET /api/products/123/pricing-tiers
GET /api/warehouses
After (GraphQL):
query {
product(id: 123) {
name
images { url }
pricingTiers { currency amount }
warehouse { name location }
}
}- Only requested fields are serialized and transmitted
- Perfect for low-bandwidth scenarios (mobile networks)
- Reduced payload size improves perceived performance
- Schema is executable documentation
- IDEs (VS Code, WebStorm) provide autocompletion and inline help
- No manual API docs to keep in sync — they're generated from schema
type Product {
id: ID!
name: String!
price: Float! # old field
pricing: Pricing! # new field (coexists peacefully)
legacyPrice: Float @deprecated(reason: "Use pricing instead")
}Built-in cursor-based pagination and complex filter expressions eliminate need for multiple endpoints.
- OAuth2 Bearer token authentication (same as REST)
- Query complexity analysis prevents malicious queries
- Field-level resolver permissions
- Eager-loading strategies prevent N+1 queries
src/Genaker/Bundle/GraphQLBundle/
├── Controller/
│ ├── AbstractGraphQLController.php # HTTP layer, request parsing
│ └── GraphQLController.php # Concrete actions (executeAction, introspect)
├── Schema/
│ │ SchemaFactory.php # Builds the TypeMap from resolver hints
│ ├── Type/
│ │ ├── QueryType.php # Root query { product(...) { ... } }
│ │ ├── MutationType.php # Root mutation { testMutation }
│ │ ├── ProductType.php # Product entity mapped to GraphQL type
│ │ ├── ProductConnectionType.php # Pagination cursor wrapper
│ │ └── ...
│ └── Resolver/
│ ├── ProductResolver.php # Field resolvers for Product queries
│ └── MutationResolver.php # Field resolvers for mutations
├── Tests/
│ ├── Integration/GraphQLIntegrationTest.php
│ └── phpunit.xml.dist
├── Resources/
│ ├── config/
│ │ └── services.yml
│ └── schema.graphql # SDL schema documentation
└── GenakerGraphQLBundle.php
The GraphQL schema is documented in GraphQL Schema Definition Language (SDL) using a modular, extensible format at:
Resources/schema/ # Modular schema directory
├── shared-types.graphql # Scalars, interfaces, common types
├── query.graphql # Root Query type
├── mutation.graphql # Root Mutation type
├── product.graphql # Product resource (queries + types)
└── [order.graphql] # Future: Order resource
Main entry point:
Resources/schema.graphql # Composite schema (documents the structure)
✅ Add New Resources Without Modifying Core:
# New file: Resources/schema/order.graphql
type Order {
id: ID!
orderNumber: String!
status: String!
}
extend type Query {
orders(status: String): [Order!]!
}✅ Domain-Driven Organization:
- Each resource in its own file
- Easy to find and maintain
- Clear dependencies between domains
✅ Reusable Shared Types:
shared-types.graphqldefines scalars, interfaces, errors- All resources inherit base types
# View main schema documentation
cat src/Genaker/Bundle/GraphQLBundle/Resources/schema.graphql
# View product-specific definitions
cat src/Genaker/Bundle/GraphQLBundle/Resources/schema/product.graphql
# View shared types and interfaces
cat src/Genaker/Bundle/GraphQLBundle/Resources/schema/shared-types.graphqlGraphQL tooling supports modular schemas automatically:
- Insomnia — Reads all schema files
- GraphQL Playground — URL:
https://localhost:8000/admin/api/graphql - VS Code GraphQL Extension — Auto-discovers schema/ directory
POST /admin/api/graphql
↓
[OAuth2 api_secured Firewall] ← Validates Bearer token
↓
GraphQLController::executeAction()
↓
AbstractGraphQLController::handleQuery() ← Parses JSON body, extracts query
↓
SchemaFactory::createSchema() ← Builds type map from resolvers
↓
GraphQL::executeQuery() ← webonyx/graphql-php engine
↓
ProductResolver, etc. ← Field-level business logic
↓
200 OK { data: { product: {...} }, errors?: [...] }
All endpoints are protected by OAuth2 Bearer token authentication (firewall: api_secured).
GET /admin/api/graphql
Authorization: Bearer <token>
Accept: application/json
Response:
{
"status": "ok",
"queries": {
"product": "Product",
"products": "[Product!]!"
},
"bundle": "GenakerGraphQLBundle",
"version": "1.0.0"
}
POST /admin/api/graphql
Authorization: Bearer <token>
Content-Type: application/json
{
"query": "query { products(limit: 10 status: ENABLED) { edges { node { id name } } } }",
"variables": {}
}
Response:
{
"data": {
"products": {
"edges": [
{ "node": { "id": "1", "name": "Widget Pro" } },
...
]
}
}
}
POST /admin/api/graphql
Authorization: Bearer <token>
Content-Type: application/json
{
"query": "mutation { testMutation(message: \"Hello GraphQL\") }"
}
Response:
{
"data": {
"testMutation": true
}
}
The testMutation is a reference implementation that always returns true. It's useful for:
- Testing mutation infrastructure
- Validating client mutation implementations
- Dry-running integration tests
OroCommerce is an enterprise B2B e-commerce platform built on Symfony, designed for complex business requirements:
- Multi-Channel Commerce — unified catalog across B2B portals, marketplaces, and integrations
- Business Rules Engine — dynamic pricing, shipping, and order workflows
- Rich Security Model — user roles, ownership, organization hierarchies
- Scalable Architecture — real-time inventory sync, high-volume order processing
- Flexible Data Model — custom attributes, flexible product properties, extensible entities
This GraphQL bundle integrates seamlessly with OroCommerce's:
- Doctrine ORM — automatic lazy-loading and relationship resolution
- Security Context — user organization filtering, role-based field access
- Cache System — Result caching and query plan optimization
- Event Dispatcher — hooks for custom business logic
# Obtain an OAuth2 Bearer token
TOKEN=$(curl -X POST https://localhost:8000/oauth2-token \
-d 'grant_type=client_credentials' \
-d 'client_id=your_client' \
-d 'client_secret=your_secret' \
| jq -r '.access_token')
echo $TOKENcurl -X GET https://localhost:8000/admin/api/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .curl -X POST https://localhost:8000/admin/api/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "{ products(limit: 5) { edges { node { id name status } } } }"
}' | jq .Install Insomnia or GraphQL Playground and point it at https://localhost:8000/admin/api/graphql with your Bearer token.
# ❌ Avoid (O(N) scan)
query {
products(offset: 10000 limit: 10) { ... }
}
# ✅ Use (O(1) lookup)
query {
products(first: 10 after: "<cursor>") { ... }
}# ❌ Over-fetching
query {
products { id name description images pricing data }
}
# ✅ Efficient
query {
products { id name }
}# ❌ Multiple queries
query { product(id: 1) { name } }
query { product(id: 2) { name } }
# ✅ Single query
query {
p1: product(id: 1) { name }
p2: product(id: 2) { name }
}# ❌ String interpolation (harder to cache)
query { product(id: $productId) { ... } }
# ✅ GraphQL variables (query plan cached)
query GetProduct($id: ID!) {
product(id: $id) { ... }
}Run the integration test suite:
bin/phpunit -c src/Genaker/Bundle/GraphQLBundle/Tests/phpunit.xml.dist --testdoxTests cover:
- ✅ Schema introspection
- ✅ Query execution with authentication
- ✅ Malformed request handling (400 errors)
- ✅ Product filter and pagination
- ✅ Content-Type negotiation
The modular schema structure makes it easy to add new resources without modifying core files.
Create Resources/schema/{resource}.graphql:
"""
Order GraphQL types and queries.
"""
type Order implements Timestamped {
id: ID!
orderNumber: String!
status: String!
totalAmount: Float!
createdAt: DateTime!
updatedAt: DateTime!
}
extend type Query {
"""Get a single order by ID"""
order(id: Int!): Order
"""List orders with pagination"""
orders(status: String, limit: Int, offset: Int): [Order!]!
}Create Schema/Type/OrderType.php:
<?php
namespace Genaker\Bundle\GraphQLBundle\Schema\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class OrderType extends ObjectType {
public function __construct(OrderResolver $resolver) {
parent::__construct([
'name' => 'Order',
'fields' => [
'id' => ['type' => Type::nonNull(Type::id())],
'orderNumber' => ['type' => Type::nonNull(Type::string())],
'status' => ['type' => Type::string()],
'totalAmount' => ['type' => Type::float()],
],
'interfaces' => [/* Timestamped interface */],
]);
}
}Create Schema/Resolver/OrderResolver.php:
<?php
namespace Genaker\Bundle\GraphQLBundle\Schema\Resolver;
class OrderResolver {
public function resolveOrder(mixed $obj, array $args): ?Order { /* ... */ }
public function resolveOrders(mixed $obj, array $args): array { /* ... */ }
}Update Schema/SchemaFactory.php:
public function __construct(
private readonly OrderType $orderType, // Add
// ... other types
) {}
public function createSchema(): Schema {
return new Schema(
SchemaConfig::create()
->setQuery($this->queryType)
->setMutation($this->mutationType)
->setTypes([
$this->productType,
$this->orderType, // Add
])
);
}Update Resources/config/services.yml:
Genaker\Bundle\GraphQLBundle\Schema\Type\OrderType:
shared: true
Genaker\Bundle\GraphQLBundle\Schema\Resolver\OrderResolver: {}
Genaker\Bundle\GraphQLBundle\Schema\SchemaFactory:
arguments:
$orderType: '@Genaker\Bundle\GraphQLBundle\Schema\Type\OrderType'Add tests in Tests/Integration/GraphQLIntegrationTest.php:
public function testExecute_order_byId_returnsAllFields(): void {
$response = $this->post(
self::ENDPOINT,
['query' => '{ order(id: 1) { id orderNumber status totalAmount } }'],
['Authorization' => 'Bearer ' . $this->token]
);
$this->assertSame(200, $response->getStatusCode());
$this->assertArrayHasKey('data', json_decode($response->getContent(), true));
}The schema composition happens automatically via:
SchemaFactory::createSchema()— Registers all types- Type fields — Defined via resolvers
setTypes()— Makes types discoverable for introspection
For more advanced cases, use GraphQL's extend directive directly in SDL files:
# Resources/schema/order.graphql
extend type Query {
orders(status: String): [Order!]!
}
extend type Mutation {
createOrder(input: CreateOrderInput!): Order!
}Then register in PHP without modifying QueryType/MutationType.
The schema currently supports queries and mutations only. WebSocket subscriptions are planned for real-time notifications:
type Subscription {
orderStatusChanged(orderId: Int!): Order!
}Check your Bearer token is valid:
bin/console debug:security:token --token="<your_token>"
Ensure the object exists in the database:
SELECT * FROM oro_product WHERE id = <id>;
Enable Doctrine query logging:
// In GraphQL resolver, use eager loading:
->joinEagerLoad('images')
->joinEagerLoad('pricingTiers')
- GraphQL Official Docs: https://graphql.org
- Webonyx GraphQL-PHP: https://github.com/webonyx/graphql-php
- Apollo Client (JavaScript): https://www.apollographql.com/docs/react
- OroCommerce Docs: https://doc.oroinc.com
Property of Genaker / Licensed under the same terms as OroCommerce Enterprise Edition.
Questions? Contact the ERP integration team or check the Oro Developer Guide.