<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stefan Pilz</title>
    <description>The latest articles on DEV Community by Stefan Pilz (@stefan-freelancer).</description>
    <link>https://dev.to/stefan-freelancer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2982091%2F7bc5e3fd-4e35-4ba1-96d5-ca89258824e5.png</url>
      <title>DEV Community: Stefan Pilz</title>
      <link>https://dev.to/stefan-freelancer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stefan-freelancer"/>
    <language>en</language>
    <item>
      <title>Shopware Plugin Architecture: How to Build Update-Safe Plugins</title>
      <dc:creator>Stefan Pilz</dc:creator>
      <pubDate>Fri, 02 Jan 2026 14:59:05 +0000</pubDate>
      <link>https://dev.to/stefan-freelancer/shopware-plugin-architecture-how-to-build-update-safe-plugins-538e</link>
      <guid>https://dev.to/stefan-freelancer/shopware-plugin-architecture-how-to-build-update-safe-plugins-538e</guid>
      <description>&lt;p&gt;Shopware plugins often fail &lt;strong&gt;not because of bugs&lt;/strong&gt;, but because of &lt;strong&gt;architecture decisions&lt;/strong&gt; that don’t survive updates.&lt;/p&gt;

&lt;p&gt;Every major Shopware release exposes the same pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugins that rely on core overrides break&lt;/li&gt;
&lt;li&gt;fragile hacks stop working&lt;/li&gt;
&lt;li&gt;maintenance costs explode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is a &lt;strong&gt;practical guide&lt;/strong&gt; to building &lt;strong&gt;update-safe Shopware plugins&lt;/strong&gt; that survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minor updates&lt;/li&gt;
&lt;li&gt;major versions&lt;/li&gt;
&lt;li&gt;core refactorings&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why “update-safe” matters in Shopware
&lt;/h2&gt;

&lt;p&gt;Shopware evolves fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;core services are refactored&lt;/li&gt;
&lt;li&gt;internal logic moves&lt;/li&gt;
&lt;li&gt;admin &amp;amp; storefront architectures change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your plugin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overrides core classes&lt;/li&gt;
&lt;li&gt;depends on internal behavior&lt;/li&gt;
&lt;li&gt;patches logic at the wrong layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then &lt;strong&gt;updates will break it&lt;/strong&gt; — not &lt;em&gt;maybe&lt;/em&gt;, but &lt;em&gt;eventually&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Update-safe plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;integrate instead of replace&lt;/li&gt;
&lt;li&gt;react instead of override&lt;/li&gt;
&lt;li&gt;extend behavior without owning it&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1️⃣ Events instead of core overrides
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ The classic mistake: overriding core classes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomCartService&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CartService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks simple — and it’s fragile.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;core services change signatures&lt;/li&gt;
&lt;li&gt;internal logic shifts&lt;/li&gt;
&lt;li&gt;parent behavior becomes unpredictable&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ The correct approach: react via events
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartSubscriber&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;EventSubscriberInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getSubscribedEvents&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;CartChangedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'onCartChanged'&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;onCartChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CartChangedEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no dependency on internal implementation&lt;/li&gt;
&lt;li&gt;forward-compatible&lt;/li&gt;
&lt;li&gt;predictable execution order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;If an event exists, never override the core.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Services over static helpers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Anti-pattern: static helper classes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceHelper&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no dependency injection&lt;/li&gt;
&lt;li&gt;hard to test&lt;/li&gt;
&lt;li&gt;tightly coupled logic&lt;/li&gt;
&lt;li&gt;difficult to extend&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Proper service-based architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;MyPlugin\Service\PriceCalculator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@shopware.cart.calculator'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceCalculator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;CartCalculator&lt;/span&gt; &lt;span class="nv"&gt;$calculator&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3️⃣ Subscriber &amp;gt; Decorator (most of the time)
&lt;/h2&gt;

&lt;p&gt;Decorators are powerful — but risky.&lt;/p&gt;

