Verständnisfrage Access Key

Guten Abend,
aktuell steige ich als Azubi in Shopware ein und da gibt einige Punkte, die mir noch nicht klar sind.
Aktuell arbeite ich an einem Plugin (Stock Notification Plugin). Nun möchte ich erreichen, dass wenn Stocks manuell in der Administration eingetragen werden Mails an User geschickt werden, wenn stock > 0 ist.

<?php declare(strict_types=1);

namespace StockNotificationPlugin\Controller;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use StockNotificationPlugin\Service\StockAlertService;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Symfony\Component\Routing\Annotation\Route;
use Shopware\Core\Framework\Uuid\Uuid;

/**
 * @RouteScope(scopes={"store-api"})
 */
class EmailAddressController extends AbstractController
{

    private EntityRepositoryInterface $stockNotificationRepository;
    private ?StockAlertService $alertService;
    private EntityRepositoryInterface $productRepository;


    public function __construct(EntityRepositoryInterface $stockNotificationRepository, StockAlertService $alertService, EntityRepositoryInterface $productRepository)
    {
        $this->stockNotificationRepository = $stockNotificationRepository;
        $this->alertService = $alertService;
        $this->productRepository = $productRepository;
    }

    /**
     * @Route("/store-api/v{version}/StockNotificationPlugin/email", name="store-api.StockNotificationPlugin.email", methods={"POST"})
     */
    public function getEmailAddress(Request $request, Context $context) : Response {

        $productId = $request->request->get("productId");

        $criteria = new Criteria();
        $criteria->addFilter(new EqualsFilter("id", $productId));

        $productName = $this->productRepository->search($criteria,$context)->first()->get('name');
        if(!$productName){
            $productName = "product";
        }
        
        $email = $request->request->get('Email');

        $salesChannelId = $context->getSource()->getSalesChannelId();
        
        $this->stockNotificationRepository->upsert([
            ["id"=> Uuid::randomHex(),"email" => $email,"productId" => $productId, "salesChannelId" => $salesChannelId, "productName" => $productName, "isSent" => false]
        ], $context);

        $this->alertService->notificationProcess();
        $this->addFlash("success", "Your notification service was registered successfully!" );
        return $this->redirectToRoute('frontend.detail.page', ['productId' => $productId]);

    }
}

Bisher sieht mein Controller so aus. Jetzt möchte ich den sw-access-key im Header übergeben (damit ich store api nutzen kann), allerdings werde ich aus der Doku nicht so wirklich schlau und ich weiß nicht, wie ich das am besten machen könnte.

Vielleicht könnt ihr mir ja helfen :slight_smile:

Verstehe den Zusammenhang von API-Controller und sw-access-key nicht wirklich.

Für das Frontend gibt es eine Storefront-API JavaScript Klasse, mit der du Storefront-API Calls ausführen kannst. Über die Klasse kannst du Requests direkt senden ohne dich um etwas kümmern zu müssen.

Danke für deine rasche Antwort.

Wenn ich eine Email mithilfe des Controllers hinzufügen möchte, bekomme ich die folgende Fehlermeldung:

"code": "0",
"status": "401",
"title": "Unauthorized",
"detail": "Header \"sw-access-key\" is required.",

Ich frage mich, wie ich am besten den Access Key mit dem Header senden kann. Gibt es dafür einen „besten“ Weg?

Wie oben geschrieben, wenn du den httpClient nutzt, dann ist alles im Header mit drin, ohne dass du dich um etwas kümmern musst.

Dann werde ich das versuchen. Also kann ich meine Class so lassen und nur den httpClient einbinden?

Ich habe eine URL verlinkt, da ist ein komplettes Beispiel zu sehen. Deine Code ist Backend… was du machen musst/möchtest ist Frontend, oder nicht?!

Ja, tut mir leid. Ich erkläre es nochmal:

<?php declare(strict_types= 1);

namespace StockNotificationPlugin\Storefront\Controller;

