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

Configuration Type im Detail: eigene Config-Quellen jenseits von System/Config

Configuration Type im Detail: eigene Config-Quellen jenseits von System/Config

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

Kapitel 7 hat eine System/Config/Setting-Seite gebaut: vier Werte unter mironsoft_loyalty/general/*, gespeichert in core_config_data, gelesen über ScopeConfigInterface. Das reicht für alles, was ein Shop-Betreiber im Admin selbst pflegen soll. Es gibt aber eine Klasse von Einstellungen, für die dieser Weg der falsche ist: ein globaler Kill-Switch, der die Punkte-Einlösung sofort stoppt, wenn im Ledger etwas nicht stimmt - eine Einstellung, die ein Operations-Team per Deployment setzt, nicht ein Merchant per Admin-Formular, und die im Idealfall auch dann noch lesbar ist, wenn die Datenbank gerade in einem fragwürdigen Zustand ist. Genau dafür bietet Magento eine zweite, seltener genutzte Erweiterungsstelle: eigene Configuration Types.

Wie ScopeConfigInterface wirklich funktioniert

ScopeConfigInterface::getValue() aus Kapitel 7 ist nur eine bequeme Fassade. Darunter liegt Magento\Framework\App\Config, eine Klasse, die mehrere benannte Configuration Types verwaltet - jeder davon eine Implementierung von Magento\Framework\App\Config\ConfigTypeInterface. Magento-Core registriert von Haus aus mehrere davon, unter anderem system (system.xml + core_config_data, das Kapitel 7 nutzt), env (env.php) und default (config.php). ScopeConfigInterface::getValue() fragt letztlich genau einen dieser Typen ab, standardmäßig system. Neu ist an dieser Stelle nur der Gedanke: nichts hindert ein eigenes Modul daran, der Liste einen eigenen, vierten Typ hinzuzufügen.

ConfigSourceInterface vs. ConfigTypeInterface

Zwei verschiedene Interfaces, zwei verschiedene Aufgaben. Magento\Framework\App\Config\ConfigSourceInterface ist die rohe Quelle - eine einzige Methode get(string $path = ''): array, die den kompletten, ungecachten Datenbaum liefert (eine Datei lesen, eine Umgebungsvariable auslesen, einen externen Dienst abfragen). Magento\Framework\App\Config\ConfigTypeInterface ist die registrierte, gecachte Fassade darüber, mit der Methode get($path = ''), die genau wie ScopeConfigInterface::getValue() mit Slash-Pfaden wie redemption/kill_switch angesprochen wird. Für dieses Kapitel reicht eine Quelle und ein Typ.

Die Quelle: eine eigene app/etc-Datei

Genau wie env.php und config.php ist eine reine PHP-Array-Datei unter app/etc/ die einfachste Quelle: versionierbar über Git, deploybar über CI, unabhängig von jeder Datenbankverbindung lesbar.

app/code/Mironsoft/Loyalty/Model/Config/Source/LoyaltyFeatureFlagsFileSource.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model\Config\Source;

use Magento\Framework\App\Config\ConfigSourceInterface;
use Magento\Framework\Filesystem\DirectoryList;
use Magento\Framework\Filesystem\Driver\File;

/**
 * Reads deploy-controlled loyalty feature flags from a plain PHP array file,
 * following the same app/etc/*.php convention as env.php and config.php.
 */
class LoyaltyFeatureFlagsFileSource implements ConfigSourceInterface
{
    private const FILE_NAME = 'loyalty_flags.php';

    /**
     * @param DirectoryList $directoryList Resolves the absolute path to app/etc.
     * @param File $fileDriver Checks file existence without a require()/autoload detour.
     */
    public function __construct(
        private readonly DirectoryList $directoryList,
        private readonly File $fileDriver,
    ) {
    }

    /**
     * Loads the full flags array. A missing file is a valid, deploy-time
     * "everything at its default" state, not an error.
     *
     * @param string $path Ignored - this source always returns its full tree, per ConfigSourceInterface's contract.
     * @return array
     */
    public function get(string $path = ''): array
    {
        $filePath = $this->directoryList->getPath(DirectoryList::CONFIG) . '/' . self::FILE_NAME;
        if (!$this->fileDriver->isExists($filePath)) {
            return [];
        }

        /** @var array<string, mixed>|false $flags */
        $flags = include $filePath;

        return is_array($flags) ? $flags : [];
    }
}
app/etc/loyalty_flags.php
<?php

declare(strict_types=1);

return [
    'redemption' => [
        'kill_switch' => false,
    ],
];

Der Typ: Cache und Pfadauflösung

ConfigTypeInterface deklariert bewusst nur eine einzige Methode - Cache-Strategie und Pfadauflösung sind Sache der jeweiligen Implementierung. Der eigene Typ hier cached die geladenen Daten in derselben Cache-Instanz und unter demselben Tag wie Magentos eigener Konfigurations-Cache.

app/code/Mironsoft/Loyalty/Model/Config/LoyaltyFeatureFlagsConfigType.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model\Config;