&lt;p&gt;Use decorators only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return values must be modified&lt;/li&gt;
&lt;li&gt;no suitable event exists&lt;/li&gt;
&lt;li&gt;logic must be changed synchronously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subscribers are safer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they do not replace logic&lt;/li&gt;
&lt;li&gt;they survive refactors better&lt;/li&gt;
&lt;li&gt;they don’t rely on internal call order&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4️⃣ Message Bus for async &amp;amp; heavy logic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Anti-pattern: heavy logic in request lifecycle
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;external API calls&lt;/li&gt;
&lt;li&gt;batch processing&lt;/li&gt;
&lt;li&gt;AI requests&lt;/li&gt;
&lt;li&gt;expensive calculations&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Use the Message Bus (Symfony Messenger)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncProductMessage&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$productId&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncProductHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SyncProductMessage&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// async processing&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5️⃣ Typical anti-patterns
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;accessing private core services&lt;/li&gt;
&lt;li&gt;relying on undocumented entity fields&lt;/li&gt;
&lt;li&gt;overwriting Twig blocks completely&lt;/li&gt;
&lt;li&gt;business logic in controllers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always &lt;strong&gt;extend&lt;/strong&gt;, never replace.&lt;/p&gt;




&lt;h2&gt;
  
  
  6️⃣ Plugin boundaries
&lt;/h2&gt;

&lt;p&gt;A stable plugin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;owns its own logic&lt;/li&gt;
&lt;li&gt;does not own Shopware core behavior&lt;/li&gt;
&lt;li&gt;integrates via public extension points&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;would this survive a major update?&lt;/li&gt;
&lt;li&gt;does it rely on undocumented behavior?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7️⃣ Practical rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;events over overrides&lt;/li&gt;
&lt;li&gt;services over helpers&lt;/li&gt;
&lt;li&gt;subscribers over decorators&lt;/li&gt;
&lt;li&gt;message bus over sync logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;core inheritance&lt;/li&gt;
&lt;li&gt;static utilities&lt;/li&gt;
&lt;li&gt;undocumented internals&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Update-safe Shopware plugins are not about clever hacks.&lt;/p&gt;

&lt;p&gt;They are about &lt;strong&gt;respecting boundaries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your plugin reacts instead of replacing, integrates instead of controlling and stays within public APIs, updates become boring.&lt;/p&gt;

&lt;p&gt;And boring is good.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Stefan Pilz&lt;br&gt;
Shopware Freelancer &amp;amp; Plugin Developer&lt;br&gt;
Website: &lt;a href="https://stefanpilz.ltd" rel="noopener noreferrer"&gt;https://stefanpilz.ltd&lt;/a&gt; &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>shopware</category>
    </item>
    <item>
      <title>Setting Up a New Shop in Shopware: Multi-Sales-Channel Decisions Explained</title>
      <dc:creator>Stefan Pilz</dc:creator>
      <pubDate>Wed, 24 Sep 2025 19:41:39 +0000</pubDate>
      <link>https://dev.to/stefan-freelancer/setting-up-a-new-shop-in-shopware-multi-sales-channel-decisions-explained-13lp</link>
      <guid>https://dev.to/stefan-freelancer/setting-up-a-new-shop-in-shopware-multi-sales-channel-decisions-explained-13lp</guid>
      <description>&lt;h1&gt;
  
  
  Setting Up a New Shop in Shopware: Multi-Sales-Channel Decisions Explained
&lt;/h1&gt;

&lt;p&gt;When starting a new shop project in Shopware, one of the most overlooked – yet crucial – architectural decisions is &lt;strong&gt;how to structure your sales channels&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;Should you use &lt;strong&gt;different domains&lt;/strong&gt; for each market, rely on &lt;strong&gt;subfolders&lt;/strong&gt; under a single domain, or set up &lt;strong&gt;completely separate sales channels&lt;/strong&gt;? Each approach has its strengths and weaknesses for SEO, maintenance, and scalability.  &lt;/p&gt;

