Eine eigene Preference für die Punkte-Berechnung schreiben
Eine eigene Preference für die Punkte-Berechnung schreiben
~9 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Das Business-Szenario aus Kapitel 40: dieser Shop nutzt bereits eine lizenzierte Drittanbieter-Erweiterung, die die B2B-Sonderkonditionen aus Kapitel 22 mit einem externen ERP-System synchronisiert - fiktiver Hersteller-Namespace ErpSync\CorporateRewards. Neue Anforderung: der von der ERP-Anbindung gelieferte Firmen-Multiplikator soll nie unter den Wert fallen, den loyalty_tier_override (Kapitel 22) für die Firma vorsieht - aber die Erweiterung selbst kennt dieses Attribut naturgemäß nicht.
Die Ausgangslage: eine fremde final-Methode
<?php
declare(strict_types=1);
namespace ErpSync\CorporateRewards\Api;
/**
* Vendor-provided contract for resolving a company's ERP-driven pricing
* multiplier. Part of a licensed third-party extension, not this project's code.
*/
interface MultiplierResolverInterface
{
/**
* @param int $companyId Company entity ID.
* @return float
*/
public function resolveForCompany(int $companyId): float;
}<?php
declare(strict_types=1);
namespace ErpSync\CorporateRewards\Model;
use ErpSync\CorporateRewards\Api\MultiplierResolverInterface;
/**
* Default implementation, shipped by the vendor. resolveForCompany() is
* declared final on purpose - the vendor wants every shop's ERP multiplier
* to always come straight from their sync service, with no local drift.
*/
class MultiplierResolver implements MultiplierResolverInterface
{
// ... constructor with the vendor's own ERP client dependency ...
/**
* @param int $companyId Company entity ID.
* @return float
*/
final public function resolveForCompany(int $companyId): float
{
// ... calls the external ERP system, falls back to 1.0 on error ...
}
}Der Hersteller registriert seine eigene Implementierung bereits per Preference in seiner eigenen di.xml: <preference for="ErpSync\CorporateRewards\Api\MultiplierResolverInterface" type="ErpSync\CorporateRewards\Model\MultiplierResolver"/>.
Warum ein Plugin hier scheitert
Ein Plugin auf resolveForCompany() würde bei setup:di:compile (oder beim ersten Laufzeit-Aufruf, falls der Compiler-Schritt übersprungen wird) mit einem PHP-Fatal-Error abbrechen: die generierte Interceptor-Klasse müsste MultiplierResolver erweitern und resolveForCompany() überschreiben - genau das verbietet final kategorisch. Es gibt keinen Weg, dieses eine Detail per Plugin anzupassen, ohne die final-Deklaration selbst zu entfernen (und damit den Quellcode eines fremden, lizenzierten Moduls zu verändern - keine Option).
Die Lösung: Preference mit Komposition statt Vererbung
Der entscheidende Kniff: die eigene Preference-Klasse extends MultiplierResolver nicht - sie implementiert stattdessen direkt MultiplierResolverInterface und hält die Original-Implementierung als ganz normale, injizierte Abhängigkeit (Komposition). Da final ausschließlich Vererbung verbietet, nicht aber das Halten einer Instanz als Kollaborator, bleibt der Original-Algorithmus des Herstellers vollständig nutzbar - nur eben nicht überschreibbar.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model\Erp;
use ErpSync\CorporateRewards\Api\MultiplierResolverInterface;
use ErpSync\CorporateRewards\Model\MultiplierResolver;
use Magento\Company\Api\CompanyRepositoryInterface;
use Mironsoft\Loyalty\Model\Source\LoyaltyTier;
/**
* Replaces the vendor's MultiplierResolver via a di.xml preference. Composes
* - rather than extends - the vendor's final-method implementation, so its ERP
* logic stays intact while a project-specific floor value is layered on top.
*/
class CompanyMultiplierPreference implements MultiplierResolverInterface
{
/**
* @var array<string, float>
*/
private const TIER_FLOOR_MULTIPLIERS = [
LoyaltyTier::TIER_BRONZE => 1.0,
LoyaltyTier::TIER_SILVER => 1.2,
LoyaltyTier::TIER_GOLD => 1.5,
];
/**
* @param MultiplierResolver $originalResolver The vendor's own final-method implementation, composed rather than extended.
* @param CompanyRepositoryInterface $companyRepository Loads the company to read loyalty_tier_override (chapter 22).
*/
public function __construct(
private readonly MultiplierResolver $originalResolver,
private readonly CompanyRepositoryInterface $companyRepository,
) {
}
/**
* Resolves the ERP multiplier, raised to the project's tier floor if needed.
*
* @param int $companyId Company entity ID.
* @return float
*/
public function resolveForCompany(int $companyId): float
{
$erpMultiplier = $this->originalResolver->resolveForCompany($companyId);
$company = $this->companyRepository->get($companyId);
// @phpstan-ignore-next-line CompanyInterface::getData() is not on the interface but exists on the model
$tierOverride = (string) $company->getData('loyalty_tier_override');
$floor = self::TIER_FLOOR_MULTIPLIERS[$tierOverride] ?? 0.0;
return max($erpMultiplier, $floor);
}
}<preference for="ErpSync\CorporateRewards\Api\MultiplierResolverInterface"
type="Mironsoft\Loyalty\Model\Erp\CompanyMultiplierPreference"/>Wo dieser Wert später einfließt
CompanyMultiplierPreference bleibt bewusst eigenständig und ist noch nicht in PointsCalculator::calculatePoints() (Kapitel 5) verdrahtet - das würde eine neue Konstruktor-Abhängigkeit in einer bereits fixierten Klasse dieser Serie bedeuten und liegt außerhalb des heutigen Kapitel-Fokus. Für dieses Kapitel zählt die Preference-Technik selbst: ein final-Hindernis sauber umgangen, ohne fremden Quellcode zu verändern.
Tipp: Der Komposition-statt-Vererbung-Ansatz zahlt sich auch beim Testen aus: CompanyMultiplierPreference lässt sich isoliert instanziieren, indem MultiplierResolver und CompanyRepositoryInterface im Unit-Test durch Test-Doubles ersetzt werden (Kapitel 91/92) - kein ObjectManager, kein generierter Interceptor, kein Setup-Overhead.
Achtung: Pro Interface gewinnt immer nur eine Preference. Registriert irgendein anderes, später geladenes Modul ebenfalls eine Preference für MultiplierResolverInterface, wird CompanyMultiplierPreference stillschweigend nie instanziiert - ohne Fehlermeldung. Kapitel 42 zeigt, wie dieses Risiko in der Praxis aussieht und wie man es eindämmt.