Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Projektvorstellung: eine Veranstaltungen-GraphQL-API, Modul- und Datenmodell

Projektvorstellung: eine Veranstaltungen-GraphQL-API, Modul- und Datenmodell

~9 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

Ab hier zieht sich ein einziges Projekt durch den Rest dieser Serie: eine GraphQL-API für Veranstaltungen, als eigenständiges Modul Mironsoft\Event. Jedes der folgenden 16 Kapitel erweitert genau dieses Modul - kein Beispielwechsel mehr.

Fachliche Anforderungen

Eine Veranstaltung hat einen Titel, einen eindeutigen Identifier (für die Einzelabfrage in Kapitel 15), eine Beschreibung, einen Ort, einen Start- und optionalen Endzeitpunkt, eine optionale Kapazität und einen Aktiv-Status. Zusätzlich soll ein eingeloggter Kunde eine Veranstaltung als Favorit markieren können (Block 5) - dafür braucht es eine n:m-Beziehung zwischen Veranstaltungen und Kunden, exakt nach demselben Muster wie eine Wunschliste.

Zwei Tabellen: Veranstaltungen und Favoriten

Startzustand des Event-Moduls (Block 4)

app/code/Mironsoft/Event/
├── registration.php
├── composer.json
├── etc/
│   ├── module.xml
│   ├── db_schema.xml
│   └── schema.graphqls
├── Api/
│   ├── Data/
│   │   ├── EventInterface.php
│   │   └── EventSearchResultsInterface.php
│   └── EventRepositoryInterface.php
└── Model/
    ├── Event.php
    ├── EventRepository.php
    └── ResourceModel/
        ├── Event.php
        └── Event/
            └── Collection.php
app/code/Mironsoft/Event/etc/db_schema.xml
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="mironsoft_event" resource="default" engine="innodb"
           comment="Mironsoft Event Table">
        <column xsi:type="int" name="event_id" padding="10" unsigned="true"
                nullable="false" identity="true" comment="Event ID"/>
        <column xsi:type="varchar" name="identifier" nullable="false" length="64"
                comment="URL-safe Unique Identifier"/>
        <column xsi:type="varchar" name="title" nullable="false" length="255"
                comment="Title"/>
        <column xsi:type="text" name="description" nullable="true"
                comment="Description"/>
        <column xsi:type="varchar" name="location" nullable="false" length="255"
                comment="Location"/>
        <column xsi:type="timestamp" name="start_at" nullable="false"
                default="CURRENT_TIMESTAMP" comment="Start Date/Time"/>
        <column xsi:type="timestamp" name="end_at" nullable="true"
                comment="End Date/Time"/>
        <column xsi:type="int" name="capacity" padding="10" unsigned="true"
                nullable="true" comment="Capacity"/>
        <column xsi:type="smallint" name="is_active" padding="5" unsigned="true"
                nullable="false" identity="false" default="1" comment="Is Active"/>
        <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false"
                default="CURRENT_TIMESTAMP" comment="Created At"/>
        <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false"
                default="CURRENT_TIMESTAMP" comment="Updated At"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="event_id"/>
        </constraint>
        <constraint xsi:type="unique" referenceId="MIRONSOFT_EVENT_IDENTIFIER">
            <column name="identifier"/>
        </constraint>
    </table>

    <table name="mironsoft_event_customer_favorite" resource="default" engine="innodb"
           comment="Mironsoft Event To Customer Favorite Linkage Table">
        <column xsi:type="int" name="event_id" padding="10" unsigned="true"
                nullable="false" identity="false" comment="Event ID"/>
        <column xsi:type="int" name="customer_id" padding="10" unsigned="true"
                nullable="false" identity="false" comment="Customer ID"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="event_id"/>
            <column name="customer_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="MIRONSOFT_EVENT_CUSTOMER_FAVORITE_EVENT_ID_EVENT_EVENT_ID"
                    table="mironsoft_event_customer_favorite" column="event_id"
                    referenceTable="mironsoft_event" referenceColumn="event_id"
                    onDelete="CASCADE"/>
        <constraint xsi:type="foreign" referenceId="MIRONSOFT_EVENT_CUSTOMER_FAVORITE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID"
                    table="mironsoft_event_customer_favorite" column="customer_id"
                    referenceTable="customer_entity" referenceColumn="entity_id"
                    onDelete="CASCADE"/>
    </table>
</schema>

Die Favoriten-Tabelle wird erst ab Kapitel 18 tatsächlich beschrieben, steht aber von Anfang an im Schema - so bleibt setup:upgrade ein einmaliger Schritt für dieses Kapitel, statt über mehrere spätere Kapitel verteilt zu werden.