&lt;p&gt;In this guide, we’ll explore the three main options, highlight the SEO pitfalls (like duplicate content between &lt;code&gt;.de&lt;/code&gt; and &lt;code&gt;.at&lt;/code&gt;), and give you a clear framework for making the right decision.  &lt;/p&gt;




&lt;h2&gt;
  
  
  Option 1: Multiple domains on a single channel
&lt;/h2&gt;

&lt;p&gt;Example setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myshop.de&lt;/code&gt; → Germany
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myshop.at&lt;/code&gt; → Austria
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myshop.com&lt;/code&gt; → international customers
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong local signals (.de, .at build trust)
&lt;/li&gt;
&lt;li&gt;Customers immediately recognize a domain that feels “local”
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If content is identical (e.g. Germany and Austria both in German), Google sees it as &lt;strong&gt;duplicate content&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Needs proper &lt;code&gt;hreflang&lt;/code&gt; tags and/or localized differences to avoid SEO issues
&lt;/li&gt;
&lt;li&gt;Slightly more complex to manage SSL, redirects, and tracking
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Option 2: Subfolders on one main domain
&lt;/h2&gt;

&lt;p&gt;Example setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myshop.com/de&lt;/code&gt; → Germany
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myshop.com/at&lt;/code&gt; → Austria
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myshop.com/en&lt;/code&gt; → international customers
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All countries benefit from one strong domain authority
&lt;/li&gt;
&lt;li&gt;Easier to manage (single SSL, single analytics property)
&lt;/li&gt;
&lt;li&gt;Lower risk of duplicate content across markets
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less local perception: Austrian customers may trust &lt;code&gt;myshop.at&lt;/code&gt; more than &lt;code&gt;myshop.com/at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Legal or tax differences are harder to reflect in one channel
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Option 3: Separate sales channels with individual domains
&lt;/h2&gt;

&lt;p&gt;Example setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myshop.de&lt;/code&gt; → B2C Germany
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myshop.at&lt;/code&gt; → B2C Austria
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;businessshop.de&lt;/code&gt; → B2B customers
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maximum flexibility: different assortments, prices, legal texts, shipping methods
&lt;/li&gt;
&lt;li&gt;Clear separation between B2B and B2C if needed
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highest maintenance effort: products, categories, and CMS content must be updated separately
&lt;/li&gt;
&lt;li&gt;Domains may compete with each other in search results
&lt;/li&gt;
&lt;li&gt;More technical complexity (plugins, tracking, SSL per domain)
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The duplicate content trap (.de vs .at)
&lt;/h2&gt;

&lt;p&gt;A common mistake: running &lt;code&gt;myshop.de&lt;/code&gt; and &lt;code&gt;myshop.at&lt;/code&gt;, both in German with nearly identical product and category content.  &lt;/p&gt;

&lt;p&gt;Google treats this as &lt;strong&gt;duplicate content&lt;/strong&gt;, and one domain will almost always lose rankings.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;hreflang&lt;/code&gt; tags to indicate which content belongs to which market
&lt;/li&gt;
&lt;li&gt;Introduce small but meaningful differences: local pricing, shipping info, legal texts, or editorial content
&lt;/li&gt;
&lt;li&gt;Reconsider if separate ccTLDs are really necessary – subfolders might be the safer SEO play
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Visual comparison
&lt;/h2&gt;

&lt;p&gt;Here’s a radar chart that compares the three approaches across &lt;strong&gt;SEO, effort, and flexibility&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiznvqveurfj070vahu9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiznvqveurfj070vahu9e.png" alt=" " width="800" height="651"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;SEO Impact&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Flexibility&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiple domains&lt;/td&gt;
&lt;td&gt;+ Local signals, trust &lt;br&gt; - Duplicate content risk&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Merchants focusing on local branding (.de/.at)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subfolders&lt;/td&gt;
&lt;td&gt;+ Strong domain authority &lt;br&gt; - Less local perception&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;SEO-driven, international expansion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate channels&lt;/td&gt;
&lt;td&gt;+ Full separation &lt;br&gt; - Domains may compete&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;B2B/B2C split, different assortments or legal setups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Shopware gives you full flexibility. The right choice depends on three factors:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SEO:&lt;/strong&gt; Avoid duplicate content and configure &lt;code&gt;hreflang&lt;/code&gt; correctly
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business:&lt;/strong&gt; Are assortments, prices, and legal rules different per market?
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Do you need the trust of local ccTLDs, or do you prefer consolidating global SEO power on one domain?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;Golden rule:&lt;/strong&gt; &lt;em&gt;As few channels as possible, as many as necessary.&lt;/em&gt;  &lt;/p&gt;




