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

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
app/code/Mironsoft/Testimonial/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_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

app/code/Mironsoft/Testimonial/Model/Testimonial.php
<?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);
    }
}
app/code/Mironsoft/Testimonial/Model/ResourceModel/Testimonial.php
<?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');
    }
}
app/code/Mironsoft/Testimonial/Model/ResourceModel/Testimonial/Collection.php
<?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:clean

Tipp: 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.