meiji/pimcore-datahub-operation-loader-bundle

Pimcore Datahub Operation Loader Bundle

Installs: 111

Dependents: 0

Suggesters: 0

Security: 0

Type:pimcore-bundle

pkg:composer/meiji/pimcore-datahub-operation-loader-bundle

0.0.4 2025-10-01 18:32 UTC

This package is not auto-updated.

Last update: 2025-10-01 18:34:12 UTC


README

Last updated: 2025-10-01 18:26

EnglishРусский

English

What is this?

A lightweight Pimcore bundle that lets you register GraphQL operations (Queries/Mutations) into Pimcore DataHub by code. You declare which operation classes belong to a DataHub client in Symfony config, and the bundle wires them on DataHub build events.

TL;DR: put your schema field contract in AbstractQuery/AbstractMutation, business flow in AbstractResolver, and reusable field configs in AbstractObjectConfigType / AbstractInputObjectConfigType.

Features

  • Code-first registration of GraphQL operations for a given DataHub client.
  • Clean split of responsibilities:
    • AbstractQuery/AbstractMutation — name, args, type, resolver binding.
    • AbstractResolverstatic resolve($params, $args, $context, ResolveInfo $info): array.
    • AbstractObjectConfigType / AbstractInputObjectConfigType — reusable GraphQL type configs (multiton).
  • Works with Pimcore DataHub build events (QueryEvents::PRE_BUILD / MutationEvents::PRE_BUILD).

Requirements

  • PHP ≥ 8.1 (typed properties, argument unpacking ...).
  • Pimcore / DataHub compatible with GraphQL runtime (Pimcore 11+ recommended).
  • Symfony ≥ 6 (typical for Pimcore 11).

Installation

composer require meiji/pimcore-datahub-operation-loader-bundle

Register the bundle if not auto-discovered (Symfony config/bundles.php):

return [
    Meiji\DataHubOperationLoaderBundle\DataHubOperationLoaderBundle::class => ['all' => true],
];

Configuration

Create config/packages/data_hub_operation_loader.yaml:

data_hub_operation_loader:
  webservices:
    # The key must match your DataHub client name (e.g. ?clientname=public_api)
    public_api:
      queries:
        - \\App\\GraphQL\\V2\\Operation\\MyEntityListQuery
      mutations:
        - \\App\\GraphQL\\V2\\Operation\\UpsertEntityMutation

How client is resolved: 1) From HTTP request clientname (usual for DataHub UI/endpoint); 2) If no request, from DataHub event context (when available); 3) Otherwise, a runtime exception is thrown (no client specified).

Define a Query

namespace App\GraphQL\V2\Operation;

use Meiji\DataHubOperationLoaderBundle\GraphQL\AbstractQuery;
use GraphQL\Type\Definition\Type as Gql;

final class MyEntityListQuery extends AbstractQuery
{
    protected string $name = 'myEntityList';
    protected string $resolverClass = \App\GraphQL\V2\Resolver\MyEntityResolver::class;
    protected string $typeClass     = \App\GraphQL\V2\Type\MyEntityType::class; // provided by AbstractObjectConfigType

    protected function getArgs(): array
    {
        return [
            'filter' => ['type' => \App\GraphQL\V2\Type\MyFilterInput::getInstance().getConfig()], // or your way to expose input type
            'page'   => ['type' => Gql::int()],
            'limit'  => ['type' => Gql::int()],
        ];
    }
}

Implement the Resolver

namespace App\GraphQL\V2\Resolver;

use Meiji\DataHubOperationLoaderBundle\GraphQL\AbstractResolver;
use GraphQL\Type\Definition\ResolveInfo;

final class MyEntityResolver extends AbstractResolver
{
    public static function resolve(array $params, array $args, array $context, ResolveInfo $info): array
    {
        // pre: ACL / tenancy checks
        // step: normalize args -> Criteria
        // helper: use your caching/pagination helpers here
        // step: fetch data (batch-friendly) -> map to DTO for Type
        // errors: throw domain/validation exceptions with actionable messages
        return [
            // ...normalized output expected by MyEntityType
        ];
    }
}

Object & Input Types

namespace App\GraphQL\V2\Type;

use Meiji\DataHubOperationLoaderBundle\GraphQL\AbstractObjectConfigType;
use GraphQL\Type\Definition\Type as Gql;

final class MyEntityType extends AbstractObjectConfigType
{
    protected string $name = 'MyEntity';
    protected string $description = 'Public entity shape';

    protected function getFields(): array
    {
        return [
            'id'        => ['type' => Gql::nonNull(Gql::id())],
            'name'      => ['type' => Gql::nonNull(Gql::string())],
            'createdAt' => ['type' => Gql::nonNull(Gql::string())],
        ];
    }
}
final class MyFilterInput extends AbstractInputObjectConfigType
{
    protected string $name = 'MyFilterInput';
    protected string $description = 'Filter for listing';
    protected ?string $assignObject = \App\DTO\MyFilter::class; // optional

    protected function getFields(): array
    {
        return [
            'q'     => ['type' => Gql::string()],
            'page'  => ['type' => Gql::int()],
            'limit' => ['type' => Gql::int()],
        ];
    }
}