&lt;h2&gt;
  
  
  Next step
&lt;/h2&gt;

&lt;p&gt;Planning to launch a new Shopware store?&lt;br&gt;&lt;br&gt;
Before making your decision on multi-channel architecture, make sure your setup aligns with your SEO strategy and business goals.  &lt;/p&gt;

&lt;p&gt;💬 I’ve supported multiple Shopware rollouts – if you’d like insights or a sanity check on your setup, feel free to reach out!  &lt;/p&gt;

</description>
      <category>shopware</category>
      <category>ecommerce</category>
      <category>seo</category>
      <category>architecture</category>
    </item>
    <item>
      <title>What Developers Need to Know About the Shopware 6.7 Update</title>
      <dc:creator>Stefan Pilz</dc:creator>
      <pubDate>Tue, 06 May 2025 13:23:19 +0000</pubDate>
      <link>https://dev.to/stefan-freelancer/what-developers-need-to-know-about-the-shopware-67-update-59pd</link>
      <guid>https://dev.to/stefan-freelancer/what-developers-need-to-know-about-the-shopware-67-update-59pd</guid>
      <description>&lt;p&gt;&lt;em&gt;Shopware 6.7&lt;/em&gt; is a major release packed with significant changes, especially relevant for developers. This article provides a structured overview of the most important new features, technical changes, and migration tips so you can upgrade your Shopware project successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  New Features and Improvements in Shopware 6.7
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accessibility Compliance:&lt;/strong&gt; Shopware 6.7 fully implements the European Accessibility Act (EAA) requirements with comprehensive accessibility updates now &lt;strong&gt;enabled by default&lt;/strong&gt;. For developers, this means your themes and plugins must now comply with stricter accessibility standards, including enforced UI adjustments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modernized Frontend Toolchain (Webpack → Vite):&lt;/strong&gt; The admin frontend build system has been switched from Webpack to &lt;strong&gt;Vite&lt;/strong&gt;, speeding up builds and improving development cycles.&lt;br&gt;
Developers with admin UI plugins will need to migrate from &lt;code&gt;webpack.config.js&lt;/code&gt; to &lt;code&gt;vite.config.js&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vue 3 and Pinia:&lt;/strong&gt; Shopware 6.7 now runs fully on Vue 3. The legacy Vue 2 "migration build" has been removed.&lt;br&gt;
All admin UIs and extensions must be compatible with Vue 3.&lt;br&gt;
Additionally, the state management system has shifted from Vuex to &lt;strong&gt;Pinia&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Improved Caching System:&lt;/strong&gt; Caching has been optimized to enhance performance and scalability.&lt;br&gt;
Cache invalidations now happen &lt;strong&gt;asynchronously&lt;/strong&gt; (every 5 minutes by scheduled task).&lt;br&gt;
The separate Store API cache layer has been removed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Updated Tech Stack Requirements:&lt;/strong&gt; Shopware 6.7 now requires PHP &lt;strong&gt;8.2+&lt;/strong&gt; and Node.js &lt;strong&gt;20+&lt;/strong&gt;. Core libraries like PHPUnit, Doctrine DBAL, and OAuth2 have been updated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New Migration Toolkit:&lt;/strong&gt; Shopware provides a developer tool to assist with &lt;strong&gt;plugin migration&lt;/strong&gt;, capable of linting, auto-fixing, and formatting code.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Technical and Breaking Changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vite Frontend Build:&lt;/strong&gt; Any plugins using Webpack must migrate to Vite. Shopware provides migration guides for creating &lt;code&gt;vite.config.js&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removal of Vue 2 Migration Mode:&lt;/strong&gt; Legacy compatibility with Vue 2 is gone. All admin code must now use Vue 3 APIs and patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Switch from Vuex to Pinia:&lt;/strong&gt; Store definitions must be rewritten for Pinia. Access patterns shift from &lt;code&gt;this.$store&lt;/code&gt; to Pinia store instances.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New Meteor Admin Components:&lt;/strong&gt; Admin UI now uses Meteor Components aligned with the Shopware Design System. Use the &lt;code&gt;deprecated&lt;/code&gt; attribute to temporarily fallback to legacy components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removed Legacy Systems:&lt;/strong&gt; Deprecated templates and PHP methods have been removed, e.g., legacy product detail page templates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OAuth2 Token API Strictness:&lt;/strong&gt; The &lt;code&gt;/api/oauth/token&lt;/code&gt; endpoint now requires RFC-compliant requests only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugin Uninstall Behavior:&lt;/strong&gt; Plugins must ensure they clean up their database artifacts when &lt;code&gt;keepUserData = false&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migration Guide for Developers (Upgrading to 6.7)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;System Requirements:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.2+
&lt;/li&gt;
&lt;li&gt;Node.js 20+
&lt;/li&gt;
&lt;li&gt;Update your environment and test on staging first.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Theme Compatibility:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shopware’s premium themes won't be updated for 6.7.
&lt;/li&gt;
&lt;li&gt;Review custom themes for deprecated structures.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Frontend Changes for Accessibility:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update themes to meet new accessibility rules.
&lt;/li&gt;
&lt;li&gt;Adjust template structure where needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update Admin Plugins:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrate to &lt;code&gt;vite.config.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use Vue 3 + Composition API
&lt;/li&gt;
&lt;li&gt;Replace Vuex with Pinia
&lt;/li&gt;
&lt;li&gt;Update UI components to Meteor versions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Test All API Integrations:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure OAuth requests are RFC-compliant
&lt;/li&gt;
&lt;li&gt;Review response changes and endpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use the Plugin Migration Tool:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automate detection and correction of deprecated code.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Test Everything Thoroughly:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the upgrade on a staging site
&lt;/li&gt;
&lt;li&gt;Validate frontend, admin, plugins, and integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 Need help upgrading to Shopware 6.7?
&lt;/h2&gt;