use Magento\Framework\App\Cache\Type\Config as ConfigCacheType;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\App\Config\ConfigSourceInterface;
use Magento\Framework\App\Config\ConfigTypeInterface;
use Magento\Framework\Serialize\Serializer\Json;

/**
 * A brand new configuration type, independent of the built-in "system"/"env"/
 * "default" types: deploy-controlled loyalty feature flags, cached under the same
 * core "config" cache tag so bin/cache-clean config busts it too.
 */
class LoyaltyFeatureFlagsConfigType implements ConfigTypeInterface
{
    /**
     * @var string
     */
    public const CONFIG_TYPE = 'mironsoft_loyalty_flags';

    private const CACHE_ID = 'mironsoft_loyalty_flags';

    /**
     * @var array<string, mixed>|null
     */
    private ?array $data = null;

    /**
     * @param ConfigSourceInterface $source Raw, un-cached data source (see above).
     * @param CacheInterface $cache Application cache pool, reused with the core config cache tag.
     * @param Json $serializer Serializes the flags array for the cache entry.
     */
    public function __construct(
        private readonly ConfigSourceInterface $source,
        private readonly CacheInterface $cache,
        private readonly Json $serializer,
    ) {
    }

    /**
     * Resolves a slash-separated path against the cached flags tree, e.g.
     * "redemption/kill_switch". An empty path returns the full tree.
     *
     * @param string $path Slash-separated path into the flags array.
     * @return mixed
     */
    public function get($path = '')
    {
        if ($this->data === null) {
            $this->data = $this->loadData();
        }

        if ($path === '') {
            return $this->data;
        }

        $value = $this->data;
        foreach (explode('/', $path) as $segment) {
            if (!is_array($value) || !array_key_exists($segment, $value)) {
                return null;
            }
            $value = $value[$segment];
        }

        return $value;
    }

    /**
     * Reads the flags tree from cache, or from the source on a cache miss.
     *
     * @return array<string, mixed>
     */
    private function loadData(): array
    {
        $cached = $this->cache->load(self::CACHE_ID);
        if ($cached !== false) {
            /** @var array<string, mixed> $decoded */
            $decoded = $this->serializer->unserialize($cached);

            return $decoded;
        }

        $data = $this->source->get();
        $this->cache->save($this->serializer->serialize($data), self::CACHE_ID, [ConfigCacheType::CACHE_TAG]);

        return $data;
    }
}

get($path = '') bleibt bewusst ohne Parameter- und Rückgabetyp - genau wie schon AbstractBackend::beforeSave($object) in Kapitel 26 erzwingt eine lockere Interface-Signatur eine ebenso lockere Implementierung; PHP verbietet das nachträgliche Verschärfen von Typen bei einer Interface-Methode.

Registrierung in der di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\App\Config">
        <arguments>
            <argument name="types" xsi:type="array">
                <item name="mironsoft_loyalty_flags" xsi:type="object">Mironsoft\Loyalty\Model\Config\LoyaltyFeatureFlagsConfigType</item>
            </argument>
        </arguments>
    </type>
</config>

Achtung: Der types-Array-Eintrag ist ein Constructor-Argument von Magento\Framework\App\Config, einer bereits kompilierten Kern-Klasse - ein neuer Eintrag greift erst nach bin/magento setup:di:compile, selbst im developer-Modus. Wer ihn hinzufügt und nur bin/cache-clean ausführt, sieht den neuen Typ trotzdem nicht.

Verwendung

Konsumiert wird der neue Typ über Magento\Framework\App\Config::get() - nicht zu verwechseln mit ScopeConfigInterface::getValue(), das immer fest an den Typ system gebunden ist. Der erste Parameter ist der Typname aus der di.xml von oben.

// Illustrativer Ausschnitt - keine Änderung am in Kapitel 50 fixierten Redeem-Controller.
public function __construct(
    private readonly \Magento\Framework\App\Config $appConfig,
    // ...
) {
}

private function assertRedemptionAllowed(): void
{
    if ((bool) $this->appConfig->get(LoyaltyFeatureFlagsConfigType::CONFIG_TYPE, 'redemption/kill_switch')) {
        throw new \Magento\Framework\Exception\LocalizedException(
            __('Reward redemption is temporarily disabled.')
        );
    }
}

Tipp: Der Aufruf __('Reward redemption is temporarily disabled.') im Beispiel oben ist kein Zufall - genau solche Zeichenketten sind das Thema der beiden nächsten Kapitel.

Abgrenzung zu Kapitel 7: wann welcher Weg?

  • system.xml (Kapitel 7): Der Merchant soll den Wert selbst im Admin pflegen können, pro Website/Store skaliert, in core_config_data versioniert - points_per_euro, points_expiry_months.
  • Eigener Configuration Type (dieses Kapitel): Ops/Deployment kontrolliert den Wert, global, dateibasiert, unabhängig von der Datenbank lesbar - ein Kill-Switch, ein Feature-Flag für eine laufende Migration, ein Wert, der bewusst außerhalb der Reichweite des Admin-Formulars bleiben soll.

Kapitel 89 und 90 wenden sich als Nächstes einer ganz anderen Art von Konfiguration zu: nicht Werten, sondern Zeichenketten - der Mehrsprachigkeit dieses Moduls.