If assignObject is set, input values may be passed through a callable that hydrates your DTO (internally: new $this->assignObject(...$values)), simplifying resolver code.

Event Flow

  • On QueryEvents::PRE_BUILD / MutationEvents::PRE_BUILD the bundle: 1) reads data_hub_operation_loader.webservices from DI, 2) detects the DataHub client, 3) adds fields from your operation classes to the GraphQL schema.

Testing

  • Use Pimcore DataHub GraphiQL (select your client) and call myEntityList(...).
  • Verify field presence and arguments; check error/ACL behavior.

License

MIT © 2024 Meiji

Русский

Что это?

Небольшой бандл для Pimcore, который позволяет регистрировать GraphQL‑операции (Query/Mutation) в Pimcore DataHub кодом. В конфигурации Symfony вы перечисляете классы операций для конкретного клиента DataHub, а бандл подключает их на событиях сборки DataHub.

Коротко: контракт поля (имя/аргументы/тип/связка с резолвером) — в AbstractQuery/AbstractMutation, бизнес‑логика — в AbstractResolver, переиспользуемые конфиги типов — в AbstractObjectConfigType / AbstractInputObjectConfigType.

Возможности

  • Code‑first регистрация GraphQL‑операций для выбранного клиента DataHub;
  • Чёткое разделение обязанностей:
    • AbstractQuery/AbstractMutation — имя, аргументы, тип, биндинг резолвера;
    • AbstractResolverstatic resolve($params, $args, $context, ResolveInfo $info): array;
    • AbstractObjectConfigType / AbstractInputObjectConfigType — конфиги типов/инпутов (мульти‑тон);
  • Интеграция с событиями DataHub (QueryEvents::PRE_BUILD / MutationEvents::PRE_BUILD).

Требования

  • PHP ≥ 8.1 (typed properties, распаковка аргументов ...);
  • Pimcore / DataHub с поддержкой GraphQL (рекомендуется Pimcore 11+);
  • Symfony ≥ 6 (как правило для Pimcore 11).

Установка

composer require meiji/pimcore-datahub-operation-loader-bundle

Если автоподключение не сработало — добавьте бандл в config/bundles.php:

return [
    Meiji\DataHubOperationLoaderBundle\DataHubOperationLoaderBundle::class => ['all' => true],
];

Конфигурация

config/packages/data_hub_operation_loader.yaml:

data_hub_operation_loader:
  webservices:
    public_api:          # ключ — имя клиента DataHub (например, ?clientname=public_api)
      queries:
        - \\App\\GraphQL\\V2\\Operation\\MyEntityListQuery
      mutations:
        - \\App\\GraphQL\\V2\\Operation\\UpsertEntityMutation

Как определяется клиент:
1) Из HTTP‑параметра clientname; 2) при его отсутствии — из контекста события DataHub; 3) иначе выбрасывается исключение (клиент не указан).

Пример Query

final class MyEntityListQuery extends AbstractQuery
{
    protected string $name = 'myEntityList';
    protected string $resolverClass = \App\GraphQL\V2\Resolver\MyEntityResolver::class;
    protected string $typeClass     = \App\GraphQL\V2\Type\MyEntityType::class;

    protected function getArgs(): array
    {
        return [
            'filter' => ['type' => \App\GraphQL\V2\Type\MyFilterInput::getInstance().getConfig()],
            'page'   => ['type' => Gql::int()],
            'limit'  => ['type' => Gql::int()],
        ];
    }
}

Пример Resolver

final class MyEntityResolver extends AbstractResolver
{
    public static function resolve(array $params, array $args, array $context, ResolveInfo $info): array
    {
        // pre: проверка прав/контекста
        // step: нормализация аргументов -> Criteria
        // helper: используем хелперы кэша/пагинации/батчинга
        // step: выборка данных -> маппинг в DTO под тип
        // errors: кидаем предметные исключения с полезными сообщениями
        return [/* ... */];
    }
}

Объектные и входные типы

final class MyEntityType extends AbstractObjectConfigType
{
    protected string $name = 'MyEntity';
    protected string $description = 'Публичное представление сущности';

    protected function getFields(): array
    {
        return [
            'id'        => ['type' => Gql::nonNull(Gql::id())],
            'name'      => ['type' => Gql::nonNull(Gql::string())],
            'createdAt' => ['type' => Gql::nonNull(Gql::string())],
        ];
    }
}
final class MyFilterInput extends AbstractInputObjectConfigType
{
    protected string $name = 'MyFilterInput';
    protected string $description = 'Фильтр для листинга';
    protected ?string $assignObject = \App\DTO\MyFilter::class; // опционально

    protected function getFields(): array
    {
        return [
            'q'     => ['type' => Gql::string()],
            'page'  => ['type' => Gql::int()],
            'limit' => ['type' => Gql::int()],
        ];
    }
}

Если задан assignObject, значения инпута могут быть переданы в конструктор DTO (new $this->assignObject(...$values)), упрощая код резолвера.

Жизненный цикл

На событиях PRE_BUILD бандл читает конфиг, определяет клиента и добавляет поля из классов операций в схему GraphQL.

Тестирование

Через GraphiQL DataHub для вашего клиента вызовите myEntityList(...) и проверьте контракт/ошибки/права.

Лицензия

MIT © 2024 Meiji