&lt;p&gt;Whether you need migration support, plugin refactoring, or help reviewing your custom themes – I’m happy to assist as a technical partner or consultant.&lt;br&gt;&lt;br&gt;
👉 Message me here on dev.to or visit my &lt;a href="https://stefanpilz.ltd/en/shopware-freelancer" rel="noopener noreferrer"&gt;website&lt;/a&gt; to learn more.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>shopware</category>
    </item>
    <item>
      <title>Shopware Apps vs. Plugins – Real-Life Example with Code</title>
      <dc:creator>Stefan Pilz</dc:creator>
      <pubDate>Thu, 10 Apr 2025 19:14:56 +0000</pubDate>
      <link>https://dev.to/stefan-freelancer/shopware-apps-vs-plugins-real-life-example-with-code-50h3</link>
      <guid>https://dev.to/stefan-freelancer/shopware-apps-vs-plugins-real-life-example-with-code-50h3</guid>
      <description>&lt;p&gt;Shopware 6 provides two powerful ways to extend and customize your shop: &lt;strong&gt;Apps&lt;/strong&gt; and &lt;strong&gt;Plugins&lt;/strong&gt;. Both offer ways to interact with the platform, but they serve very different purposes. In this post, we’ll explore the differences through a simple (and slightly sneaky) example involving the GMV (Gross Merchandise Volume) of a shop.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We’re Building
&lt;/h2&gt;