Service Contracts statt nackter Collection

Nach den Projektkonventionen (Service Contracts, Repositories statt direkter Collection-Zugriffe) bekommt Event ein eigenes Api-Interface:

app/code/Mironsoft/Event/Api/Data/EventInterface.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Api\Data;

/**
 * Service contract for a single event entity.
 */
interface EventInterface
{
    public const EVENT_ID = 'event_id';
    public const IDENTIFIER = 'identifier';
    public const TITLE = 'title';
    public const DESCRIPTION = 'description';
    public const LOCATION = 'location';
    public const START_AT = 'start_at';
    public const END_AT = 'end_at';
    public const CAPACITY = 'capacity';
    public const IS_ACTIVE = 'is_active';

    /**
     * Returns the event ID.
     *
     * @return int|null
     */
    public function getEventId(): ?int;

    /**
     * Returns the URL-safe unique identifier.
     *
     * @return string
     */
    public function getIdentifier(): string;

    /**
     * Returns the event title.
     *
     * @return string
     */
    public function getTitle(): string;

    /**
     * Returns the event description.
     *
     * @return string|null
     */
    public function getDescription(): ?string;

    /**
     * Returns the event location.
     *
     * @return string
     */
    public function getLocation(): string;

    /**
     * Returns the start date/time (Y-m-d H:i:s).
     *
     * @return string
     */
    public function getStartAt(): string;

    /**
     * Returns the end date/time (Y-m-d H:i:s), or null if open-ended.
     *
     * @return string|null
     */
    public function getEndAt(): ?string;

    /**
     * Returns the attendee capacity, or null if unlimited.
     *
     * @return int|null
     */
    public function getCapacity(): ?int;

    /**
     * Returns whether the event is active.
     *
     * @return bool
     */
    public function isActive(): bool;
}
app/code/Mironsoft/Event/Model/Event.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Model;

use Magento\Framework\Model\AbstractModel;
use Mironsoft\Event\Api\Data\EventInterface;
use Mironsoft\Event\Model\ResourceModel\Event as EventResource;

/**
 * Event entity model.
 */
class Event extends AbstractModel implements EventInterface
{
    /**
     * Initializes the resource model.
     *
     * @return void
     */
    protected function _construct(): void
    {
        $this->_init(EventResource::class);
    }

    /**
     * @return int|null
     */
    public function getEventId(): ?int
    {
        $id = $this->getData(self::EVENT_ID);

        return $id !== null ? (int) $id : null;
    }

    /**
     * @return string
     */
    public function getIdentifier(): string
    {
        return (string) $this->getData(self::IDENTIFIER);
    }

    /**
     * @return string
     */
    public function getTitle(): string
    {
        return (string) $this->getData(self::TITLE);
    }

    /**
     * @return string|null
     */
    public function getDescription(): ?string
    {
        $description = $this->getData(self::DESCRIPTION);

        return $description !== null ? (string) $description : null;
    }

    /**
     * @return string
     */
    public function getLocation(): string
    {
        return (string) $this->getData(self::LOCATION);
    }

    /**
     * @return string
     */
    public function getStartAt(): string
    {
        return (string) $this->getData(self::START_AT);
    }

    /**
     * @return string|null
     */
    public function getEndAt(): ?string
    {
        $endAt = $this->getData(self::END_AT);

        return $endAt !== null ? (string) $endAt : null;
    }

    /**
     * @return int|null
     */
    public function getCapacity(): ?int
    {
        $capacity = $this->getData(self::CAPACITY);

        return $capacity !== null ? (int) $capacity : null;
    }

    /**
     * @return bool
     */
    public function isActive(): bool
    {
        return (bool) $this->getData(self::IS_ACTIVE);
    }
}
bin/magento setup:upgrade
bin/cache-clean config

Tipp: ResourceModel/Event.php und ResourceModel/Event/Collection.php folgen exakt demselben Muster wie im Testimonial-Modul der Admin-Grids-&-Formulare-Serie (_init() mit Tabellenname/Primärschlüssel bzw. Model/ResourceModel-Paar) - an dieser Stelle nicht erneut abgedruckt, um den Fokus auf das für GraphQL Neue zu legen.

Mit Tabellen und Model-Trias steht die Datengrundlage. Kapitel 12 beginnt mit dem eigentlichen GraphQL-Teil: dem Schema für Veranstaltungen inklusive Pagination nach dem Connection-Pattern.