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<?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:
<?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;
}<?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 configTipp: 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.