&lt;p&gt;We’ll create a Shopware &lt;strong&gt;App&lt;/strong&gt; that queries the total order value of the shop (GMV) via the Shopware Admin API.&lt;/p&gt;

&lt;p&gt;Then, we’ll implement a &lt;strong&gt;Plugin&lt;/strong&gt; that intercepts those order values and manipulates them – effectively feeding fake data back to the app. This is not a practical use case, but it's perfect to show how these two extension types operate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shopware Apps: External Logic with API Access
&lt;/h2&gt;

&lt;p&gt;Apps in Shopware 6 are &lt;strong&gt;external applications&lt;/strong&gt; that run on a separate server (not inside Shopware) and communicate with the shop via a secure API integration.&lt;/p&gt;

&lt;p&gt;When an app is installed, it registers an &lt;strong&gt;integration&lt;/strong&gt; with specific access scopes (permissions), which the shop admin has to approve. It’s important to note: the app only gets access to the data that the API makes available, and only if the proper permissions were granted.&lt;/p&gt;

&lt;p&gt;Here’s an example &lt;code&gt;manifest.xml&lt;/code&gt; for our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;manifest&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt; 
          &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Manifest/Schema/manifest-2.0.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;name&amp;gt;&lt;/span&gt;FakeGmvReporting&lt;span class="nt"&gt;&amp;lt;/name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Fake GMV Reporting&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"de-DE"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Fake GMV Reporting&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;Test app for reading orders to calculate GMV.&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;author&amp;gt;&lt;/span&gt;Friendly Hacker&lt;span class="nt"&gt;&amp;lt;/author&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;license&amp;gt;&lt;/span&gt;MIT&lt;span class="nt"&gt;&amp;lt;/license&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/meta&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;setup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;registrationUrl&amp;gt;&lt;/span&gt;http://fake-gmv-app:3000/register&lt;span class="nt"&gt;&amp;lt;/registrationUrl&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;secret&amp;gt;&lt;/span&gt;supersecret&lt;span class="nt"&gt;&amp;lt;/secret&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/setup&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;webhooks&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;webhook&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"appActivated"&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"http://fake-gmv-app:3000/activated"&lt;/span&gt; &lt;span class="na"&gt;event=&lt;/span&gt;&lt;span class="s"&gt;"app.activated"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/webhooks&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;permissions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;read&amp;gt;&lt;/span&gt;order&lt;span class="nt"&gt;&amp;lt;/read&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;read&amp;gt;&lt;/span&gt;order_line_item&lt;span class="nt"&gt;&amp;lt;/read&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;read&amp;gt;&lt;/span&gt;currency&lt;span class="nt"&gt;&amp;lt;/read&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;read&amp;gt;&lt;/span&gt;system_config&lt;span class="nt"&gt;&amp;lt;/read&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/permissions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/manifest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Shopware generates an integration with the exact set of API scopes you declare in the manifest.&lt;br&gt;&lt;br&gt;
Apps cannot access more than what’s explicitly listed – unless, of course, the scopes are overly broad.&lt;br&gt;&lt;br&gt;
👉 As a shop owner, always review which data tables are requested – &lt;code&gt;system_config&lt;/code&gt; is commonly used to extract sensitive config and credentials!&lt;/p&gt;


&lt;h2&gt;
  
  
  🛡 OAuth Flow: /register, /confirm, /activated
&lt;/h2&gt;

&lt;p&gt;The app follows the standard Shopware app lifecycle:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;/register&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Shopware calls this during installation. The app responds with a cryptographic proof.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shopUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shop-url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shopId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shop-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shopId&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;shopUrl&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;APP_NAME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;confirmation_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://fake-gmv-app:3000/confirm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;/confirm&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Shopware sends client credentials so the app can request access tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/confirm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;shopId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;shopUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://shopware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./shop-token.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ In this example, we store the API credentials in a local file – &lt;strong&gt;don’t do this in production!&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This is just for demonstration. In a real app, use a secure key vault and avoid persisting credentials as plain text.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;/activated&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/activated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✅ App was activated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;ℹ️ Note: Our Shopware instance is assumed to be running at &lt;code&gt;http://shopware&lt;/code&gt;, and our app lives at &lt;code&gt;http://fake-gmv-app&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  📊 API Endpoint: &lt;code&gt;/gmv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the core logic of our app – fetch orders and calculate GMV.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/gmv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./shop-token.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/order`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gmv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;gmv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gmv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isoCode&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Shopware Plugins: Internal Logic, Full Power
&lt;/h2&gt;