use Shopware\Core\Framework\Context;
use Shopware\Storefront\Controller\StorefrontController;
use StockNotificationPlugin\Controller\EmailAddressController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route(defaults={"_routeScope"={"storefront"}})
 */
class EmailNotificationController extends StorefrontController
{

    private EmailAddressController $emailAddressController;

    public function __construct(EmailAddressController $emailAddressController)
    {
        $this->emailAddressController = $emailAddressController;
    }

    /**
     * @Route("/email", name="frontend.email.email", methods={"GET", "POST"}, defaults={"XmlHttpRequest"=true})
     */
    public function getEmailAddress(EmailAddressController $emailAddressController, Request $request, Context $context)
    {
       return $this->emailAddressController->getEmailAddress($request, $context);
    }
}

Ich habe mich an das Beispiel gehalten und die Store API Route im Frontend verfügbar gemacht. Die Email werden jetzt wieder in der Datenbank gespeichert, Jetzt müsste ich eigentlich nur noch den httpClient nutzen, um die Route korrekt zu requesten (So wie ich das Beispiel verstanden habe).

Arbeitet der httpClient korrekt, kann ich dann auch den Subscriber in der Administration Produkt-Übersicht nutzen, da ich dann auch den sw-access-key korrekt übergebe.

Danke auf jeden Fall für deine Hilfe. Das bringt mich weiter.

Du hast in deinem Ausgangspost eine API, welche Kunden/E-Mails in die Datenbank einträgt, damit diese eine Info bekommen.

Jetzt möchtest du ja in einem zweiten Schritt E-Mails verschicken, wenn der Artikel wieder verfügbar ist. Dafür solltest du dich per Subscriber in den StockUpdater einklinken und dann den E-Mail-Versand initiieren oder du löst das über einen Cronjob.

Das hat mit dem Frontend nichts zu tun. Im Backend gib es übrigens ein AdminClient, der auf dem httpClient basiert. Du kannst du natürlich nicht mit der Store-API arbeiten, falls du Backend-API-Calls ausführen möchtest. Aber das hat mit dem aktuellen Fall nichts zu tun. Generelle Anmerkung.

Ok, verstanden. Allerdings erhalte ich in der Administration die Fehlermeldung:

"code": "9",
"status": "401",
"title": "The resource owner or authorization server denied the request.",
"detail": "Missing \"Authorization\" header",

CronJob funktioniert. Nur die manuelle Änderung der Stocks im Admin Bereich führen zu keinem Event.

Da musst du schon etwas mehr Information nennen. Wie und wo setzt du denn den (API) Request ab?

Ich glaube, ich bin auf dem falschen Dampfer. Am besten fange ich nochmal neu mit meinem Plugin an, da mein Code jetzt schon aussieht wie Kraut und Rüben. Ich möchte nochmal anfangen, um ein besseren Verständnis zu erhalten.

Mein Ziel ist es:

  • Sollte ein Product Stock = 0 sein, wird anstatt des Shopping Cart Button eine Email Form gerendert, in der sich der Kunde eintragen kann.
  • Die Email wird in einer Tabelle in der Datenbank mit allen notwendigen Daten gespeichert.
  • Sollte das Produkt wieder verfügbar sein, wird eine Email an den Kunden versendet werden
    → CronJob ist die eine Möglichkeit, ich möchte es aber am liebsten so lösen, dass bei einer manuellen Änderung (die durch einen Event Subscriber subscript wird) Emails versendet werden

Mein bisheriger Aufbau des Plugins:

  • Controller (s. o.)
  • Meine Services, die für den Versand der Mail verantwortlich sind
  • Mein Twigtemplate, dass gerendert wird
  • Meine Entity mit EntityDefinition und Collection
  • Mein Subscriber

Frage:

  • Nutze ich Storefront oder die Store API als Route für meinen Controller? Oder was würdest du in diesem Fall machen? Leider ist es immer noch schwer für mich, zu entscheiden, wann ich was nutze.
    → Das wäre mein erster Schritt, auf den ich dann alles aufbauen kann.

P.S.: Danke für deine Geduld einem blutigen Anfänger zu helfen :smiley:

In der Storefront fügst du ein input type=email hinzu. Das schickst du per JavaScript httpClienet an deine Storefront-API Controller. Somit hast du die E-Mail schon einmal in der Datenbank.

Jetzt baust du dir ein Subscriber, der sich an das „StockWritten“ Event, oder wie auch immer das heißen mag, dran hängt. Der Controller ruft dann eine von dir zu erstellende Klasse auf, in der falls zutreffend E-Mails verschickt werden. Controller als auch die weitere Klasse sind PHP Backend.

Soweit so klar?

Ja, soweit habe ich das verstanden

Mit der Input Form in der Storefront kann ich ja einfach die Detailseite überschreiben. Dazu nehme ich einfach ein Custom Twig Template


{% sw_extends '@Storefront/storefront/page/product-detail/buy-widget-form.html.twig' %}

{% block  page_product_detail_buy_form_inner %}
    {% if  page.product.availableStock == 0 %}
        <div class="p-3 rounded shadow" >
            <h5 class="text-center" style="color: #b91313;"> &otimes; Unfortunately the product is not available at the moment.</h5>
            <p class="text-center text-muted">We will inform you when the product is available again.<br>Just enter your email address.</p>
            <form class="input-group p-0" method="POST" action="{{ path('frontend.email.email') }}" data-form-csrf-handler="true" autocomplete="off">
                <input type="text" name="productId" id="productId" value= "{{ page.product.id}}" hidden>
                <input type="email" id="email" class="form-control rounded-start" placeholder="Email" name="Email" aria-label="Email" aria-describedby="button-addon2" required="required">
                <button class="btn btn-dark" type="submit" id="button-addon2">Send</button>
                {{ sw_csrf('frontend.email.email') }}
            </form>
        </div>
    {% else %}
        {{ parent() }}
    {% endif %}
{% endblock %}

So habe ich das bisher gelöst. Das sollte so in Ordnung sein oder?

Möglicherweise habe ich dich vorher unnötig verwirrt. Wenn du das nun so machst per {{ path(‚frontend.email.email‘) }}, dann wird das natürlich kein httpClient Request, der an eine Storefront-API geht, sondern dafür benötigst du ein Controller, der die Route abfängt.

Das sind zwei unterschiedliche Ansätze.

Ist httpClient hier der bessere Weg, um das zu lösen?

Da gibt es kein besser oder schlechter. Das sind zwei unterschiedliche Ansätze. Einmal schickst du ein form ab, dass ein Neuladen der gesamten Webseite notwendig macht. Das musst du mit einem Controller anfangen.

Alternativ könntest du ein httpClient Request an eine Storefront-API senden, da würde sich im Frontend dann nichts ändern, außer du nutzt die Antwort des Storefront-API Requests und änderst das HTML.

API implementiert man, wenn man darauf ohne Storefront zugreifen können möchte. In deinem Fall wird das vermutlich nicht der Fall sein, also könntest du auch einen ganz normalen Controller erstellen.

Alles klar. Dann werde ich mich heute Abend nochmal dran setzen. Danke!

Meine allerletzte Frage für heute (versprochen): Möchte ich jetzt mit meinem Subscriber auf ein Event in der Admin Produkt Liste subscriben. Dann sollte das eigentlich auch ohne Storefront-API funktionieren, oder?

Ich glaube, du verstehst noch nicht ganz, wie die Admin-Oberfläche funktioniert.

Das ist VueJS. Damit die Seite nicht ständig neu laden muss, wird alles per API Requests gesteuert. Diese werden vom API Controller verarbeitet und per PHP Klasse(n) abgearbeitet.

In VueJS etwas abzufangen macht nur Sinn, wenn du die Admin-Oberfläche in dem Moment ändern möchtest.

Was du möchtest ist per Subscriber zu überwachen, wann inStock aktualisiert wird. Das passiert innerhalb einer PHP-Klasse, daher schreibst du auch deinen Subscriber per PHP.

Dafür benötigst du weder eine Sorefront- noch Admin-API!