Projektvorstellung: eigenes Modul für Kundenstimmen, DB-Schema mit db_schema.xml
Projektvorstellung: eigenes Modul für Kundenstimmen, DB-Schema mit db_schema.xml
~9 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Ab hier zieht sich ein einziges Projekt durch den Rest dieser Serie: die Verwaltung von Kundenstimmen (Testimonials) im Admin-Bereich, als eigenständiges Modul Mironsoft\Testimonial. Jede der folgenden 17 Kapitel erweitert genau dieses Modul - kein Beispielwechsel mehr.
Fachliche Anforderungen
Eine Kundenstimme besteht aus Name und Firma des Kunden, einer Bewertung (1 bis 5 Sterne), dem eigentlichen Text, einem optionalen Foto und einem Aktiv-Status. Zusätzlich soll eine Kundenstimme sich pro Store View gezielt ein- oder ausblenden lassen (Block 4, Kapitel 18) - ein Muster, das aus Magento_Cms (CMS-Blöcke/-Seiten) bekannt ist und hier bewusst nachgebaut wird.
Zwei Tabellen statt einer
Die Store-View-Zuordnung braucht eine n:m-Beziehung: eine Kundenstimme kann für mehrere Stores sichtbar sein, ein Store kann mehrere Kundenstimmen zeigen. Dafür reicht keine einzelne store_id-Spalte - es braucht eine zweite, verknüpfende Tabelle, exakt wie cms_block_store im Magento-Core.
Startzustand des Testimonial-Moduls (Block 4)
app/code/Mironsoft/Testimonial/
├── registration.php
├── composer.json
├── etc/
│ ├── module.xml
│ ├── db_schema.xml
│ ├── acl.xml
│ └── adminhtml/
│ ├── routes.xml
│ └── menu.xml
└── Model/
├── Testimonial.php
└── ResourceModel/
├── Testimonial.php
└── Testimonial/
└── 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_testimonial" resource="default" engine="innodb"
comment="Mironsoft Testimonial Table">
<column xsi:type="int" name="testimonial_id" padding="10" unsigned="true"
nullable="false" identity="true" comment="Testimonial ID"/>
<column xsi:type="varchar" name="customer_name" nullable="false" length="255"
comment="Customer Name"/>
<column xsi:type="varchar" name="company" nullable="true" length="255"
comment="Company"/>
<column xsi:type="smallint" name="rating" padding="5" unsigned="true"
nullable="false" identity="false" default="5" comment="Rating 1-5"/>
<column xsi:type="text" name="testimonial_text" nullable="false"
comment="Testimonial Text"/>
<column xsi:type="varchar" name="image" nullable="true" length="255"
comment="Image Path"/>
<column xsi:type="int" name="position" padding="10" unsigned="false"
nullable="false" identity="false" default="0" comment="Sort Position"/>
<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="testimonial_id"/>
</constraint>
</table>
<table name="mironsoft_testimonial_store" resource="default" engine="innodb"
comment="Mironsoft Testimonial To Store Linkage Table">
<column xsi:type="int" name="testimonial_id" padding="10" unsigned="true"
nullable="false" identity="false" comment="Testimonial ID"/>
<column xsi:type="smallint" name="store_id" padding="5" unsigned="true"
nullable="false" identity="false" comment="Store ID"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="testimonial_id"/>
<column name="store_id"/>
</constraint>
<constraint xsi:type="foreign" referenceId="MIRONSOFT_TESTIMONIAL_STORE_TESTIMONIAL_ID_TESTIMONIAL_TESTIMONIAL_ID"
table="mironsoft_testimonial_store" column="testimonial_id"
referenceTable="mironsoft_testimonial" referenceColumn="testimonial_id"
onDelete="CASCADE"/>
<constraint xsi:type="foreign" referenceId="MIRONSOFT_TESTIMONIAL_STORE_STORE_ID_STORE_STORE_ID"
table="mironsoft_testimonial_store" column="store_id"
referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/>
</table>
</schema>onDelete="CASCADE" auf beiden Fremdschlüsseln sorgt dafür, dass beim Löschen einer Kundenstimme automatisch auch ihre Store-Zuordnungen verschwinden - ohne verwaiste Zeilen und ohne dass der Delete-Controller (Kapitel 17) das explizit selbst erledigen muss.
Model, ResourceModel, Collection
<?php
declare(strict_types=1);
namespace Mironsoft\Testimonial\Model;
use Magento\Framework\Model\AbstractModel;
use Mironsoft\Testimonial\Model\ResourceModel\Testimonial as TestimonialResource;
/**
* Testimonial entity model.
*/
class Testimonial extends AbstractModel
{
/**
* Initializes the resource model.
*
* @return void
*/
protected function _construct(): void
{
$this->_init(TestimonialResource::class);
}
}<?php
declare(strict_types=1);
namespace Mironsoft\Testimonial\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
/**
* Testimonial resource model, maps the entity onto mironsoft_testimonial.
* Store relations are added in chapter 18.
*/
class Testimonial extends AbstractDb
{
/**
* Initializes the main table and primary key column.
*
* @return void
*/
protected function _construct(): void
{
$this->_init('mironsoft_testimonial', 'testimonial_id');
}
}<?php
declare(strict_types=1);
namespace Mironsoft\Testimonial\Model\ResourceModel\Testimonial;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use Mironsoft\Testimonial\Model\ResourceModel\Testimonial as TestimonialResource;
use Mironsoft\Testimonial\Model\Testimonial as TestimonialModel;
/**
* Collection of testimonial entities, used by grid DataProviders.
*/
class Collection extends AbstractCollection
{
/**
* Binds the collection to its model and resource model pair.
*
* @return void
*/
protected function _construct(): void
{
$this->_init(TestimonialModel::class, TestimonialResource::class);
}
}ACL, Menü, Routes - analog zu Block 1
Diese Dateien folgen exakt dem Muster aus Kapitel 3 - ACL-Ressource Mironsoft_Testimonial::testimonial, Route mironsoft_testimonial, Menüpunkt unter Content. Kapitel 25 baut die ACL-Ressource später zu drei getrennten Knoten (Ansicht, Formular, Löschen) aus - hier reicht für den Einstieg der einzelne Knoten.
bin/magento setup:upgrade
bin/magento cache:cleanTipp: Nach dem ersten setup:upgrade lohnt sich ein Blick in die Datenbank (bin/mysql), um zu prüfen, ob beide Tabellen inklusive Fremdschlüssel wie erwartet angelegt wurden - SHOW CREATE TABLE mironsoft_testimonial_store; zeigt die tatsächlich generierten Constraint-Namen.
Mit Tabellen, Model-Trias und Backend-Zugriff steht die Grundlage. Kapitel 15 baut darauf den Grid.