&lt;p&gt;Plugins, unlike apps, run &lt;strong&gt;inside the Shopware core&lt;/strong&gt; and have access to everything – services, database, events, etc.&lt;/p&gt;

&lt;p&gt;Let’s modify our GMV example:&lt;br&gt;&lt;br&gt;
We’ll create a plugin that recognizes requests made by our app – and manipulates the data being returned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;onOrderLoaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EntityLoadedEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;AdminApiSource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$criteria&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Criteria&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$criteria&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EqualsFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'FakeGmvReporting'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$apps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$criteria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEntities&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$apps&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getIntegrationId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getIntegrationId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEntities&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$entity&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;OrderEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAmountTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAmountNet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.84&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setShippingTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTaxStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gross'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🤯 The plugin detects API requests made by the app and changes the order data &lt;strong&gt;on the fly&lt;/strong&gt;!&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Shopware App&lt;/th&gt;
&lt;th&gt;Shopware Plugin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs where?&lt;/td&gt;
&lt;td&gt;External server&lt;/td&gt;
&lt;td&gt;Inside the Shopware environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;No deployment on Shopware required&lt;/td&gt;
&lt;td&gt;Installed as plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Needs explicit permissions&lt;/td&gt;
&lt;td&gt;Has full access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use Cases&lt;/td&gt;
&lt;td&gt;External services, integrations&lt;/td&gt;
&lt;td&gt;Deep customizations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example&lt;/td&gt;
&lt;td&gt;Read orders via Admin API&lt;/td&gt;
&lt;td&gt;Modify data before it's returned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Need Help with Your Own Shopware App or Plugin?
&lt;/h2&gt;

&lt;p&gt;If you're planning to develop your own Shopware 6 app or plugin and could use some expert support, I’m happy to help.&lt;br&gt;&lt;br&gt;
From figuring out the Shopware API to building a clean plugin structure — I’ve got your back.&lt;/p&gt;

&lt;p&gt;You can find more info about my services here: &lt;a href="https://stefanpilz.ltd/en/shopware-freelancer/shopware-plugin-development" rel="noopener noreferrer"&gt;Shopware 6 plugin development&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>shopware</category>
      <category>ecommerce</category>
      <category>node</category>
      <category>php</category>
    </item>
    <item>
      <title>Things I Wish I Knew Before Building My First Shopware Plugin #shopware #symfony #php</title>
      <dc:creator>Stefan Pilz</dc:creator>
      <pubDate>Thu, 27 Mar 2025 14:42:02 +0000</pubDate>
      <link>https://dev.to/stefan-freelancer/things-i-wish-i-knew-before-building-my-first-shopware-plugin-shopware-symfony-php-195a</link>
      <guid>https://dev.to/stefan-freelancer/things-i-wish-i-knew-before-building-my-first-shopware-plugin-shopware-symfony-php-195a</guid>
      <description>&lt;h1&gt;
  
  
  🧠 Things I Wish I Knew Before Building My First Shopware Plugin
&lt;/h1&gt;

