Ich habe fast einen ganzen Tag herumexperimentiert und vieles ausprobiert und nun endlich eine Lösung gefunden.
Diese Lösung (getestet mit Shopware 6.5.7.3 ) deckt das Hinzufügen einer Checkbox zum Checkout Formular sowie das Verarbeiten dessen Werts ab. Interessant ist dieses Szenario vor allem, weil Checkboxen für Formulare ein richtiger pain in the you know what ist:
1. Das Custom Field
Zur Referenz, damit die nachfolgenden Schritte korrekt verstanden werden, zeige ich hier kurz das Custom Feld, welches ich bei der Installation meines Plugins der Order Entity hinzufüge:
public function addCustomFieldsToOrders($context): void {
$this->customFieldSetRepository->create([
[
'name' => 'b2bbasics_order',
'config' => [
'label' => [
'en-GB' => 'B2BBasics Order field set',
'de-DE' => 'B2BBasics Bestellzusatzfelder'
],
],
'relations' => [
[
'entityName' => 'order'
]
],
'customFields' => [
[
'name' => 'b2bbasics_order_allow_partial_delivery',
'type' => CustomFieldTypes::BOOL,
'config' => [
'type' => 'checkbox',
'componentName' => 'sw-field',
'customFieldType' => 'checkbox',
'label'=> [
'en-GB' => 'Allow partial delivery',
'de-DE' => 'Teillieferungen erlauben'
],
]
]
]
]
], $context);
}
Wie wir sehen ist es ein CustomField vom Typ Bool, welches mit einer Checkbox dargestellt wird und dem Kunden die Entscheidung gibt, ob Teillieferungen bei der Bestellungen tolleriert werden oder nicht.
Zur Vervollständigung hier der Funktionsaufruf bei der Plugin-Installation:
// src/custom/plugins/B2BBasics/src/B2BBasics.php
<?php declare(strict_types=1);
namespace B2BBasics;
use \Shopware\Core\Defaults;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use B2BBasics\Services\RepoCreator;
class B2BBasics extends Plugin
{
public function install(InstallContext $context): void
{
$repoCreator = new RepoCreator($this->container->get('custom_field_set.repository'));
$repoCreator->addCustomFieldsToOrders($context->getContext());
}
}
2. Hinzufügen des Custom Fields zum Checkout Formular
Wie es beim Templating so ist, muss man die richtige Stelle finden, an der man das Feld hinzufügt. Ich habe mich in der Checkout Confirm Seite dazu entschieden, den Block page_checkout_additional
für meine Bedürfnisse anzupassen und dort das Feld hinzuzufügen:
// src/custom/plugins/B2BBasics/src/Resources/views/storefront/page/checkout/confirm/index.html.twig
{% sw_extends '@Storefront/storefront/page/checkout/confirm/index.html.twig'%}
// Normalerweise ist der ganze Block so designt, dass dort ausschließlich das Customer Comment Field ist.
// Deswegen müssen wir den gesamten Block umschreiben
{% block page_checkout_additional %}
<div class="checkout-additional">
<div class="card checkout-card">
<div class="card-body">
{% block page_checkout_finish_customer_comment %}
{{ block('page_checkout_confirm_customer_comment_header')}}
{{ block('page_checkout_confirm_customer_comment_control')}}
{% endblock %}
// Ab hier beginnt der relevante Part, wo wir unser custom Field hinzufügen
{% block page_checkout_additional_partial_delivery %}
{% block page_checkout_additional_partial_delivery_input %}
<input form="confirmOrderForm" //Der Form Name ist sehr wichtig
data-partial-delivery-checkbox-handler // Attribut ist wichtig für Part 2b
type="checkbox" class="form-check-input"
name="allowPartialDelivery" value="1" id="allowPartialDelivery" // Name und ID sind auch wichtig
checked> // So schlaue Füchse wie wir sind, ist das Feld standardmäßig angehakt
{% endblock %}
{% block page_checkout_additional_partial_delivery_label %}
<label class="form-check-label custom-control-label" for="allowPartialDelivery">
{{ "customFields.b2bbasics_order_allow_partial_delivery"|trans|sw_sanitize }}
{% endblock %}
{% endblock %}
</div>
</div>
</div>
{% endblock %}
2b. But wait, there’s more (Oder auch: Warum Checkboxen keinen Spaß machen)
Wie immer wäre es schön, wenn alles einfach funktionieren würde. Checkboxen gehören leider zu den Geschichten, welche das nicht tun. Aus folgenden Gründen:
- Checkboxen haben einen fest definierten Value oder emitten „On“ wenn keiner gesetzt ist
- Ja, eigentlich würde die Checkbox „Off“ emitten, jedoch wird eine ungecheckte Checkbox ohne Wert vom Formular gnadenlos ignoriert
- Wir müssen unsere eigene Logik hinzuschreiben, welche den Wert der Checkbox abhängig von seinem Status ändert
Introducing: CheckboxHandler.js
CheckboxHandler.js ist ein Script, welches wir an Checkboxen anhängen können, um deren Value entsprechend ihrem Checked Status setzen zu können:
// src/custom/plugins/B2BBasics/src/Resources/App/storefront/src/B2BBasics/CheckboxHandler.js
import Plugin from 'src/plugin-system/plugin.class';
export default class CheckboxHandler extends Plugin {
init() {
this.el.value = this.el.checked; // Setzt den Value des Elements entsprechend des Checked Values
this.el.addEventListener("click", this.changeCheckboxValueState.bind(this)); // Hier hören wir auf das Event, welches beim Interagieren mit der Checkbox ausgelöst wird (funktioniert auch mit Leertasten-Interaktion)
}
changeCheckboxValueState() {
this.el.value = this.el.checked; // Setzt den Value Wert neu. Zur erhöhten Zuverlässigkeit negieren wir den Wert nicht einfach
}
}
Damit dieser Spaß auch richtig an die Checkbox attached wird, müssen wir in unserer main.js das Plugin entsprechend registrieren:
// src/custom/plugins/B2BBasics/src/Resources/app/storefront/src/main.js
import CheckboxHandler from "./B2BBasics/CheckboxHandler"; // Importieren unseres Scripts
const PluginManager = window.PluginManager;
// Hier wird das Script an das Checkbox-Element attached. Man beachte das Attribut, welches wir zur Identifikation verwenden und auch im Template vorhanden ist
PluginManager.register('CheckboxHandler', CheckboxHandler, '[data-partial-delivery-checkbox-handler]');
Mit diesen Schritten wird der Wert der Checkbox nun richtig gesetzt und beim „Submitten“ der Checkout Confirmation wird der Wert entsprechend mitgesendet, sodass wir diesen verarbeiten können.
3. Rawdogging beim Order Event
Ich habe viele verschiedene Lösungsansätze zur Verarbeitung des Werts bei Bestellungen gesehen und teilweise ausprobiert und alle für bestenfalls mäßig befunden.
Mein Ansatz sieht wiefolgt aus:
- Wir hören auf das
CheckoutOrderPlacedEvent
, um entsprechend auf die Bestellerstellung reagieren zu können
- Wir fügen dem Subscriber Service noch entsprehcend den
RequestStack
als Eingabeparameter hinzu, weil das CheckoutOrderPlacedEvent
keine Daten unserer Order Submit Request enthält (wäre nur zu praktisch)
- Wir lesen den Wert unserer Checkbox aus (oder versuchen es) und setzen im ´OrderRepository´ bei der entsprechenden Entity den Wert
Das sieht dann so aus:
// src/custom/plugins/B2BBasics/src/Subscriber/OrderPlacedSubscriber.php
<?php declare(strict_types=1);
namespace B2BBasics\Subscriber;
use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class OrderPlacedSubscriber implements EventSubscriberInterface {
private $requestStack;
private EntityRepository $orderRepo;
// Unser Service braucht sowohl den Request Stack, als auch das Order Repository
public function __construct(RequestStack $requestStack, EntityRepository $orderRepo)
{
$this->requestStack = $requestStack;
$this->orderRepo = $orderRepo;
}
// Wir subscriben zu dem CheckoutOrderPlacedEvent
public static function getSubscribedEvents()
{
return [
CheckoutOrderPlacedEvent::class => 'onOrderPlaced', // Das Subscriben zu dem Event mit der Konstante `CartEvents::CHECKOUT_ORDER_PLACED` hat nicht funktioniert. Deswegen lieber die Klasse angeben
];
}
public function onOrderPlaced(CheckoutOrderPlacedEvent $event): void {
// Da wir in unserem Event keine Informationen erhalten, welche bei der Request versendet wurden, müssen wir uns damit behelfen die Request selbst abzurufen und den Wert auszulesen
$partialDeliveryAllowed = $this->requestStack->getCurrentRequest()->get('allowPartialDelivery'); // Man beachte die id der Checkbox im Template
// Sollte der Wert nicht mitgesendet worden sein, brechen wir ab
if($partialDeliveryAllowed === null) {
return;
}
// Zum Glück müssen wir nicht komplett rawdoggen: Die OrderID können wir aus unserem Event nocht erhalten
$orderId = $event->getOrder()->getId();
// Im Order Repository aktualiseren wir nun unsere Bestellung und setzen den entsprechenden Custom Field Wert. Man beachte, dass in diesem Code das CustomFields Array überschrieben werden würde. Sollte man Masochist sein und noch mehr Custom Fields Bestellungen hinzufügen wollen, müsste man den Code entsprechend ändern
$this->orderRepo->update([['id' => $orderId, 'customFields' => ['b2bbasics_order_allow_partial_delivery' => filter_var($partialDeliveryAllowed, FILTER_VALIDATE_BOOLEAN)]]], $event->getContext());
}
}
4. Den Service Registrieren
Zum Schluss müssen wir unseren Subscriber noch als Service registrieren und die entsprechenden Eingabeparameter übermitteln lassen:
// src/custom/plugins/B2BBasics/src/Resources/config/services.xml in abgespeckter Form
<?xml version="1.0"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="B2BBasics\Subscriber\OrderPlacedSubscriber"> // Unser Subscriber
<argument type="service" id="request_stack" /> // Eingabeparameter RequestStack
<argument type="service" id="order.repository" /> // Eingabeparameter OrderRepository
<tag name="kernel.event_subscriber"></tag> // Subscriber Tag setzen, damit der Bums auch funktioniert
</service>
</services>
</container>
5. Ganz viele Bilder
Zum Abschluss nun, wie das am Ende aussehen soll:
Unsere Checkbox wird nun auf der Checkout Confirmation Page dargestellt und ist standardmäßig angehakt (haha wir Schlingel)
Nach dem Absenden des Formulars können wir im Network Inspector von Firefox sehen, dass der Wert unserer Checkbox mit übermittelt wurde
Im Admin-Panel von Shopware können wir bei der entsprechenden Bestellung nun sehen, dass der Wert gesetzt wurde, wie wir es wollten.
6. Epilogue
Dies war eine gigantische Reise mit viel Frust und sehr viel Hirnschmalz, das geflossen ist. Ich hoffe, dieses Beispiel hilft anderen Entwicklern bei der Lösung zweier Probleme:
- Wie man Checkboxen Formularen hinzufügt und dessen Wert verarbeitet
- Wie man den Wert eines CustomFields bei Bestellungen setzt, welches vom User beim Checkout gesetzt werden kann
Zu beidem habe ich nämlich nie etwas gefunden und ich bin froh, wenn ich auch nur einer Person damit weiterhelfen konnte.
Beste Grüße und viel Erfolg bei aktuellen und zukünftigen Projekten,
Simon