&lt;p&gt;Building a custom plugin for Shopware 6 can feel exciting, especially if you come from a Symfony or PHP background. But once you dive in, the learning curve can be surprisingly steep. After developing several Shopware plugins, here are some things I &lt;em&gt;really&lt;/em&gt; wish I had known earlier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Plugin or App?&lt;/li&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;Docs &amp;amp; Core Code&lt;/li&gt;
&lt;li&gt;Cache Issues&lt;/li&gt;
&lt;li&gt;Twig Logic&lt;/li&gt;
&lt;li&gt;Testing&lt;/li&gt;
&lt;li&gt;Shopware Services&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Need Help?&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Plugin or App? Know the Difference &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Shopware offers two extension systems: &lt;strong&gt;plugins&lt;/strong&gt; and &lt;strong&gt;apps&lt;/strong&gt;. If you just need to display something, handle webhooks, or connect to an external API, apps are often simpler and update-safe. Plugins, on the other hand, offer deeper system-level integration. I jumped into plugins without realizing an app might've been enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Evaluate your needs carefully – plugins are powerful, but apps are easier to maintain and don't require direct code deployment on the shop server.&lt;/p&gt;




&lt;h3&gt;
  
  
  Embrace Dependency Injection Early &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Shopware is built on Symfony, which means everything revolves around dependency injection. Your custom services, event subscribers, and classes won’t work unless properly defined in &lt;code&gt;services.xml&lt;/code&gt; (or &lt;code&gt;services.yaml&lt;/code&gt;, depending on setup).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Don't assume your service is automatically injected. Use &lt;code&gt;bin/console debug:container&lt;/code&gt; to see what's registered and where.&lt;/p&gt;




&lt;h3&gt;
  
  
  Read the Docs – Then Read the Core Code &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The Shopware developer docs are helpful, but they don't cover everything. Some use cases (like customizing admin modules or extending checkout logic) may be outdated or missing. The best resource? The core source code and existing plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Browse the Shopware GitHub repos and inspect how they build core features. Real examples are better than theory.&lt;/p&gt;




&lt;h3&gt;
  
  
  Cache: Your Best Friend and Worst Enemy &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Made a change but nothing happens? Welcome to Shopware caching. You’ll often need to clear the cache using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/console cache:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, remember to recompile the storefront or admin if working on those parts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/console theme:compile
bin/console administration:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Automate cache clearing in your local setup. It'll save your sanity.&lt;/p&gt;




&lt;h3&gt;
  
  
  Avoid Business Logic in Twig &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;It's tempting to put logic directly into your Twig templates, especially for quick fixes. But it's a trap. Shopware provides &lt;strong&gt;ViewSubscribers&lt;/strong&gt; and &lt;strong&gt;Decorators&lt;/strong&gt; for a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt; Keep templates clean. Move logic to PHP and inject variables into views.&lt;/p&gt;




&lt;h3&gt;
  
  
  Start Testing Early &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When I built my first plugin, I skipped tests thinking it would be faster. Big mistake. Even a few unit or integration tests can save hours of debugging later on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestion:&lt;/strong&gt; Add a basic PHPUnit setup. Even if it's just one or two tests at first, it pays off.&lt;/p&gt;




&lt;h3&gt;
  
  
  Reuse Existing Shopware Services &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Need to upload files? Send emails? Generate thumbnails? Shopware probably already has a service for that. Don't reinvent the wheel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; The &lt;code&gt;media.service&lt;/code&gt; handles file uploads and is way more robust than anything I could’ve written on the fly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Shopware plugin development is incredibly powerful but comes with complexity. Understanding the underlying Symfony architecture, leveraging existing services, and writing clean, maintainable code can make the difference between a headache and a success story.&lt;/p&gt;

&lt;p&gt;If you're just starting out: stay curious, stay patient, and read the core.&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 What about you?
&lt;/h2&gt;

&lt;p&gt;What was your biggest "aha!" moment when working with Shopware (or Symfony)? Drop it in the comments – I'm always curious to hear other devs' lessons learned!&lt;/p&gt;




&lt;h2&gt;
  
  
  Need help with Shopware plugin development? &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I offer freelance services for custom plugin development, troubleshooting, and consulting. &lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://stefanpilz.ltd/en/shopware-freelancer/shopware-plugin-development" rel="noopener noreferrer"&gt;https://stefanpilz.ltd/en/shopware-freelancer/shopware-plugin-development&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
