---
url: >-
/docs/resources/references/adr/2024-12-19-offer-html-alternative-to-our-pdf-standard-document.md
---
# "\[A11y] Offer HTML alternative to our pdf standard documents"
::: info
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2024-12-19-offer-html-alternative-to-our-pdf-standard-document.md)
:::
## Context
To comply with Web Content Accessibility Guidelines (WCAG), we aim to make Shopware 6's document solution accessible (A11y-compliant). This ensures that our solution remains usable while meeting legal requirements and making documents accessible to customers with disabilities.
Currently, our PDF generation library, DomPDF, does not meet accessibility standards, posing a significant challenge.
## Decision
We have decided to make HTML documents available in addition to PDF documents, as these are more accessible.
* **Better for Accessibility**: HTML is naturally organized, making it easier for accessibility tools to read and present content to people with disabilities.
* **Lack of Support**: As our current PDFs lack support for accessibility. Few tools, especially in PHP, can create accessible tagged PDFs, making it difficult to maintain PDF accessibility.
* **Industry Trends**: Many organizations are already moving from PDFs to HTML for accessibility. For example, government websites have been required to meet accessibility standards since the early 2000s. Most of them now use HTML for most of their content because it meets these standards better.
Providing HTML documents aligns with these trends and ensures we are using best practices for accessibility.
### Affected Areas
We will integrate HTML A11y document support in the following areas:
1. **Document Type Support**:
* Support includes all document types Shopware provides by default, `invoice`, `delivery note`, `credit note`, and `cancellation invoice`. Extensions must adapt themselves.
2. **Administration**:
* **Order Detail Page**: Option to download HTML alongside PDF for each document type.
* **Document Settings**: Toggle to generate HTML documents.
3. **Storefront**:
* **Order History**: Customers can access HTML and PDF versions of documents.
4. **Flow Builder**:
* This setup requires no additional special actions, and merchants can customize file generation for "Generate documents" in the `Document Settings`
5. **Email Delivery**:
* Enhance the original email by including a link to the HTML document. Customers will need to log in to access the document, and additional guidance will be provided.
* We canβt attach the HTML file directly due to issues with "virus scanners", as many email providers do not allow HTML file attachments. Instead we will provide a link inside the Email.
* A lot of the major platforms (Microsoft, Google, Amazon, etc.) will also email a summary with a link to the customer account for things like Azure/Google Cloud/etc.
### Core concept
#### Document Template
1. **Adjust Twig Template for A11y**:
* Modify the `html.twig` templates to support accessibility (A11y) by adding elements like `tabindex` and appropriate CSS styles.
`src/Core/Framework/Resources/views/documents/invoice.html.twig`:
```twig
{% block document_headline %}
{% endblock %}
```
`src/Core/Framework/Resources/views/documents/style_base_html.css.twig`:
```twig
{% block document_style_html %}
body {
max-width: 1200px;
margin: auto;
font-size: 14px;
line-height: 18px;
}
...
{% endblock %}
```
2. **Metadata and Security**:
* The generation date of the HTML will be "fingerprinted" by adding a metadata header. This allows users to track the creation date of the document.
* Implement a Content-Security-Policy meta-tag to minimize XSS attack risks, such as disallowing JavaScript and Restricts to the same domain, protecting against base URL manipulation.
Added new Twig block for metadata`src/Core/Framework/Resources/views/documents/base.html.twig`:
```twig
{% block document_head_meta_protection %}
{% endblock %}
```
#### Core
1. **Abstract Class for Multi-Format Rendering**
* We will introduce an abstract class, `src/Core/Checkout/Document/Service/AbstractDocumentTypeRenderer`, to support rendering multiple document types, including `PDF` and `HTML`.
```php
abstract class AbstractDocumentTypeRenderer
{
abstract public function render(RenderedDocument $document): string;
}
class HtmlRenderer extends AbstractDocumentTypeRenderer
{
public function render(RenderedDocument $document): string
{
$content = $this->documentTemplateRenderer->render(
...$options
);
$document->setContentType(self::FILE_CONTENT_TYPE);
$document->setFileExtension(self::FILE_EXTENSION);
$document->setContent($content);
return $content;
}
}
class PdfRenderer extends AbstractDocumentTypeRenderer {}
```
2. **Service Registration**:
* We need to use the service tag `document_type.renderer` for the `Shopware\Core\Checkout\Document\Service\DocumentFileRendererRegistry` to recognize this service. This is essential for the proper registration and functioning of the `HtmlRenderer`.
```xml
```
3. **Database Schema**:
* We will add a new column `document_a11y_media_file_id` to the `document` table to store the media file ID for HTML A11y documents.
```sql
ALTER TABLE `document`
ADD COLUMN `document_a11y_media_file_id` BINARY(16);
```
* The column is intended to link each document entry with its corresponding A11y media file `src/Core/Checkout/Document/DocumentDefinition.php`
```php
(new FkField('document_a11y_media_file_id', 'documentA11yMediaFileId', MediaDefinition::class))
->addFlags(new ApiAware());
```
### Email Migration
* For templates that have been customized, new content must be migrated same as code below:
`src/Core/Migration/Fixtures/mails/invoice_mail/de-plain.html.twig`
```twig
{% if a11yDocuments %}
For better accessibility, we also provide an HTML version of the documents here:
{% for a11y in a11yDocuments %}
{% set documentLink = rawUrl(
'frontend.account.order.single.document.a11y',
{
documentId: a11y.documentId,
deepLinkCode: a11y.deepLinkCode,
fileType: a11y.fileExtension,
},
salesChannel.domains|first.url
) %}
- {{ documentLink }}
{% endfor %}
{% endif %}
```
## Consequences
With this implementation, Shopware 6 will support HTML A11y documents alongside PDFs for standard document types. This change will have the following consequences:
* **Renderer Updates**: Document renderers need changes to handle HTML output, using the `AbstractDocumentTypeRenderer` [here](#core).
* **Email Integration**: For templates that have been customized, new content must be migrated as detailed in [here](#email-migration).
* **Improved Accessibility**: HTML documents make content easier to access for users with disabilities, aligning with WCAG standards.
* **Customizability**: Options in Document settings to enable or disable HTML documents should be added, giving merchants choice in document format.
---
---
url: >-
/docs/v6.6/resources/references/adr/2024-12-19-offer-html-alternative-to-our-pdf-standard-document.md
---
# "\[A11y] Offer HTML alternative to our pdf standard documents"
::: info
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2024-12-19-offer-html-alternative-to-our-pdf-standard-document.md)
:::
## Context
To comply with Web Content Accessibility Guidelines (WCAG), we aim to make Shopware 6's document solution accessible (A11y-compliant). This ensures that our solution remains usable while meeting legal requirements and making documents accessible to customers with disabilities.
Currently, our PDF generation library, DomPDF, does not meet accessibility standards, posing a significant challenge.
## Decision
We have decided to make HTML documents available in addition to PDF documents, as these are more accessible.
* **Better for Accessibility**: HTML is naturally organized, making it easier for accessibility tools to read and present content to people with disabilities.
* **Lack of Support**: As our current PDFs lack support for accessibility. Few tools, especially in PHP, can create accessible tagged PDFs, making it difficult to maintain PDF accessibility.
* **Industry Trends**: Many organizations are already moving from PDFs to HTML for accessibility. For example, government websites have been required to meet accessibility standards since the early 2000s. Most of them now use HTML for most of their content because it meets these standards better.
Providing HTML documents aligns with these trends and ensures we are using best practices for accessibility.
### Affected Areas
We will integrate HTML A11y document support in the following areas:
1. **Document Type Support**:
* Support includes all document types Shopware provides by default, `invoice`, `delivery note`, `credit note`, and `cancellation invoice`. Extensions must adapt themselves.
2. **Administration**:
* **Order Detail Page**: Option to download HTML alongside PDF for each document type.
* **Document Settings**: Toggle to generate HTML documents.
3. **Storefront**:
* **Order History**: Customers can access HTML and PDF versions of documents.
4. **Flow Builder**:
* This setup requires no additional special actions, and merchants can customize file generation for "Generate documents" in the `Document Settings`
5. **Email Delivery**:
* Enhance the original email by including a link to the HTML document. Customers will need to log in to access the document, and additional guidance will be provided.
* We canβt attach the HTML file directly due to issues with "virus scanners", as many email providers do not allow HTML file attachments. Instead we will provide a link inside the Email.
* A lot of the major platforms (Microsoft, Google, Amazon, etc.) will also email a summary with a link to the customer account for things like Azure/Google Cloud/etc.
### Core concept
#### Document Template
1. **Adjust Twig Template for A11y**:
* Modify the `html.twig` templates to support accessibility (A11y) by adding elements like `tabindex` and appropriate CSS styles.
`src/Core/Framework/Resources/views/documents/invoice.html.twig`:
```twig
{% block document_headline %}
{% endblock %}
```
`src/Core/Framework/Resources/views/documents/style_base_html.css.twig`:
```twig
{% block document_style_html %}
body {
max-width: 1200px;
margin: auto;
font-size: 14px;
line-height: 18px;
}
...
{% endblock %}
```
2. **Metadata and Security**:
* The generation date of the HTML will be "fingerprinted" by adding a metadata header. This allows users to track the creation date of the document.
* Implement a Content-Security-Policy meta-tag to minimize XSS attack risks, such as disallowing JavaScript and Restricts to the same domain, protecting against base URL manipulation.
Added new Twig block for metadata`src/Core/Framework/Resources/views/documents/base.html.twig`:
```twig
{% block document_head_meta_protection %}
{% endblock %}
```
#### Core
1. **Abstract Class for Multi-Format Rendering**
* We will introduce an abstract class, `src/Core/Checkout/Document/Service/AbstractDocumentTypeRenderer`, to support rendering multiple document types, including `PDF` and `HTML`.
```php
abstract class AbstractDocumentTypeRenderer
{
abstract public function render(RenderedDocument $document): string;
}
class HtmlRenderer extends AbstractDocumentTypeRenderer
{
public function render(RenderedDocument $document): string
{
$content = $this->documentTemplateRenderer->render(
...$options
);
$document->setContentType(self::FILE_CONTENT_TYPE);
$document->setFileExtension(self::FILE_EXTENSION);
$document->setContent($content);
return $content;
}
}
class PdfRenderer extends AbstractDocumentTypeRenderer {}
```
2. **Service Registration**:
* We need to use the service tag `document_type.renderer` for the `Shopware\Core\Checkout\Document\Service\DocumentFileRendererRegistry` to recognize this service. This is essential for the proper registration and functioning of the `HtmlRenderer`.
```xml
```
3. **Database Schema**:
* We will add a new column `document_a11y_media_file_id` to the `document` table to store the media file ID for HTML A11y documents.
```sql
ALTER TABLE `document`
ADD COLUMN `document_a11y_media_file_id` BINARY(16);
```
* The column is intended to link each document entry with its corresponding A11y media file `src/Core/Checkout/Document/DocumentDefinition.php`
```php
(new FkField('document_a11y_media_file_id', 'documentA11yMediaFileId', MediaDefinition::class))
->addFlags(new ApiAware());
```
### Email Migration
* For templates that have been customized, new content must be migrated same as code below:
`src/Core/Migration/Fixtures/mails/invoice_mail/de-plain.html.twig`
```twig
{% if a11yDocuments %}
For better accessibility, we also provide an HTML version of the documents here:
{% for a11y in a11yDocuments %}
{% set documentLink = rawUrl(
'frontend.account.order.single.document.a11y',
{
documentId: a11y.documentId,
deepLinkCode: a11y.deepLinkCode,
fileType: a11y.fileExtension,
},
salesChannel.domains|first.url
) %}
- {{ documentLink }}
{% endfor %}
{% endif %}
```
## Consequences
With this implementation, Shopware 6 will support HTML A11y documents alongside PDFs for standard document types. This change will have the following consequences:
* **Renderer Updates**: Document renderers need changes to handle HTML output, using the `AbstractDocumentTypeRenderer` [here](#core).
* **Email Integration**: For templates that have been customized, new content must be migrated as detailed in [here](#email-migration).
* **Improved Accessibility**: HTML documents make content easier to access for users with disabilities, aligning with WCAG standards.
* **Customizability**: Options in Document settings to enable or disable HTML documents should be added, giving merchants choice in document format.
---
---
url: /frontends/packages/composables/useAddToCart.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useAddress.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useB2bQuoteManagement.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useBreadcrumbs.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCart.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCartErrorParamsResolver.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCartItem.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCartNotification.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCategory.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCategorySearch.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCheckout.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCmsBlock.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCmsMeta.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCmsSection.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCmsTranslations.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useContext.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCountries.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCustomerOrders.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useCustomerPassword.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useDefaultOrderAssociations.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useInternationalization.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useLandingSearch.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useListing.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useLocalWishlist.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useNavigation.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useNavigationContext.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useNavigationSearch.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useNewsletter.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useNotifications.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useOrderDetails.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useOrderPayment.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/usePrice.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProduct.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductAssociations.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductConfigurator.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductPrice.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductReviews.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductSearch.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductSearchSuggest.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useProductWishlist.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useSalutations.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useSessionContext.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useShopwareContext.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useSyncWishlist.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useUrlResolver.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useUser.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/packages/composables/useWishlist.md
---
# {{NAME}}
{{DESCRIPTION}}
{{ADDITIONAL\_README}}
{{DEMO\_BLOCK}}
## Types
{{INTERFACE\_CONTENT}}
{{RETURN\_TYPES\_CONTENT}}
---
---
url: /frontends/resources/community-modules.md
---
# π€ Community Modules
:::warning
The modules listed here are not officially supported or maintained by Shopware. Please use them at your own risk.
:::
The following section contains modules, plugins and other resources that are created and maintaned by the community. If you want to contribute to this list, please create a [pull request](https://github.com/shopware/frontends/pulls) or submit a new [idea](https://github.com/shopware/frontends/discussions/categories/ideas).
---
---
url: /frontends/resources/troubleshooting.md
---
# π± Troubleshooting
Collection of common issues you may run into while working with Shopware Composable Frontends. If you need help or have other questions, feel free to join the [frontends Discord channel](https://discord.com/channels/1308047705309708348/1405501315160739951/archives/C050L6NCMGQ).
## Which SalesChannel type to use for Composable Frontends?
Currently you should use the default **Storefront SalesChannel type**. This sounds wrong, but if you using the Headless SalesChannel type you will not have nice speaking seo urls at the moment. Because the generation of seo urls will only be executed for SalesChannels with the type Storefront. We working on a more flexible solution with the core team to not have this confusion in the future.
## The access token for the store API is public visible?
In general, the store API should only output content that would also be visible on a standard storefront. Therefore, do not output any sensitive data to the store API. For our vue-starter-template, we decided to use a public access token, also to have a simple configuration. However, this does not mean that you should do the same in a production environment. To secure your access token, you can use [proxy api requests](#proxy-api-requests) also have a look at our [community modules](../resources/community-modules/) how others are doing this.
## How to use https for your localhost with Composable Frontends?
### Option 1: Manual with mkcert
* Make sure you have `mkcert` installed on your system. Otherwise, follow [here](https://github.com/FiloSottile/mkcert) to set it up.
* Create a valid certificate in your project folder by running `mkcert localhost`.
* Update the `nuxt dev` command in your `package.json`.\
It should look like this: `NODE_TLS_REJECT_UNAUTHORIZED=0 nuxt dev --https --ssl-cert localhost.pem --ssl-key localhost-key.pem`
* Now run your project with `npm run dev` or `pnpm run dev` from your project root.
* Your browser may ask you to accept the risk when you visit `https://localhost:3000`. This is because it is a self-signed certificate.
### Option 2: Vite plugin
* Execute `pnpm add -D @vitejs/plugin-basic-ssl` in your project folder
* Edit your `nuxt.config.ts` file and add:
```ts
import basicSsl from '@vitejs/plugin-basic-ssl'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
// ...
devServer: {
https: true,
},
vite: {
plugins: [
basicSsl(),
],
},
// ...
```
* Start your dev server with `pnpm run dev`
* Your browser may ask you to accept the risk when you visit `https://localhost:3000`. This is because it is a self-signed certificate.
## SSR throws error in local environment with DDEV?
If you are using DDEV as a local environment with SSR = true (Nuxt config for routes) and you always get a 500 error message that the context is not provided for category, you may have a problem with the SSL certificate. Try to use `NODE_TLS_REJECT_UNAUTHORIZED = 0` in [.env file](https://nuxt.com/docs/guide/directory-structure/env) (this is a issue with self-signed certificates). To validate if this is your problem: Connect the local Frontend with a valid SSL from a cloud instance and check it against this instance. Also check if you can reach any local store API endpoint with some API client.
## 412 error page during local development?
The HTTP status code 412 (Precondition Failed) usually means in the Shopware `store API` context that the specified `accessToken` is incorrect or not correct for the specified `endpoint`. Check your `nuxt.config.ts` file, if you do not see an error, please try connecting directly to your `store API` endpoint using an API client.
```ts
// a part of nuxt.config.ts
shopware: {
accessToken: "SWSCBHFSNTVMAWNZDNFKSHLAYW", // access token for corresponding sales channel
endpoint: "https://demo-frontends.shopware.store/store-api/", // endpoint where store-api is available
devStorefrontUrl: "https://demo-frontends.shopware.store", // see section below
},
```
## What is `devStorefrontUrl` and when to use it?
The `devStorefrontUrl` configuration option is primarily used for **customer registration** functionality. The Shopware registration endpoint requires a `storefrontUrl` parameter in its payload to identify which sales channel domain the customer is registering from.
### Why is it needed?
By default, the application uses `window.location.origin` (e.g., `https://your-store.com`) to determine the storefront URL. However, this fails in certain scenarios:
* **Local development** - Your browser origin is `http://localhost:3000`, which doesn't match any configured sales channel domain
* **Separate frontend/API hosting** - When your frontend runs on a different domain than what's configured in Shopware
### How to configure it
Set `devStorefrontUrl` to a domain that is configured in your Shopware admin under **Sales Channel β Domains**:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
shopware: {
endpoint: "https://your-shop.shopware.store/store-api",
accessToken: "your-access-token",
devStorefrontUrl: "https://your-shop.shopware.store", // must match a domain in Sales Channel settings
},
},
},
});
```
Or use an environment variable:
```bash
NUXT_PUBLIC_SHOPWARE_DEV_STOREFRONT_URL=https://your-shop.shopware.store
```
:::tip
If customer registration works in production but fails locally, `devStorefrontUrl` is likely the solution. Set it to your production storefront domain during local development.
:::
## Access from origin 127.0.0.1:3000 has been blocked by CORS policy
Depending on your server, you may need to set the `Access-Control-Allow-Origin` header to access your server from an external origin. And yes, your local development server is also an external origin in this case. Also, have a look at this [documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin) from MDN.
## Proxy API requests
If you're encountering issues related to Cross-Origin Resource Sharing (CORS) or if you wish to conceal the backend API URL, you can use Vite's proxy mechanism
### Nuxt example
Edit your `nuxt.config.ts` file and add:
```
vite: {
server: {
proxy: {
"/store-api": {
target: "",
changeOrigin: true,
secure: false,
},
},
},
},
```
Modify the Shopware API endpoint to match your local frontend URL.
```
{
...
shopware: {
endpoint: "store-api/",
...
}
}
```
## Broadcasting and BFCache Compatibility
### Issue
When Broadcasting is enabled, the BFCache (Back-Forward Cache) functionality is not operational. This incompatibility can lead to suboptimal performance and user experience when navigating back and forth between pages.
### Resolution (vue-demo template)
To leverage the benefits of BFCache, we have decided to disable Broadcasting. By turning off Broadcasting, we ensure that the BFCache can function correctly, providing a smoother and faster navigation experience for users.
```
...
runtimeConfig: {
broadcasting: true,
},
...
```
### Additional Information
BFCache is a browser optimization that allows pages to be stored in memory, enabling instant loading when users navigate back or forward. While Broadcasting is useful for real-time updates, its current implementation conflicts with BFCache. Disabling Broadcasting allows us to prioritize the performance improvements offered by BFCache.
For more details on BFCache, refer to the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Performance/Navigation_and_resource_timing#bfcache), [WHATWG](https://github.com/whatwg/html/issues/7253)
## CORS (Cross-Origin Resource Sharing) Issues
See the [CORS](./troubleshooting/CORS) page for more information on how to handle CORS issues in your project.
## \[unimport] failed to find "createShopwareContext" imported from "#imports"
### Problem
This error occurs when `@shopware/nuxt-module` is added to your project, but `@shopware/composables/nuxt-layer` is not extended in your Nuxt configuration.
### Why it happens
The `@shopware/nuxt-module` plugin imports `createShopwareContext` from the `#imports` alias. The `@shopware/composables/nuxt-layer` is responsible for configuring Nuxt's auto-import system and TypeScript paths to make composables exports available via `#imports`.
When you use Nuxt layers, the layer system merges TypeScript configuration files from both the composables layer and your project. This merge adds the composables exports to the `#imports` alias scope. Without extending the composables layer, these exports are not available, causing the import error.
### Solution
Extend `@shopware/composables/nuxt-layer` in your `nuxt.config.ts`:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
extends: ["@shopware/composables/nuxt-layer"],
modules: ["@shopware/nuxt-module"],
// ... rest of your configuration
});
```
:::tip
If you're using `@shopware/cms-base-layer`, you can extend both layers together:
```ts
extends: [
"@shopware/composables/nuxt-layer",
"@shopware/cms-base-layer",
"@shopware/unocss-design-tokens-layer"
],
```
:::
### Additional Information
* The `@shopware/composables/nuxt-layer` sets up auto-imports for all composables from the `src` directory
* It also configures TypeScript path aliases (`#imports` and `#shopware`) that are required by the nuxt-module
* Always extend the composables layer when using `@shopware/nuxt-module` in your project
---
---
url: /frontends/resources/links.md
---
# π Links
::: tip Do we miss some Link? πΆβπ«οΈ
Please tell us via [Community Discord](https://discord.com/channels/1308047705309708348/1405501315160739951/archives/C050L6NCMGQ), so we can add it.
:::
## Blog posts
Sorted by date, newest first
* [Unofficial API aware guidelines for Shopware 6](https://www.brocksi.net/blog/unofficial-api-aware-guidelines-shopware-6/)
* [How to work with Shopware Frontends: Experience creating POC](https://itdelight.io/how-to-work-with-shopware-frontends-experience-creating-poc/)
* [Multi-Page or Single-Page Variants Selection](https://www.brocksi.net/blog/variants-selection-multi-page-or-single-page/)
* [Create a CI/CD Pipeline for Shopware Frontends](https://kiplingi.de/create-a-ci-cd-pipeline-for-shopware-frontends/)
* [Gross and Net-Switch for B2B and B2C Shops built with Composable Frontends](https://dev.to/shopware/gross-and-net-switch-for-b2b-and-b2c-shops-built-with-composable-frontends-2b24)
* [Komponentenbasierte Entwicklung mit dem Shopware Frontends Framework](https://sitegeist.de/blog/e-commerce/komponentenbasierte-entwicklung-mit-dem-shopware-frontends-framework.html)
* [Frontend API with Nuxt + Nitro = Flexibility π](https://www.brocksi.net/blog/frontend-api-with-nuxt-and-nitro-will-lead-to-flexibility/)
* [Remixing the Shopware Checkout (Part 1)](https://elkmod.dev/blogs/remixing-shopware-checkout)
* [Create a vue.js composable and call any API within Shopware Frontends](https://www.brocksi.net/blog/vue-js-composable-call-api-shopware-frontends/)
* [The future of Shopware PWA](https://www.shopware.com/de/news/the-future-of-shopware-pwa/)
* [Frontends - yet another storefront?](https://www.shopware.com/en/news/frontends-yet-another-storefront/)
## Presentations
* [Quick-Start Composable Frontends - Shopware Boostday 2023](https://ecommerce.shopware.com/hubfs/Boost%20Days/Quick%20Start%20-%20Shopware%20Composable%20Frontends.pdf) (PDF)
## Videos
Sorted by date, newest first
* [Developer Brunch April 2024 - "Composable frontends"](https://www.youtube.com/watch?v=Tz-86f72cDk)
* [Going Headless - One Page Shop mit Shopware & Nuxt π | shopware x synaigy Meetup](https://www.youtube.com/watch?v=RXaNWRMuea8)
* [Performance Improvements Headless Shopware Frontends - Niklas Wolf, Mothership GmbH](https://www.youtube.com/watch?v=GhniPTMtIt8)
* [Ein Microstore mit Shopware Frontends | Shopware Meetup der Mothership GmbH in MΓΌnchen](https://www.youtube.com/watch?v=Dal-z94WLCk)
* [Shopware Composable Frontends in Action - A real-life example with Miriam MΓΌller](https://www.youtube.com/watch?v=AClnII3-GhQ)
* [Quick-Start Composable Frontends - Shopware Boostday 2023](https://www.youtube.com/watch?v=2AwLWvPOffw)
* [Shopware Composable Frontends: An interview. What is it, when to use it?](https://www.youtube.com/watch?v=A_O2nke4yoo)
* [Your new tool: Composable Frontends | #SCD23](https://www.youtube.com/watch?v=hN3t96zVfpw)
* [Shopwareβs Vue.js framework for building custom storefronts](https://www.youtube.com/watch?v=0W_3xWIpYho)
* [Ramona Schwering - Ecommerce as easy as an UI component - Vuejs Amsterdam 2023](https://www.youtube.com/watch?v=VivLHGGds6c)
* [shopcast.fm Folge 38 - "Frontends" Revisited](https://www.youtube.com/watch?v=eW9-jrXx4wA)
* [shopcast.fm Folge 37 - Projekt Shopware "Frontends"](https://www.youtube.com/watch?v=vupiRTNoePU)
## Code examples
* [Multi sales channel support Nuxt plugin](https://github.com/shopware/frontends/tree/main/examples/multi-sales-channel)
* [Multi-Instances Repo Example for Composable Frontends](https://github.com/patzick/frontends-multiinstances-example)
* Language/Translation Switch [StackBlitz](https://stackblitz.com/github/mkucmus/language-translations?file=app.vue) / [GitHub](https://github.com/mkucmus/language-translations)
::: tip More Code examples
Check the [examples folder](https://github.com/shopware/frontends/tree/main/examples) in our Frontends Repository.
:::
---
---
url: /frontends/integrations.md
---
# π οΈ Integrations
This is your go-to resource for seamlessly incorporating various platforms into Shopware Composable Frontends. Explore the following sub-pages to find detailed instructions on integrating different systems,
ensuring a harmonious and efficient synergy between your Shopware store and diverse external services.
## Overview
---
---
url: /docs/resources/guidelines/code/core/6.5-new-php-language-features.md
---
::: info
This document represents core guidelines and has been mirrored from the core in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/coding-guidelines/core/6.5-new-php-language-features.md)
:::
# 2023-05-16 - PHP 8.1 & Symfony 6.1 new features
## Context
As of Shopware 6.5 the minimum version of PHP is 8.1 and the minimum version of Symfony is 6.1. We would like to *promote* the usage of the newly available features.
Many of the new features allow us to reduce boilerplate, make it easier to prevent common mistakes, improve refactoring support, increase legibility, perform faster and so on.
By using the latest features we allow the reader and writer of code to focus on the domain rather than the language.
## PHP 8.0 & 8.1 new features
### Promoted Properties
* [PHP Docs](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion)
* [PHP Watch](https://php.watch/versions/8.0/constructor-property-promotion)
*We have automatically refactored all existing code to use Promoted Properties using Rector.*
Promoted properties allow us to reduce the boilerplate when defining classes, by removing the need to type the property name four times and the type twice.
Class properties, with their visibility and flags can now be specified entirely in the constructor.
From:
```php
class Point {
private int $x;
private int $y;
public function __construct(int $x, int $y)
{
$this->x = $x;
$this->y = $y;
}
}
```
To:
```php
class Point {
public function __construct(private int $x, private int $y)
{
}
}
```
Note: It is still possible to use normal property definitions/assignments with promoted properties. For example, if you need to manipulate some dependencies.
Advantages:
* Less code, less duplication.
* Better refactoring.
#### Backwards Compatibility / Migration Strategy
Migrating to promoted properties does not represent a breaking change.
### New in initializers
* [PHP Docs](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.new)
It is now possible to specify an object as a default parameter value in a function/method. Previously it was only possible to specify scalar values.
From:
```php
class PasswordHasher
{
private Hasher $hasher;
public function __construct(private Hasher $hasher = null)
{
$this->hasher = $hasher ?? new Bcrypt();
}
}
```
To:
```php
class PasswordHasher
{
public function __construct(private Hasher $hasher = new Bcrypt())
{}
}
```
Advantages:
* Less code
* More consistent
#### Backwards Compatibility / Migration Strategy
Migrating to inline object default parameters does not represent a breaking change.
### Match
* [PHP Docs](https://www.php.net/manual/en/control-structures.match.php)
* [PHP Watch](https://php.watch/versions/8.0/match-expression)
*We have automatically refactored all existing code to use match instead of switch using Rector.*
In most cases, `switch` statements can be replaced with `match` statements:
* Match uses strict equality unlike switch which uses weak comparison and can lead to subtle bugs.
* Each match arm does not fall through without a break statement, unlike switch.
* Match expressions must be exhaustive, if there is no default arm specified, and no arm matches the given value, an `UnhandledMatchError` is thrown.
* Match is an expression and thus returns a value, reducing unnecessary variables and reducing the risk of accessing undefined variables.
From:
```php
switch ($statusCode) {
case 200:
case 300:
$message = null;
break;
case 400:
$message = 'not found';
break;
case 500:
$message = 'server error';
break;
default:
$message = 'unknown status code';
break;
}
```
To:
```php
$message = match ($statusCode) {
200, 300 => null,
400 => 'not found',
500 => 'server error',
default => 'unknown status code',
};
```
Note: Conditions can be combined in a much simpler fashion.
#### Backwards Compatibility / Migration Strategy
There are cases where migrating from a switch to a match could case a BC break. For example switch performs lose type checks and throws an exception for unhandled values.
When migrating code, be sure to check that values are the correct types and that all cases are handled.
### New string functions
* `str_contains`
* `str_starts_with`
* `str_ends_with`
Advantages:
* Simpler and more concise.
* Saner return types.
* It is harder to get their usage wrong, for example checking for 0 vs false with `strpos`.
* The functions are faster, being implemented in C.
* The operations require less function calls, for example no usages of strlen are required.
### Named arguments
* [PHP Docs](https://www.php.net/manual/en/functions.arguments.php)
* [PHP Watch](https://php.watch/versions/8.0/named-parameters)
Named arguments are useful when calling code with bad and/or large API's. For example, many of PHP's global functions.
In terms of calling bad PHP API's, the following advantages apply:
* It is possible to skip defaults in between the arguments you want to change.
* The code is better documented since the argument label is specified with the value, very useful for boolean flags.
From:
```php
htmlspecialchars($string, ENT_COMPAT | ENT_HTML, 'UTF-8', false);
```
To:
```php
htmlspecialchars($string, double_encode: false);
```
Note: The second argument is not changed, but in the first example we must provide the default value, in order to change the double encode flag.
#### Backwards Compatibility / Migration Strategy
We do not want to use named parameters when calling Shopware API's as parameter names are not a part of the Backwards compatability promise.
Named parameters should only be used when calling PHP API's.
### Type improvements
* [PHP Docs](https://www.php.net/manual/en/language.types.type-system.php)
* [PHP Watch - Union Types](https://php.watch/versions/8.0/union-types)
* [PHP Watch - Mixed Type](https://php.watch/versions/8.0/mixed-type)
* [PHP Watch - Intersection Types](https://php.watch/versions/8.1/intersection-types)
**It will now only be necessary to reach for @var & @param annotations when defining array shapes, generics and more specific types such as `class-string`, `positive-int` etc. Everything else should be natively typed.**
When a type can really be any value, this can now be expressed as `mixed`.
When a type can be multiple, but not all, this can now be expressed as a union type, eg: `int|string`.
When a type must be an intersection of multiple types, this can now be expressed as an intersection type, eg: `MyService&MockObject`.
These improvements come with various advantages:
* The types are enforced by PHP, so TypeError's will be thrown when attempting to pass non-valid types.
* It allows us to move more type information from phpdoc into function signatures.
* It prevents incorrect function information. phpdocs can often become stale when they are not updated with the function itself.
### Enums
* [PHP Docs](https://www.php.net/manual/en/language.types.enumerations.php)
* [PHP Watch](https://php.watch/versions/8.1/enums)
PHP finally has native support for enumerations, with various advantages over common userland packages and using const's.
Enums are useful where we have a predefined list of constant values. It's now not necessary to provide values as constants, and it's not necessary to create arrays of the constants to check validity.
From:
```php
class Indexer
{
public const PARTIAL = 'partial';
public const FULL = 'full';
public function product(int $id, string $method): void
{
if (!in_array($method, [self::PARTIAL, self::FULL], true)) {
throw new \InvalidArgumentException();
}
match ($method) {
self::PARTIAL => $this->partial($id),
self::FULL => $this->full($id)
};
}
}
```
To:
```php
enum IndexMethod
{
case PARTIAL;
case FULL;
}
class Indexer
{
public function product(int $id, IndexMethod $method): void
{
match ($method) {
IndexMethod::PARTIAL => $this->partial($id),
IndexMethod::FULL => $this->full($id)
};
}
}
```
Advantages:
* Works great with `match` - an `UnhandledMatchError` exception will be thrown if there is no match arm for a given enum case.
* Can type hint on an enum.
* No need to validate a case.
* Can provide backed values and serialize/unserialize with `MyEnum::from()` && `MyEnum::tryFrom()`.
* Enums can provide methods and implement interfaces.
* Better comparison features, e.g. Enums are singletons.
#### Backwards Compatibility / Migration Strategy
See the [Use PHP 8.1 Enums](../../../references/adr/2023-05-16-php-enums) ADR for the decision and migration strategy.
### Readonly properties
* [PHP Docs](https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties)
* [PHP Watch](https://php.watch/versions/8.1/readonly)
Readonly properties are very useful when building DTOs. When you want to communicate a payload to a system or service, `readonly` properties allow us to create immutable data structures with a lot less code.
In conjunction with promoted properties, we can reduce the boilerplate of a class significantly. Consider a product reindex command:
From:
```php
class ProductReindexCommand
{
private int $productId;
private bool $includeStock:
public function __construct(int $productId, bool $includeStock)
{
$this->productId = $productId;
$this->includeStock = $includeStock;
}
public function getProductId(): int
{
return $this->productId;
}
public function includeStock(): bool
{
return $this->includeStock;
}
}
```
To:
```php
class ProductReindexCommand
{
public function __construct(public readonly int $productId, public readonly bool $includeStock)
{
}
}
```
In the first example, we use private properties to prohibit updates and public getters to allow access to the data. In the second we change the properties to `public` to allow access to the data without getters, but use `readonly` to prohibit updates. We also use promoted properties to make it even more succinct.
Advantages:
* Reduced boilerplate.
* Make the intent of code clearer.
#### Backwards Compatibility / Migration Strategy
All private properties, which are not written to after instantiation can successfully be migrated to `readonly` without BC breaks.
New code can use `readonly` on public and protected properties, but for existing code, that would be a BC break.
### First-class callable syntax
* [PHP Docs](https://www.php.net/manual/en/functions.first_class_callable_syntax.php)
* [PHP Watch](https://php.watch/versions/8.1/first-class-callable-syntax)
This is a new method of referencing callables with strings and arrays. It allows for improved refactoring support, better static analysis and fixes some subtle bugs with scope.
Consider an operation to find the longest string in an array, you might use `strlen` within an `array_map`:
From:
```php
$longest = max(array_map('strlen', $strings));
```
To:
```php
$longest = max(array_map(strlen(...), $strings));
```
Instead of using an arbitrary string as a reference to a function, we can now use the `(...)` syntax to create a callable.
It can also be used with object methods, instance or static, e.g.:
```php
$callable = $object->doCoolStuff(...);
$callable = \My\Object::doCoolStuff(...);
```
Advantages:
* Refactoring support.
* Better static analyses.
* Fixes scope issues.
### Attributes
* [PHP Docs](https://www.php.net/manual/en/language.attributes.overview.php)
* [PHP Watch](https://php.watch/versions/8.0/attributes)
*We have automatically refactored all existing code to use attributes instead of annotations using Rector.*
It is now possible to use native PHP attributes to store structured metadata, rather than using the error-prone PHP docblock.
For us Shopware developers, this will be most useful in conjunction with Symfony bundled attributes, which allow us to configure services and routes directly in controllers and services.
From:
```php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
/**
* @Route("/blog", name="blog_list")
*/
public function list(): Response
{
// ...
}
}
```
To:
```php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// ...
}
}
```
Advantages:
* Add Metadata to classes, methods, properties, arguments and so on.
* They can replace PHP doc blocks, each with custom parsers and rules to a unified standard supported by PHP.
* Type safety & autocompletion.
* The data can be introspected using PHP's Reflection API's.
### Nullsafe operator
* [PHP Docs](https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.nullsafe)
* [PHP Watch](https://php.watch/versions/8.0/null-safe-operator)
The nullsafe operator works the same as property or method accesses, except that if the object being dereferenced is null then null will be returned rather than an exception thrown. If the dereference is part of a chain, the rest of the chain is skipped.
Put another way; it allows chaining multiple property or method accesses on an object, without first checking if each returned value is null before proceeding.
Consider the following code:
```php
class User
{
public string $firstName;
public string $lastName;
public ?int $age = null;
public ?Address $address = null;
}
class Address
{
public int $number;
public string $addressLine1;
public ?string $addressLine2 = null;
}
```
Pre PHP 8.0, in order to access `addressLine2` for an address of a user, it would be necessary to write the following code:
```php
$user = new User(/** */);
$address = $user->address;
if ($address !== null) {
$addressLine2 = $address->addressLine2;
if ($addressLine2 !== null) {
//do something
}
}
```
Instead, we can now write:
```php
$user = new User(/** */);
$addressLine2 = $user?->address?->addressLine2;
if ($addressLine2 !== null) {
//do something
}
```
Advantages:
* Much less code for simple operations where null is a valid value.
* If the operator is part of a chain, anything to the right of the null will not be executed, the statements will be short-circuited.
* Can be used on methods where null coalescing cannot `$user->getCreatedAt()->format('d-m-Y') ?? null` where `getCreatedAt()` could return `null` or a `\DateTime` instance.
### Other
* `never` return type: https://www.php.net/manual/en/language.types.never.php
* `array_is_list` function: https://www.php.net/manual/en/function.array-is-list.php
* `final const X` final for class constants: https://www.php.net/manual/en/language.oop5.final.php
* `$object::class` instead of `get_class($object)`: https://wiki.php.net/rfc/class\_name\_literal\_on\_object
* Array unpacking with string keys is now supported: https://www.php.net/manual/en/language.types.array.php#language.types.array.unpacking
There are many more changes, including deprecations and backwards compatibility breaks. Please read the official announcement pages for both PHP 8.0 & PHP 8.1 for a deeper understanding:
* [PHP 8.0](https://www.php.net/releases/8.0/en.php)
* [PHP 8.0 - PHP Watch](https://php.watch/versions/8.0)
* [PHP 8.1](https://www.php.net/releases/8.1/en.php)
* [PHP 8.1 - PHP Watch](https://php.watch/versions/8.1)
## Symfony 6.1 new features
### Enums in route definitions
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-improved-routing-requirements-and-utf-8-parameters)
We can specify route parameters which will be validated against a given enums cases:
```php
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Requirement\EnumRequirement;
#[Route('/foo/{bar}', requirements: ['bar' => new EnumRequirement(SomeEnum::class)])]
```
### Service autowiring attributes
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-service-autowiring-attributes)
We can now wire up dependencies without touching XML. It is possible to define the required services directly in the class:
```php
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Mailer
{
public function __construct(
#[Autowire(service: 'email_adapter')]
private Adapter $adapter,
#[Autowire('%kernel.debug_mode%')]
private bool $debugMode,
) {}
}
```
Further to that, we can decorate services with attributes: https://symfony.com/blog/new-in-symfony-6-1-service-decoration-attributes
#### Backwards Compatibility / Migration Strategy
See the [Symfony Dependency Management](../../../references/adr/_superseded/2023-05-16-symfony-dependency-management) ADR for the decision and migration strategy.
### Improved console autocompletion
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-improved-console-autocompletion)
Autocompletion values can now be defined directly in the command input definition, as the 5th parameter, for both arguments and inputs:
```php
public function configure(): void
{
$this->addArgument(
'features',
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
'The features to enable',
null,
fn () => self::availableFeatures()
);
}
```
---
---
url: /docs/v6.5/resources/guidelines/code/core/6.5-new-php-language-features.md
---
::: info
This document represents core guidelines and has been mirrored from the core in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/coding-guidelines/core/6.5-new-php-language-features.md)
:::
# 2023-05-16 - PHP 8.1 & Symfony 6.1 new features
## Context
As of Shopware 6.5 the minimum version of PHP is 8.1 and the minimum version of Symfony is 6.1. We would like to *promote* the usage of the newly available features.
Many of the new features allow us to reduce boilerplate, make it easier to prevent common mistakes, improve refactoring support, increase legibility, perform faster and so on.
By using the latest features we allow the reader and writer of code to focus on the domain rather than the language.
## PHP 8.0 & 8.1 new features
### Promoted Properties
* [PHP Docs](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion)
* [PHP Watch](https://php.watch/versions/8.0/constructor-property-promotion)
*We have automatically refactored all existing code to use Promoted Properties using Rector.*
Promoted properties allow us to reduce the boilerplate when defining classes, by removing the need to type the property name four times and the type twice.
Class properties, with their visibility and flags can now be specified entirely in the constructor.
From:
```php
class Point {
private int $x;
private int $y;
public function __construct(int $x, int $y)
{
$this->x = $x;
$this->y = $y;
}
}
```
To:
```php
class Point {
public function __construct(private int $x, private int $y)
{
}
}
```
Note: It is still possible to use normal property definitions/assignments with promoted properties. For example, if you need to manipulate some dependencies.
Advantages:
* Less code, less duplication.
* Better refactoring.
#### Backwards Compatibility / Migration Strategy
Migrating to promoted properties does not represent a breaking change.
### New in initializers
* [PHP Docs](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.new)
It is now possible to specify an object as a default parameter value in a function/method. Previously it was only possible to specify scalar values.
From:
```php
class PasswordHasher
{
private Hasher $hasher;
public function __construct(private Hasher $hasher = null)
{
$this->hasher = $hasher ?? new Bcrypt();
}
}
```
To:
```php
class PasswordHasher
{
public function __construct(private Hasher $hasher = new Bcrypt())
{}
}
```
Advantages:
* Less code
* More consistent
#### Backwards Compatibility / Migration Strategy
Migrating to inline object default parameters does not represent a breaking change.
### Match
* [PHP Docs](https://www.php.net/manual/en/control-structures.match.php)
* [PHP Watch](https://php.watch/versions/8.0/match-expression)
*We have automatically refactored all existing code to use match instead of switch using Rector.*
In most cases, `switch` statements can be replaced with `match` statements:
* Match uses strict equality unlike switch which uses weak comparison and can lead to subtle bugs.
* Each match arm does not fall through without a break statement, unlike switch.
* Match expressions must be exhaustive, if there is no default arm specified, and no arm matches the given value, an `UnhandledMatchError` is thrown.
* Match is an expression and thus returns a value, reducing unnecessary variables and reducing the risk of accessing undefined variables.
From:
```php
switch ($statusCode) {
case 200:
case 300:
$message = null;
break;
case 400:
$message = 'not found';
break;
case 500:
$message = 'server error';
break;
default:
$message = 'unknown status code';
break;
}
```
To:
```php
$message = match ($statusCode) {
200, 300 => null,
400 => 'not found',
500 => 'server error',
default => 'unknown status code',
};
```
Note: Conditions can be combined in a much simpler fashion.
#### Backwards Compatibility / Migration Strategy
There are cases where migrating from a switch to a match could case a BC break. For example switch performs lose type checks and throws an exception for unhandled values.
When migrating code, be sure to check that values are the correct types and that all cases are handled.
### New string functions
* `str_contains`
* `str_starts_with`
* `str_ends_with`
Advantages:
* Simpler and more concise.
* Saner return types.
* It is harder to get their usage wrong, for example checking for 0 vs false with `strpos`.
* The functions are faster, being implemented in C.
* The operations require less function calls, for example no usages of strlen are required.
### Named arguments
* [PHP Docs](https://www.php.net/manual/en/functions.arguments.php)
* [PHP Watch](https://php.watch/versions/8.0/named-parameters)
Named arguments are useful when calling code with bad and/or large API's. For example, many of PHP's global functions.
In terms of calling bad PHP API's, the following advantages apply:
* It is possible to skip defaults in between the arguments you want to change.
* The code is better documented since the argument label is specified with the value, very useful for boolean flags.
From:
```php
htmlspecialchars($string, ENT_COMPAT | ENT_HTML, 'UTF-8', false);
```
To:
```php
htmlspecialchars($string, double_encode: false);
```
Note: The second argument is not changed, but in the first example we must provide the default value, in order to change the double encode flag.
#### Backwards Compatibility / Migration Strategy
We do not want to use named parameters when calling Shopware API's as parameter names are not a part of the Backwards compatability promise.
Named parameters should only be used when calling PHP API's.
### Type improvements
* [PHP Docs](https://www.php.net/manual/en/language.types.type-system.php)
* [PHP Watch - Union Types](https://php.watch/versions/8.0/union-types)
* [PHP Watch - Mixed Type](https://php.watch/versions/8.0/mixed-type)
* [PHP Watch - Intersection Types](https://php.watch/versions/8.1/intersection-types)
**It will now only be necessary to reach for @var & @param annotations when defining array shapes, generics and more specific types such as `class-string`, `positive-int` etc. Everything else should be natively typed.**
When a type can really be any value, this can now be expressed as `mixed`.
When a type can be multiple, but not all, this can now be expressed as a union type, eg: `int|string`.
When a type must be an intersection of multiple types, this can now be expressed as an intersection type, eg: `MyService&MockObject`.
These improvements come with various advantages:
* The types are enforced by PHP, so TypeError's will be thrown when attempting to pass non-valid types.
* It allows us to move more type information from phpdoc into function signatures.
* It prevents incorrect function information. phpdocs can often become stale when they are not updated with the function itself.
### Enums
* [PHP Docs](https://www.php.net/manual/en/language.types.enumerations.php)
* [PHP Watch](https://php.watch/versions/8.1/enums)
PHP finally has native support for enumerations, with various advantages over common userland packages and using const's.
Enums are useful where we have a predefined list of constant values. It's now not necessary to provide values as constants, and it's not necessary to create arrays of the constants to check validity.
From:
```php
class Indexer
{
public const PARTIAL = 'partial';
public const FULL = 'full';
public function product(int $id, string $method): void
{
if (!in_array($method, [self::PARTIAL, self::FULL], true)) {
throw new \InvalidArgumentException();
}
match ($method) {
self::PARTIAL => $this->partial($id),
self::FULL => $this->full($id)
};
}
}
```
To:
```php
enum IndexMethod
{
case PARTIAL;
case FULL;
}
class Indexer
{
public function product(int $id, IndexMethod $method): void
{
match ($method) {
IndexMethod::PARTIAL => $this->partial($id),
IndexMethod::FULL => $this->full($id)
};
}
}
```
Advantages:
* Works great with `match` - an `UnhandledMatchError` exception will be thrown if there is no match arm for a given enum case.
* Can type hint on an enum.
* No need to validate a case.
* Can provide backed values and serialize/unserialize with `MyEnum::from()` && `MyEnum::tryFrom()`.
* Enums can provide methods and implement interfaces.
* Better comparison features, e.g. Enums are singletons.
#### Backwards Compatibility / Migration Strategy
See the [Use PHP 8.1 Enums](../../../references/adr/2023-05-16-php-enums) ADR for the decision and migration strategy.
### Readonly properties
* [PHP Docs](https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties)
* [PHP Watch](https://php.watch/versions/8.1/readonly)
Readonly properties are very useful when building DTOs. When you want to communicate a payload to a system or service, `readonly` properties allow us to create immutable data structures with a lot less code.
In conjunction with promoted properties, we can reduce the boilerplate of a class significantly. Consider a product reindex command:
From:
```php
class ProductReindexCommand
{
private int $productId;
private bool $includeStock:
public function __construct(int $productId, bool $includeStock)
{
$this->productId = $productId;
$this->includeStock = $includeStock;
}
public function getProductId(): int
{
return $this->productId;
}
public function includeStock(): bool
{
return $this->includeStock;
}
}
```
To:
```php
class ProductReindexCommand
{
public function __construct(public readonly int $productId, public readonly bool $includeStock)
{
}
}
```
In the first example, we use private properties to prohibit updates and public getters to allow access to the data. In the second we change the properties to `public` to allow access to the data without getters, but use `readonly` to prohibit updates. We also use promoted properties to make it even more succinct.
Advantages:
* Reduced boilerplate.
* Make the intent of code clearer.
#### Backwards Compatibility / Migration Strategy
All private properties, which are not written to after instantiation can successfully be migrated to `readonly` without BC breaks.
New code can use `readonly` on public and protected properties, but for existing code, that would be a BC break.
### First-class callable syntax
* [PHP Docs](https://www.php.net/manual/en/functions.first_class_callable_syntax.php)
* [PHP Watch](https://php.watch/versions/8.1/first-class-callable-syntax)
This is a new method of referencing callables with strings and arrays. It allows for improved refactoring support, better static analysis and fixes some subtle bugs with scope.
Consider an operation to find the longest string in an array, you might use `strlen` within an `array_map`:
From:
```php
$longest = max(array_map('strlen', $strings));
```
To:
```php
$longest = max(array_map(strlen(...), $strings));
```
Instead of using an arbitrary string as a reference to a function, we can now use the `(...)` syntax to create a callable.
It can also be used with object methods, instance or static, e.g.:
```php
$callable = $object->doCoolStuff(...);
$callable = \My\Object::doCoolStuff(...);
```
Advantages:
* Refactoring support.
* Better static analyses.
* Fixes scope issues.
### Attributes
* [PHP Docs](https://www.php.net/manual/en/language.attributes.overview.php)
* [PHP Watch](https://php.watch/versions/8.0/attributes)
*We have automatically refactored all existing code to use attributes instead of annotations using Rector.*
It is now possible to use native PHP attributes to store structured metadata, rather than using the error-prone PHP docblock.
For us Shopware developers, this will be most useful in conjunction with Symfony bundled attributes, which allow us to configure services and routes directly in controllers and services.
From:
```php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
/**
* @Route("/blog", name="blog_list")
*/
public function list(): Response
{
// ...
}
}
```
To:
```php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// ...
}
}
```
Advantages:
* Add Metadata to classes, methods, properties, arguments and so on.
* They can replace PHP doc blocks, each with custom parsers and rules to a unified standard supported by PHP.
* Type safety & autocompletion.
* The data can be introspected using PHP's Reflection API's.
### Nullsafe operator
* [PHP Docs](https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.nullsafe)
* [PHP Watch](https://php.watch/versions/8.0/null-safe-operator)
The nullsafe operator works the same as property or method accesses, except that if the object being dereferenced is null then null will be returned rather than an exception thrown. If the dereference is part of a chain, the rest of the chain is skipped.
Put another way; it allows chaining multiple property or method accesses on an object, without first checking if each returned value is null before proceeding.
Consider the following code:
```php
class User
{
public string $firstName;
public string $lastName;
public ?int $age = null;
public ?Address $address = null;
}
class Address
{
public int $number;
public string $addressLine1;
public ?string $addressLine2 = null;
}
```
Pre PHP 8.0, in order to access `addressLine2` for an address of a user, it would be necessary to write the following code:
```php
$user = new User(/** */);
$address = $user->address;
if ($address !== null) {
$addressLine2 = $address->addressLine2;
if ($addressLine2 !== null) {
//do something
}
}
```
Instead, we can now write:
```php
$user = new User(/** */);
$addressLine2 = $user?->address?->addressLine2;
if ($addressLine2 !== null) {
//do something
}
```
Advantages:
* Much less code for simple operations where null is a valid value.
* If the operator is part of a chain, anything to the right of the null will not be executed, the statements will be short-circuited.
* Can be used on methods where null coalescing cannot `$user->getCreatedAt()->format('d-m-Y') ?? null` where `getCreatedAt()` could return `null` or a `\DateTime` instance.
### Other
* `never` return type: https://www.php.net/manual/en/language.types.never.php
* `array_is_list` function: https://www.php.net/manual/en/function.array-is-list.php
* `final const X` final for class constants: https://www.php.net/manual/en/language.oop5.final.php
* `$object::class` instead of `get_class($object)`: https://wiki.php.net/rfc/class\_name\_literal\_on\_object
* Array unpacking with string keys is now supported: https://www.php.net/manual/en/language.types.array.php#language.types.array.unpacking
There are many more changes, including deprecations and backwards compatibility breaks. Please read the official announcement pages for both PHP 8.0 & PHP 8.1 for a deeper understanding:
* [PHP 8.0](https://www.php.net/releases/8.0/en.php)
* [PHP 8.0 - PHP Watch](https://php.watch/versions/8.0)
* [PHP 8.1](https://www.php.net/releases/8.1/en.php)
* [PHP 8.1 - PHP Watch](https://php.watch/versions/8.1)
## Symfony 6.1 new features
### Enums in route definitions
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-improved-routing-requirements-and-utf-8-parameters)
We can specify route parameters which will be validated against a given enums cases:
```php
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Requirement\EnumRequirement;
#[Route('/foo/{bar}', requirements: ['bar' => new EnumRequirement(SomeEnum::class)])]
```
### Service autowiring attributes
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-service-autowiring-attributes)
We can now wire up dependencies without touching XML. It is possible to define the required services directly in the class:
```php
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Mailer
{
public function __construct(
#[Autowire(service: 'email_adapter')]
private Adapter $adapter,
#[Autowire('%kernel.debug_mode%')]
private bool $debugMode,
) {}
}
```
Further to that, we can decorate services with attributes: https://symfony.com/blog/new-in-symfony-6-1-service-decoration-attributes
#### Backwards Compatibility / Migration Strategy
See the [Symfony Dependency Management](../../../references/adr/2023-05-16-symfony-dependency-management) ADR for the decision and migration strategy.
### Improved console autocompletion
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-improved-console-autocompletion)
Autocompletion values can now be defined directly in the command input definition, as the 5th parameter, for both arguments and inputs:
```php
public function configure(): void
{
$this->addArgument(
'features',
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
'The features to enable',
null,
fn () => self::availableFeatures()
);
}
```
---
---
url: /docs/v6.6/resources/guidelines/code/core/6.5-new-php-language-features.md
---
::: info
This document represents core guidelines and has been mirrored from the core in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/coding-guidelines/core/6.5-new-php-language-features.md)
:::
# 2023-05-16 - PHP 8.1 & Symfony 6.1 new features
## Context
As of Shopware 6.5 the minimum version of PHP is 8.1 and the minimum version of Symfony is 6.1. We would like to *promote* the usage of the newly available features.
Many of the new features allow us to reduce boilerplate, make it easier to prevent common mistakes, improve refactoring support, increase legibility, perform faster and so on.
By using the latest features we allow the reader and writer of code to focus on the domain rather than the language.
## PHP 8.0 & 8.1 new features
### Promoted Properties
* [PHP Docs](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion)
* [PHP Watch](https://php.watch/versions/8.0/constructor-property-promotion)
*We have automatically refactored all existing code to use Promoted Properties using Rector.*
Promoted properties allow us to reduce the boilerplate when defining classes, by removing the need to type the property name four times and the type twice.
Class properties, with their visibility and flags can now be specified entirely in the constructor.
From:
```php
class Point {
private int $x;
private int $y;
public function __construct(int $x, int $y)
{
$this->x = $x;
$this->y = $y;
}
}
```
To:
```php
class Point {
public function __construct(private int $x, private int $y)
{
}
}
```
Note: It is still possible to use normal property definitions/assignments with promoted properties. For example, if you need to manipulate some dependencies.
Advantages:
* Less code, less duplication.
* Better refactoring.
#### Backwards Compatibility / Migration Strategy
Migrating to promoted properties does not represent a breaking change.
### New in initializers
* [PHP Docs](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.new)
It is now possible to specify an object as a default parameter value in a function/method. Previously it was only possible to specify scalar values.
From:
```php
class PasswordHasher
{
private Hasher $hasher;
public function __construct(private Hasher $hasher = null)
{
$this->hasher = $hasher ?? new Bcrypt();
}
}
```
To:
```php
class PasswordHasher
{
public function __construct(private Hasher $hasher = new Bcrypt())
{}
}
```
Advantages:
* Less code
* More consistent
#### Backwards Compatibility / Migration Strategy
Migrating to inline object default parameters does not represent a breaking change.
### Match
* [PHP Docs](https://www.php.net/manual/en/control-structures.match.php)
* [PHP Watch](https://php.watch/versions/8.0/match-expression)
*We have automatically refactored all existing code to use match instead of switch using Rector.*
In most cases, `switch` statements can be replaced with `match` statements:
* Match uses strict equality unlike switch which uses weak comparison and can lead to subtle bugs.
* Each match arm does not fall through without a break statement, unlike switch.
* Match expressions must be exhaustive, if there is no default arm specified, and no arm matches the given value, an `UnhandledMatchError` is thrown.
* Match is an expression and thus returns a value, reducing unnecessary variables and reducing the risk of accessing undefined variables.
From:
```php
switch ($statusCode) {
case 200:
case 300:
$message = null;
break;
case 400:
$message = 'not found';
break;
case 500:
$message = 'server error';
break;
default:
$message = 'unknown status code';
break;
}
```
To:
```php
$message = match ($statusCode) {
200, 300 => null,
400 => 'not found',
500 => 'server error',
default => 'unknown status code',
};
```
Note: Conditions can be combined in a much simpler fashion.
#### Backwards Compatibility / Migration Strategy
There are cases where migrating from a switch to a match could case a BC break. For example switch performs lose type checks and throws an exception for unhandled values.
When migrating code, be sure to check that values are the correct types and that all cases are handled.
### New string functions
* `str_contains`
* `str_starts_with`
* `str_ends_with`
Advantages:
* Simpler and more concise.
* Saner return types.
* It is harder to get their usage wrong, for example checking for 0 vs false with `strpos`.
* The functions are faster, being implemented in C.
* The operations require less function calls, for example no usages of strlen are required.
### Named arguments
* [PHP Docs](https://www.php.net/manual/en/functions.arguments.php)
* [PHP Watch](https://php.watch/versions/8.0/named-parameters)
Named arguments are useful when calling code with bad and/or large API's. For example, many of PHP's global functions.
In terms of calling bad PHP API's, the following advantages apply:
* It is possible to skip defaults in between the arguments you want to change.
* The code is better documented since the argument label is specified with the value, very useful for boolean flags.
From:
```php
htmlspecialchars($string, ENT_COMPAT | ENT_HTML, 'UTF-8', false);
```
To:
```php
htmlspecialchars($string, double_encode: false);
```
Note: The second argument is not changed, but in the first example we must provide the default value, in order to change the double encode flag.
#### Backwards Compatibility / Migration Strategy
We do not want to use named parameters when calling Shopware API's as parameter names are not a part of the Backwards compatability promise.
Named parameters should only be used when calling PHP API's.
### Type improvements
* [PHP Docs](https://www.php.net/manual/en/language.types.type-system.php)
* [PHP Watch - Union Types](https://php.watch/versions/8.0/union-types)
* [PHP Watch - Mixed Type](https://php.watch/versions/8.0/mixed-type)
* [PHP Watch - Intersection Types](https://php.watch/versions/8.1/intersection-types)
**It will now only be necessary to reach for @var & @param annotations when defining array shapes, generics and more specific types such as `class-string`, `positive-int` etc. Everything else should be natively typed.**
When a type can really be any value, this can now be expressed as `mixed`.
When a type can be multiple, but not all, this can now be expressed as a union type, eg: `int|string`.
When a type must be an intersection of multiple types, this can now be expressed as an intersection type, eg: `MyService&MockObject`.
These improvements come with various advantages:
* The types are enforced by PHP, so TypeError's will be thrown when attempting to pass non-valid types.
* It allows us to move more type information from phpdoc into function signatures.
* It prevents incorrect function information. phpdocs can often become stale when they are not updated with the function itself.
### Enums
* [PHP Docs](https://www.php.net/manual/en/language.types.enumerations.php)
* [PHP Watch](https://php.watch/versions/8.1/enums)
PHP finally has native support for enumerations, with various advantages over common userland packages and using const's.
Enums are useful where we have a predefined list of constant values. It's now not necessary to provide values as constants, and it's not necessary to create arrays of the constants to check validity.
From:
```php
class Indexer
{
public const PARTIAL = 'partial';
public const FULL = 'full';
public function product(int $id, string $method): void
{
if (!in_array($method, [self::PARTIAL, self::FULL], true)) {
throw new \InvalidArgumentException();
}
match ($method) {
self::PARTIAL => $this->partial($id),
self::FULL => $this->full($id)
};
}
}
```
To:
```php
enum IndexMethod
{
case PARTIAL;
case FULL;
}
class Indexer
{
public function product(int $id, IndexMethod $method): void
{
match ($method) {
IndexMethod::PARTIAL => $this->partial($id),
IndexMethod::FULL => $this->full($id)
};
}
}
```
Advantages:
* Works great with `match` - an `UnhandledMatchError` exception will be thrown if there is no match arm for a given enum case.
* Can type hint on an enum.
* No need to validate a case.
* Can provide backed values and serialize/unserialize with `MyEnum::from()` && `MyEnum::tryFrom()`.
* Enums can provide methods and implement interfaces.
* Better comparison features, e.g. Enums are singletons.
#### Backwards Compatibility / Migration Strategy
See the [Use PHP 8.1 Enums](../../../references/adr/2023-05-16-php-enums) ADR for the decision and migration strategy.
### Readonly properties
* [PHP Docs](https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties)
* [PHP Watch](https://php.watch/versions/8.1/readonly)
Readonly properties are very useful when building DTOs. When you want to communicate a payload to a system or service, `readonly` properties allow us to create immutable data structures with a lot less code.
In conjunction with promoted properties, we can reduce the boilerplate of a class significantly. Consider a product reindex command:
From:
```php
class ProductReindexCommand
{
private int $productId;
private bool $includeStock:
public function __construct(int $productId, bool $includeStock)
{
$this->productId = $productId;
$this->includeStock = $includeStock;
}
public function getProductId(): int
{
return $this->productId;
}
public function includeStock(): bool
{
return $this->includeStock;
}
}
```
To:
```php
class ProductReindexCommand
{
public function __construct(public readonly int $productId, public readonly bool $includeStock)
{
}
}
```
In the first example, we use private properties to prohibit updates and public getters to allow access to the data. In the second we change the properties to `public` to allow access to the data without getters, but use `readonly` to prohibit updates. We also use promoted properties to make it even more succinct.
Advantages:
* Reduced boilerplate.
* Make the intent of code clearer.
#### Backwards Compatibility / Migration Strategy
All private properties, which are not written to after instantiation can successfully be migrated to `readonly` without BC breaks.
New code can use `readonly` on public and protected properties, but for existing code, that would be a BC break.
### First-class callable syntax
* [PHP Docs](https://www.php.net/manual/en/functions.first_class_callable_syntax.php)
* [PHP Watch](https://php.watch/versions/8.1/first-class-callable-syntax)
This is a new method of referencing callables with strings and arrays. It allows for improved refactoring support, better static analysis and fixes some subtle bugs with scope.
Consider an operation to find the longest string in an array, you might use `strlen` within an `array_map`:
From:
```php
$longest = max(array_map('strlen', $strings));
```
To:
```php
$longest = max(array_map(strlen(...), $strings));
```
Instead of using an arbitrary string as a reference to a function, we can now use the `(...)` syntax to create a callable.
It can also be used with object methods, instance or static, e.g.:
```php
$callable = $object->doCoolStuff(...);
$callable = \My\Object::doCoolStuff(...);
```
Advantages:
* Refactoring support.
* Better static analyses.
* Fixes scope issues.
### Attributes
* [PHP Docs](https://www.php.net/manual/en/language.attributes.overview.php)
* [PHP Watch](https://php.watch/versions/8.0/attributes)
*We have automatically refactored all existing code to use attributes instead of annotations using Rector.*
It is now possible to use native PHP attributes to store structured metadata, rather than using the error-prone PHP docblock.
For us Shopware developers, this will be most useful in conjunction with Symfony bundled attributes, which allow us to configure services and routes directly in controllers and services.
From:
```php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
/**
* @Route("/blog", name="blog_list")
*/
public function list(): Response
{
// ...
}
}
```
To:
```php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// ...
}
}
```
Advantages:
* Add Metadata to classes, methods, properties, arguments and so on.
* They can replace PHP doc blocks, each with custom parsers and rules to a unified standard supported by PHP.
* Type safety & autocompletion.
* The data can be introspected using PHP's Reflection API's.
### Nullsafe operator
* [PHP Docs](https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.nullsafe)
* [PHP Watch](https://php.watch/versions/8.0/null-safe-operator)
The nullsafe operator works the same as property or method accesses, except that if the object being dereferenced is null then null will be returned rather than an exception thrown. If the dereference is part of a chain, the rest of the chain is skipped.
Put another way; it allows chaining multiple property or method accesses on an object, without first checking if each returned value is null before proceeding.
Consider the following code:
```php
class User
{
public string $firstName;
public string $lastName;
public ?int $age = null;
public ?Address $address = null;
}
class Address
{
public int $number;
public string $addressLine1;
public ?string $addressLine2 = null;
}
```
Pre PHP 8.0, in order to access `addressLine2` for an address of a user, it would be necessary to write the following code:
```php
$user = new User(/** */);
$address = $user->address;
if ($address !== null) {
$addressLine2 = $address->addressLine2;
if ($addressLine2 !== null) {
//do something
}
}
```
Instead, we can now write:
```php
$user = new User(/** */);
$addressLine2 = $user?->address?->addressLine2;
if ($addressLine2 !== null) {
//do something
}
```
Advantages:
* Much less code for simple operations where null is a valid value.
* If the operator is part of a chain, anything to the right of the null will not be executed, the statements will be short-circuited.
* Can be used on methods where null coalescing cannot `$user->getCreatedAt()->format('d-m-Y') ?? null` where `getCreatedAt()` could return `null` or a `\DateTime` instance.
### Other
* `never` return type: https://www.php.net/manual/en/language.types.never.php
* `array_is_list` function: https://www.php.net/manual/en/function.array-is-list.php
* `final const X` final for class constants: https://www.php.net/manual/en/language.oop5.final.php
* `$object::class` instead of `get_class($object)`: https://wiki.php.net/rfc/class\_name\_literal\_on\_object
* Array unpacking with string keys is now supported: https://www.php.net/manual/en/language.types.array.php#language.types.array.unpacking
There are many more changes, including deprecations and backwards compatibility breaks. Please read the official announcement pages for both PHP 8.0 & PHP 8.1 for a deeper understanding:
* [PHP 8.0](https://www.php.net/releases/8.0/en.php)
* [PHP 8.0 - PHP Watch](https://php.watch/versions/8.0)
* [PHP 8.1](https://www.php.net/releases/8.1/en.php)
* [PHP 8.1 - PHP Watch](https://php.watch/versions/8.1)
## Symfony 6.1 new features
### Enums in route definitions
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-improved-routing-requirements-and-utf-8-parameters)
We can specify route parameters which will be validated against a given enums cases:
```php
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Requirement\EnumRequirement;
#[Route('/foo/{bar}', requirements: ['bar' => new EnumRequirement(SomeEnum::class)])]
```
### Service autowiring attributes
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-service-autowiring-attributes)
We can now wire up dependencies without touching XML. It is possible to define the required services directly in the class:
```php
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Mailer
{
public function __construct(
#[Autowire(service: 'email_adapter')]
private Adapter $adapter,
#[Autowire('%kernel.debug_mode%')]
private bool $debugMode,
) {}
}
```
Further to that, we can decorate services with attributes: https://symfony.com/blog/new-in-symfony-6-1-service-decoration-attributes
#### Backwards Compatibility / Migration Strategy
See the [Symfony Dependency Management](../../../references/adr/2023-05-16-symfony-dependency-management) ADR for the decision and migration strategy.
### Improved console autocompletion
[Symfony Blog](https://symfony.com/blog/new-in-symfony-6-1-improved-console-autocompletion)
Autocompletion values can now be defined directly in the command input definition, as the 5th parameter, for both arguments and inputs:
```php
public function configure(): void
{
$this->addArgument(
'features',
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
'The features to enable',
null,
fn () => self::availableFeatures()
);
}
```
---
---
url: /frontends/best-practices/testing/ab-testing.md
---
# A/B Testing practices
A/B testing is a method of comparing two versions of a webpage or app against each other to determine which one performs better. It is a way to compare two versions of a single variable, typically by testing a subject's response to variant A against variant B, and determining which of the two variants is more effective.
## Providers
There are planty of A/B testing providers available. Here are some of the most popular ones:
* [AB Tasty](https://www.abtasty.com/)
* [Optimizely](https://www.optimizely.com/)
* [VWO](https://vwo.com/)
* [Split.io](https://www.split.io/)
* [Kameleoon](https://www.kameleoon.com/)
* [PostHog](https://posthog.com/)
You need to pick the right one for your needs. Depending on the size of your company, the complexity of your tests, and the budget you have available. There are generous free plans available in that list, so in most cases, you can start with that.
## Best practices
### Start with a hypothesis
Before you start your A/B test, you should have a clear hypothesis. What do you want to test? What do you expect to happen? What is the goal of the test?
### Split components dynamically to avoid enlagred bundle sizes
You should split your components dynamically. This will help you to avoid enlarged bundle sizes. You can use the `import()` function to load components on demand. Example:
```ts
const myExperimentFlag = useABTesting("myExperimentFlag");
const MyComponent = myExperimentFlag ? import("./MyComponentVariantA") : import("./MyComponentVariantB");
// later in the template
```
### Testing smaller components
While dynamic splitting is very effective to avoid loading too much code to the client's browser, this would not be efficient with some very small components. For example if you only want to test a different button variant, then in most cases it could be done in a single component. Example:
```ts
const myExperimentFlag = useABTesting("myExperimentFlag");
// later in the template
// or more slear split using v-show/v-if
```
### Clean your code
After the test is finished, you should clean your code. Remove all the unused code and components. This will help you to keep your codebase clean and maintainable. Not removing unused variants will cost you many maintenance problems, especially while refactoring your application.
---
---
url: /docs/guides/development/accessibility.md
---
# Accessibility
Shopware is committed to creating inclusive and barrier-free shopping experiences. Accessibility affects both the core Storefront and all custom themes and extensions. Developers are responsible for ensuring their implementations comply with accessibility standards, such as WCAG 2.1 Level AA.
## In this section
## Why accessibility matters
* Legal compliance (e.g., EU accessibility regulations)
* Better usability for all users
* Improved SEO and performance
* Future-proof storefront implementations
Shopware continuously introduces accessibility improvements in new releases. Always test extensions with the `ACCESSIBILITY_TWEAKS` feature flag enabled to ensure compatibility.
---
---
url: /docs/guides/development/accessibility/storefront-accessibility.md
---
# Accessibility in the Storefront
Shopware is committed to creating inclusive and barrier-free shopping experiences for our merchants and their customers.
## What does Shopware do to ensure accessibility?
* Shopware is committed to fulfilling the [WCAG 2.1 AA](https://www.w3.org/TR/WCAG21/) accessibility guidelines and Barrier-Free Information Technology Regulation (BITV 2.0) in the Storefront.
* You can find more information on [shopware.design](https://shopware.design/foundations/accessibility.html) and [in our blog post](https://www.shopware.com/en/news/accessible-online-store-by-2025/).
* The Storefront is using [Bootstrap components](https://getbootstrap.com/docs/5.3/getting-started/accessibility/) that already consider good accessibility practices, for example, using aria roles.
* Much of the HTML structure and CSS styling already fulfill accessibility guidelines. However, there are still [open accessibility issues](https://github.com/shopware/shopware/issues?q=state%3Aopen%20label%3Aarea%2Faccessibility) that will be addressed.
* Automated [E2E testing with playwright](https://github.com/shopware/shopware/tree/trunk/tests/acceptance) and axe reporter are used to ensure future accessibility.
## How are core accessibility improvements released?
Starting with **Shopware 6.6+**, accessibility improvements have been introduced, and **6.7+** includes further enhancements. Accessibility improvements are rolled out in regular minor releases, similar to other improvements or bug fixes.
There is no significant "accessibility release" planned that ships all accessibility improvements at once.
## How to deal with breaking accessibility changes?
Ensuring an accessible shop page can require changes in the HTML/Twig structure or the CSS. This can cause unintended behavior in an extension that modifies an area being changed to improve accessibility.
Because of this, breaking accessibility changes are not enabled by default. All accessibility changes that include breaking changes are implemented behind a feature flag:
```env
ACCESSIBILITY_TWEAKS=1
```
However, breaking accessibility changes are still released regularly inside minor releases. They are not active by default to not cause a breaking change.
The feature flag `ACCESSIBILITY_TWEAKS` can be activated in your `.env`, similar to the major feature flags like `V6_7_0_0`. When the feature flag is enabled, all available accessibility improvements are activated.
This allows you to check whether your project or extension is affected by the change and to prepare an adaptation already if necessary.
::: warning
With the major version v6.7.0, all accessibility improvements will become the default.
:::
### Example of a breaking accessibility change
Let's say, for example, that a list is not using proper markup and is changed to improve accessibility.
This is what a suboptimal HTML structure could look like:
```twig
```
Let's assume it should be changed to a proper list. Instead of implementing this right away, it is implemented behind the `ACCESSIBILITY_TWEAKS` flag, including instructions on how it should be changed:
```twig
{# @deprecated tag:v6.7.0 - The list will be changed to `
` and `
` to improve accessibility #}
{% if feature('ACCESSIBILITY_TWEAKS') %}
{% endif %}
```
If the block `component_list_items` is being extended, the new accessibility change can already be considered. If the change was rolled out without a feature flag, the extension still assumes a `
` which would likely result in incorrect HTML:
```twig
{% sw_extends '@Storefront/storefront/component/list.html.twig' %}
{# Consider the new structure already #}
{% block component_list_items_inner %}
{{ parent() }}
{% endblock %}
```
## Overview of accessibility issues for iteration 1
::: info
With accessibility iteration 1, we have addressed the most critical accessibility problems and implemented multiple improvements.
:::
### Continuous efforts to ensure accessibility
We are continuously testing our core Storefront to meet accessibility requirements. This includes screen reader usage, keyboard operation, or color contrast analysis.
We are using the [WCAG 2.1 Level AA](https://www.w3.org/TR/WCAG21/) standard and do our best to resolve all issues to meet its requirements.
### Overview of released accessibility improvements
* Below, you find a list of recent accessibility improvements. The list includes a changelog and the release versions for each improvement.
* Enable the feature flag `ACCESSIBILITY_TWEAKS` to activate all breaking accessibility changes.
| Topic | Breaking changes | Changelog | Release versions |
|-------------------------------------------------------------------------------------------------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| Missing semantic markup of form address headings | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-13-registration-form-fieldset-improvement.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Product image zoom modal keyboard accessibility | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-08-improve-image-zoom-modal-accessibility.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Focused slides in the carousel are not being moved into the visible area | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-05-improve-slider-element-accessibility.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Focus jumps to the top of the page after closing a modal | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-01-add-focus-handling-to-storefront.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Ensure that resizing content up to 200% does not cause breaks | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-13-Improved-storefront-text-scaling.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Language of each Storefront passage or phrase in the content can be programmatically determined | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-05-add-language-to-reviews.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Check Lighthouse Accessibility Score | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-21-fix-scroll-up-button-accessibility.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Pagination does not have links | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2023-08-31-pagination-with-links.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Non-informative document title | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.1.0/changelog/release-6-6-1-0/2024-03-12-distinctive-document-titles.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.1.0) |
| The form element quantity selector is not labeled | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.5.0/changelog/release-6-6-5-0/2024-07-15-the-form-element-quantity-selector-is-not-labeled.md) | [v6.6.5.0](https://github.com/shopware/shopware/releases/tag/v6.6.5.0) |
| Slider reports confusing status changes to screen readers | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.4.0/changelog/release-6-6-4-0/2024-05-31-remove-unwanted-aria-live-attributes-from-sliders.md) | [v6.6.4.0](https://github.com/shopware/shopware/releases/tag/v6.6.4.0) |
| The user needs to be able to close triggered, additional content | No | [Changelog](https://github.com/shopware/shopware/blob/trunk/changelog/release-6-6-3-0/2024-05-03-esc-key-for-nav-flyout-close.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| Improve "Remove Product" button labeling in checkout | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.3.0/changelog/release-6-6-3-0/2024-05-03-improve-line-item-labels-and-alt-texts.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| Missing alternative text for product images in the shopping cart | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.3.0/changelog/release-6-6-3-0/2024-05-03-improve-line-item-labels-and-alt-texts.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| A closing mechanism for the navigation | No | [Changelog](https://github.com/shopware/shopware/blob/trunk/changelog/release-6-6-3-0/2024-05-03-esc-key-for-nav-flyout-close.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| Change shipping toggle in OffCanvas cart to button element | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.2.0/changelog/release-6-6-2-0/2024-04-17-change-shipping-costs-toggle-to-button-element.md) | [v6.6.2.0](https://github.com/shopware/shopware/releases/tag/v6.6.2.0) |
| Add heading elements for account login page | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.2.0/changelog/release-6-6-2-0/2024-04-15-heading-elements-on-registration-page.md) | [v6.6.2.0](https://github.com/shopware/shopware/releases/tag/v6.6.2.0) |
| Provide distinctive document titles for each page | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.1.0/changelog/release-6-6-1-0/2024-03-12-distinctive-document-titles.md) | [v6.6.1.0](https://github.com/shopware/shopware/releases/tag/v6.6.1.0) |
| No empty nav element in top-bar | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.1.0/changelog/release-6-6-1-0/2023-03-05-no-empty-nav.md) | [v6.6.1.0](https://github.com/shopware/shopware/releases/tag/v6.6.1.0) |
| Update the focus states so that they are clearly visible | No | [Multiple changes](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26712\&type=commits) | [Multiple releases](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26712\&type=code) |
| Increase compatibility of Storefront with future assistance technologies | No | [Multiple changes](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26717\&type=commits) | [Multiple releases](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26717\&type=code) |
| Content functionality operable through keyboard | Yes | [Multiple changes](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26705\&type=commits) | [Multiple releases](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26705\&type=code) |
| No keyboard traps should occur in the Storefront | - | Verification work without released code changes | - |
| Mechanism for the user to pause, stop, or hide moving content | - | Verification work without released code changes | - |
| Add text to components that only work with icons to identify their purpose | - | - |
| Check if all non-text content has a text alternative and provide if necessary | - | - |
| Provide error correction suggestions | - | - |
| Text styles need to be adjusted (line height, paragraph spacing) | - | - |
| Keyboard/Tabs should work for nav main-navigation-menu | - | - |
### Overview of known accessibility issues
To report any new accessibility issues, click the New Issue button, select Bug Report, fill out the required fields, and make sure to add the area/accessibility label.
Here is a reference to the existing [accessibility Issues](https://github.com/shopware/shopware/issues?q=state%3Aopen%20label%3Aarea%2Faccessibility).
## Best practices for accessibility (a11y) in Shopware extensions
Ensuring accessibility in your Shopware extension improves **usability, inclusivity, and compliance** with standards like **WCAG 2.1** and the **EU Web Accessibility Directive**. Below are best practices to help you build accessible extensions and themes.
### 1. Setting up for accessibility testing
#### Activate the accessibility feature flag
Enable the **feature flag** to test changes before release by activating it in your local environment. Modify your `project-root/.env`.
```env
ACCESSIBILITY_TWEAKS=1
```
Once `ACCESSIBILITY_TWEAKS` is enabled, a theme recompilation is needed to apply all styling improvements, such as adjusted font sizes.
```text
bin/console theme:compile
```
Now you can:
* Preview upcoming a11y improvements before they become mandatory.
* Identify potential breaking changes in your theme or extension.
* Ensure your UI remains functional with new [accessibility enhancements](./accessibility-checklist.md).
### 2. Testing for accessibility compliance
#### Automated testing tools
Use automated tools to **detect common accessibility issues** in your extension:
* **[Google Lighthouse](../testing/store/quality-guidelines.md#lighthouse-a-b-testing)** β Run audits in Chrome DevTools.
* **AXE DevTools** β More detailed accessibility testing.
* **WAVE (WebAIM Tool)** β Identifies HTML structure and ARIA misuses.
#### Manual Testing
While automation helps, **manual checks** ensure real-world usability:
* **Keyboard Navigation** β Ensure users can navigate your store using `Tab`, `Enter`, and `Esc`.
* **Screen Readers** β Test with **NVDA (Windows)** or **VoiceOver (Mac/iOS)**.
* **Color Contrast Checks** β Use [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) to verify readability.
### 3. Validating accessibility in extensions
#### Common a11y issues to fix before release
* Misuse of **HTML & ARIA roles** (Ensure correct semantic structure).
* Missing **form labels & alt text** for images/icons.
* Improper **focus management** in modals, dropdowns, and popups.
* **Dynamic content updates** not announced to screen readers.
#### Shopware QA and self-certification
* Developers can **self-certify** an extension as a11y-compliant.
* Shopware **QA verification** may be required for listing in the store.
### 4. Accessibility support in Shopware versions
| **Shopware version** | **Accessibility support** |
|---------------------|------------------------|
| **6.7+** | Full A11y improvements available for testing |
| **6.6+** | Accessibility features introduced (use `ACCESSIBILITY_TWEAKS` feature flag) |
| **Shopware 5** | **No accessibility support** |
### 5. Getting help with accessibility
* Work with **Shopware-certified agencies** for A11y audits.
* Stay updated with Shopware's **developer guidelines**.
* Discuss A11y best practices with other developers in the **Shopware community**.
## Conclusion
Understanding accessibility best practices is just the beginning. To truly create an inclusive storefront, these ideas must be translated into practice. To help you turn these into action, we have created a comprehensive [Storefront Accessibility Checklist](./accessibility-checklist.md). It outlines the key technical and design practices needed to build accessible interfacesβfrom semantic HTML to keyboard navigation and ARIA usage.
---
---
url: /docs/v6.6/resources/accessibility/storefront.md
---
# Accessibility in the Storefront
At Shopware, we are committed to creating inclusive and barrier-free shopping experiences for our merchants and their customers.
## What shopware does to ensure accessibility
* Shopware is committed to fulfill the [WCAG 2.1 AA](https://www.w3.org/TR/WCAG21/) accessibility guidelines and Barrier-Free Information Technology Regulation (BITV 2.0) in the Storefront.
* You can find more information on [shopware.design](https://shopware.design/foundations/accessibility.html) and [in our blog post](https://www.shopware.com/en/news/accessible-online-store-by-2025/).
* The Storefront is using [Bootstrap components](https://getbootstrap.com/docs/5.3/getting-started/accessibility/) that already consider good accessibility practices, for example, using aria roles.
* Much of the HTML structure and CSS styling already fulfill accessibility guidelines. However, there are still [open accessibility issues](#Overview-of-known-accessibility-issues) that will be addressed.
* Automated [E2E testing with playwright](https://github.com/shopware/shopware/tree/trunk/tests/acceptance) and axe reporter are used to ensure future accessibility.
## How core accessibility improvements are released
Accessibility improvements are rolled out in regular minor releases, similar to other improvements or bug-fixes. We implement all accessibility improvements in the current major version `6.6.x` and its minor versions.
There is no large "accessibility release" planned that ships all accessibility improvements at once.
## How to deal with breaking accessibility changes
Ensuring an accessible shop page can require changes in the HTML/Twig structure or the CSS. This can cause unintended behavior for an extension that is modifying an area that is being changed to improve accessibility.
Because of this, breaking accessibility changes are not enabled by default. All accessibility changes that include breaking changes are implemented behind a feature flag:
```env
ACCESSIBILITY_TWEAKS
```
However, breaking accessibility changes are still released regularly inside minor releases. They are just not active by default to not cause a breaking change.
The feature flag `ACCESSIBILITY_TWEAKS` can be activated inside your `.env`, similar to the major feature flags like `V6_7_0_0`. When the feature flag is enabled, all available accessibility improvements are activated.
This allows you to check if your project or extension is effected by the change and already prepare an adaptation to the change if it is necessary.
::: warning
With the major version v6.7.0 all accessibility improvements will become the default.
:::
### Example of a breaking accessibility change
Let's say, for example, that a list is not using a proper markup, and it is changed to improve accessibility.
This is what a suboptimal HTML structure could look like:
```twig
```
Let's assume it should be changed to a proper list. Instead of implementing this right away, it is implemented behind the `ACCESSIBILITY_TWEAKS` flag, including instructions how it should be changed:
```twig
{# @deprecated tag:v6.7.0 - The list will be changed to `
` and `
` to improve accessibility #}
{% if feature('ACCESSIBILITY_TWEAKS') %}
{% endif %}
```
If the block `component_list_items` is being extended, the new accessibility change can already be considered. If the change was rolled out without a feature flag, the extension still assumes a `
` which would likely result in incorrect HTML:
```twig
{% sw_extends '@Storefront/storefront/component/list.html.twig' %}
{# Consider the new structure already #}
{% block component_list_items_inner %}
{{ parent() }}
{% endblock %}
```
## Overview of accessibility issues for iteration 1
::: info
With accessibility iteration 1 we have addressed the most critical accessibility problems and implemented multiple improvements.
You can find an overview of the accessibility iteration 1 epic in the following ticket: [NEXT-37039](https://issues.shopware.com/issues/NEXT-37039)
:::
### Continuous efforts to ensure accessibility
We are continuously testing our core Storefront to meet accessibility requirements. This includes screen reader usage, keyboard-operation or color contrast analyzes.
We are using the [WCAG 2.1 Level AA](https://www.w3.org/TR/WCAG21/) standard and do our best to solve all issues to meet the WCAG 2.1 requirements.
### Overview of released accessibility improvements
* Below, you find a list of recent accessibility improvements. The list includes a changelog and the release versions for each improvement.
* Enable the feature flag `ACCESSIBILITY_TWEAKS` to activate all breaking accessibility changes.
| Topic | Breaking changes | Changelog | Release versions |
|-------------------------------------------------------------------------------------------------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| Missing semantic markup of form address headings | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-13-registration-form-fieldset-improvement.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Product image zoom modal keyboard accessibility | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-08-improve-image-zoom-modal-accessibility.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Focused slides in the carousel are not being moved into the visible area | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-05-improve-slider-element-accessibility.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Focus jumps to the top of the page after closing a modal | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-01-add-focus-handling-to-storefront.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Ensure that resizing content up to 200% does not cause breaks | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-13-Improved-storefront-text-scaling.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Language of each Storefront passage or phrase in the content can be programmatically determined | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-05-add-language-to-reviews.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Check Lighthouse Accessibility Score | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2024-08-21-fix-scroll-up-button-accessibility.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Pagination does not have links | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.6.0/changelog/release-6-6-6-0/2023-08-31-pagination-with-links.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.6.0) |
| Non-informative document title | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.1.0/changelog/release-6-6-1-0/2024-03-12-distinctive-document-titles.md) | [v6.6.6.0](https://github.com/shopware/shopware/releases/tag/v6.6.1.0) |
| The form element quantity selector is not labeled | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.5.0/changelog/release-6-6-5-0/2024-07-15-the-form-element-quantity-selector-is-not-labeled.md) | [v6.6.5.0](https://github.com/shopware/shopware/releases/tag/v6.6.5.0) |
| Slider reports confusing status changes to screen readers | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.4.0/changelog/release-6-6-4-0/2024-05-31-remove-unwanted-aria-live-attributes-from-sliders.md) | [v6.6.4.0](https://github.com/shopware/shopware/releases/tag/v6.6.4.0) |
| The user needs to be able to close triggered, additional content | No | [Changelog](https://github.com/shopware/shopware/blob/trunk/changelog/release-6-6-3-0/2024-05-03-esc-key-for-nav-flyout-close.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| Improve "Remove Product" button labeling in checkout | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.3.0/changelog/release-6-6-3-0/2024-05-03-improve-line-item-labels-and-alt-texts.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| Missing alternative text for product images in the shopping cart | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.3.0/changelog/release-6-6-3-0/2024-05-03-improve-line-item-labels-and-alt-texts.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| A closing mechanism for the navigation | No | [Changelog](https://github.com/shopware/shopware/blob/trunk/changelog/release-6-6-3-0/2024-05-03-esc-key-for-nav-flyout-close.md) | [v6.6.3.0](https://github.com/shopware/shopware/releases/tag/v6.6.3.0) |
| Change shipping toggle in OffCanvas cart to button element | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.2.0/changelog/release-6-6-2-0/2024-04-17-change-shipping-costs-toggle-to-button-element.md) | [v6.6.2.0](https://github.com/shopware/shopware/releases/tag/v6.6.2.0) |
| Add heading elements for account login page | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.2.0/changelog/release-6-6-2-0/2024-04-15-heading-elements-on-registration-page.md) | [v6.6.2.0](https://github.com/shopware/shopware/releases/tag/v6.6.2.0) |
| Provide distinctive document titles for each page | No | [Changelog](https://github.com/shopware/shopware/blob/v6.6.1.0/changelog/release-6-6-1-0/2024-03-12-distinctive-document-titles.md) | [v6.6.1.0](https://github.com/shopware/shopware/releases/tag/v6.6.1.0) |
| No empty nav element in top-bar | Yes | [Changelog](https://github.com/shopware/shopware/blob/v6.6.1.0/changelog/release-6-6-1-0/2023-03-05-no-empty-nav.md) | [v6.6.1.0](https://github.com/shopware/shopware/releases/tag/v6.6.1.0) |
| Update the focus states so that they are clearly visible | No | [Multiple changes](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26712\&type=commits) | [Multiple releases](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26712\&type=code) |
| Increase compatibility of Storefront with future assistance technologies | No | [Multiple changes](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26717\&type=commits) | [Multiple releases](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26717\&type=code) |
| Content functionality operable through keyboard | Yes | [Multiple changes](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26705\&type=commits) | [Multiple releases](https://github.com/search?q=repo%3Ashopware%2Fshopware+NEXT-26705\&type=code) |
| No keyboard traps should occur in the Storefront | - | Verification work without released code changes | - |
| Mechanism for the user to pause, stop, or hide moving content | - | Verification work without released code changes | - |
| Add text to components that only work with icons to identify their purpose | - | - |
| Check if all non-text content has text alternative and provide if necessary | - | - |
| Provide error correction suggestions | - | - |
| Text styles needs to be adjusted (line height, paragraph spacing) | - | - |
| Keyboard/Tabs should work for nav main-navigation-menu | - | - |
### Overview of known accessibility issues
[GitHub Issues](https://github.com/shopware/shopware/labels/accessibility)
---
---
url: /docs/products/paas/shopware/fundamentals/account.md
---
# Account
An account represents your access to resources within the Shopware PaaS Native backend environment. The `sw-paas account` commands cover identity inspection, context management, human user memberships, service accounts, and access tokens.
## Identity and roles
To find what resources you have access to via the CLI:
```sh
sw-paas account whoami
```
This shows the currently authenticated user and the roles attached to that user.
For role details at organization level, see [organization guide](./organization.md).
## Context
To avoid repetitive prompts for `organization-id` and `project-id`, you can set a context and the CLI will automatically use these values without asking.
Setting your context streamlines your workflow by eliminating the need to specify these parameters with every command.
```sh
sw-paas account context set
```
Display the current context:
```sh
sw-paas account context show
```
Delete the saved context:
```sh
sw-paas account context delete
```
The context is saved as `context-production.yaml` and stored alongside the main configuration file in the following locations:
| | Unix | macOS | Windows |
|-----------------|------------------------|--------------------------------------------|----------------|
| XDG\_CONFIG\_HOME | ~/.config/sw-paas | ~/Library/ApplicationΒ Support/sw-paas | %LOCALAPPDATA% |
| XDG\_STATE\_HOME | ~/.local/state/sw-paas | ~/Library/ApplicationΒ Support/sw-paas | %LOCALAPPDATA% |
## Human user access
Human user memberships at organization, project, and application level are managed through `account user`.
List memberships:
```sh
sw-paas account user list
```
Add a user membership:
```sh
sw-paas account user add
```
Remove a user membership:
```sh
sw-paas account user remove
```
Users can also request access themselves:
```sh
sw-paas account user request
sw-paas account user requests list
```
Users with the `account-admin` role can review and resolve pending requests:
```sh
sw-paas account user requests resolve
```
## Service accounts
Service accounts are machine identities for CI/CD pipelines and other automation.
They can also be used to give an external developer temporary access to an application.
Create, list, update, or delete a service account:
```sh
sw-paas account service-account create
sw-paas account service-account list
sw-paas account service-account update
sw-paas account service-account delete
```
Manage service account grants:
```sh
sw-paas account service-account grant list
sw-paas account service-account grant add
sw-paas account service-account grant policies
sw-paas account service-account grant revoke
```
:::warning
Service account tokens can have stricter authorization policies than human users. When using a
strictly scoped token, specify resources with `--organization-id`, `--project-id`, and
`--application-id`. The corresponding `--organization`, `--project`, and `--application` options
accept names, which the CLI resolves to IDs through internal list API calls. These lookups require
additional permissions that a strictly scoped token might not have. To use resource names instead,
grant the service account the `organization:viewer` and `project:viewer` policies.
:::
## Authentication tokens
The `token` command manages access tokens for either your own account or a service account. Personal access tokens can be used for personal scripts. For CI/CD, you should use service accounts.
Personal access tokens inherit the permissions of the user who created them, except the ability to create new tokens. This means any action the user can perform, the personal token can perform as well.
Service account tokens do not inherit the full permissions of the user who created them. They authenticate as the service account and are limited to the permissions granted to that service account.
:::warning
Personal access tokens should be used with caution since they are tied to a user. If someone obtains a personal access token, they can act on behalf of that user with all of their permissions.
:::
### Personal tokens
Generate a new access token:
```sh
sw-paas account token create
```
### Using a Token
To use a token you have multiple options:
```sh
token=
sw-paas --token $token account whoami
sw-paas --token "" account whoami
# Set it for the current terminal session
export SW_PAAS_TOKEN=
sw-paas account whoami
```
### Revoking a Token
Remove a specific token by ID:
```sh
sw-paas account token revoke --token-id abcd-1234
```
### Service account tokens
To manage tokens for a service account, pass `--service-account-id`:
```sh
sw-paas account token create --service-account-id
sw-paas account token list --service-account-id
sw-paas account token revoke --service-account-id
```
---
---
url: /docs/v6.6/products/paas/shopware/CLI/account.md
---
# Account
The `account` command gives you access to account-level operations such as context management, token handling, user-role mapping, and role identification. An **account** represents your access to resources within our backend environment.
## Usage
```sh
sw-paas account [command]
```
## Commands
### Account Context
The `context` command lets you define and manipulate a *context file*, allowing the CLI to skip repetitive prompts for `organization-id` and `project-id`. The default context file is saved as `context-production.yaml` and stored alongside the main config file. Below is the location of where these files are stored.
| | Unix | MacOS | Windows |
|-----------------|------------------------|--------------------------------------------|----------------|
| XDG\_CONFIG\_HOME | ~/.config/sw-paas | ~/Library/ApplicationΒ Support/sw-paas | %LOCALAPPDATA% |
| XDG\_STATE\_HOME | ~/.local/state/sw-paas | ~/Library/ApplicationΒ Support/sw-paas | %LOCALAPPDATA% |
**Usage:**
```sh
sw-paas account context [command]
```
**Available Subcommands:**
* `set`: Define or update your current context.
* `show`: Display the currently active context values.
* `delete`: Remove the saved context.
**Examples:**
```sh
# Set a new context for organization and project
sw-paas account context set --organization-id org-123 --project-id proj-456
# Set a new context for organization skipping project
sw-paas account context set --organization-id org-123 --skip-project-id
# View the current context
sw-paas account context show
# Delete the current context file
sw-paas account context delete
```
### Authentication Tokens
The `token` command manages personal access tokens for secure API and CLI usage. Tokens can be created, listed, and revoked.
**Usage:**
```sh
sw-paas account token [command]
```
**Available Subcommands:**
* `create`: Generate a new access token.
* `list`: View all your active tokens.
* `revoke`: Remove a specific token.
**Examples:**
```sh
# Create a new token
sw-paas account token create --name "ci-token"
# List all active tokens
sw-paas account token list
# Revoke a token by ID
sw-paas account token revoke --token-id abcd-1234
```
### Users and Roles
Use the `user` command to map users to specific roles within the organization. Only users with sufficient privileges (e.g., admin) can modify roles.
**Usage:**
```sh
sw-paas account user [command]
```
**Available Subcommands:**
* `add`: Add a user to the organization with a specific role.
* `remove`: Remove a user from a role.
If you already have the `project-admin` role and wish to add additional users to your organization, they can share their **user ID (sub-id)** with you. You can instruct them to retrieve it using the following command:
```sh
sw-paas account whoami --output json
```
Or, if they have `jq` installed for easier parsing:
```sh
sw-paas account whoami --output json | jq ".sub"
```
Once you receive their `sub` (subject ID), you can proceed to add them to your organization with the appropriate role.
**Available Roles:**
* `read-only`: Gets access to projects and applications. Only actions allowed are `get` and `list`.
* `developer`: Gets access to projects and applications. All actions are allowed.
* `account-admin`: Gets access to projects and applications. All actions are allowed.
* `project-admin`: Gets access to account management. Actions for managing Users are allowed.
**Examples:**
```sh
# Add a new user as a developer
sw-paas account user add --sub adbs-123 --organization-id abc-123 --role developer
# Remove a user from the developer role
sw-paas account user remove --sub adbs-123 --organization-id abc-123 --role developer
```
### **whoami** β Show Your Identity and Roles
Use the `whoami` command to display your identity, including your User ID(Sub ID), email, and associated policies within the account.
**Usage:**
```sh
sw-paas account whoami
```
This is especially helpful for confirming which roles and permissions are currently active in a given account.
## **Tips**
* Always set a context to reduce repetitive prompts across commands.
* Token management is essential for CI/CD and script-based access. You can use this in environments such as Github Action, CircleCI, GitLab CI, Travis CI etc.
* Use `whoami` to verify access if permission errors occur.
---
---
url: /frontends/frontends-recipes/account.md
---
# Account
Recipes for customer session and account flows.
---
---
url: /docs/products/extensions/b2b-suite/guides/storefront/acl-routing.md
---
# ACL and Routing
The ACL Routing component allows you to block Controller Actions for B2B users.
It relies on and extends the technologies already defined by the ACL component.
To accomplish this, the component directly maps an `action` in a given `controller` to a `resource` (= entity type) and `privilege` (= class of actions).
There are two core actions you should know: `index` and `detail`, as you can see in the following acl-config example below.
## Registering routes
All routes that need access rights need to be stored in the database.
The B2B Suite provides a service to simplify this process.
For it to work correctly, you need an array in a specific format structured like this:
```php
$myAclConfig = [
'contingentgroup' => //resource name
[
'B2bContingentGroup' => // controller name
[
'index' => 'list', // action name => privilege name
[...]
'detail' => 'detail',
],
],
];
```
This configuration array can then be synced to the database by using this service during installation:
```php
Shopware\B2B\AclRoute\Framework\AclRoutingUpdateService::create()
->addConfig($myAclConfig);
```
This way, you can easily create and store the resources.
Of course, to show a nice frontend, you must also provide snippets for translation.
The snippets get automatically created from resource and privilege names and are prefixed with `_acl_`.
So the resource `contingentgroup` needs a translation named `_acl_contingentgroup`.
## Privilege names
The default privileges are:
| Privilege name | What it means |
|:--------------:|:-----------------------------------------------------------------------------------:|
| `list` | Entity listing (e.g. indexActions, gridActions) |
| `detail` | Disabled forms, lists of assignments, but only the inspection, not the modification |
| `create` | Creation of new entities |
| `delete` | Removal of existing entities |
| `update` | Updating/changing existing entities |
| `assign` | Changing the assignment of the entity |
| `free` | No restrictions |
It is quite natural to map CRUD actions like this.
However, the assignment is a little less intuitive.
This should help:
* All assignment controllers belong to the resource on the right side of the assignment (e.g., the `B2BContactRole` controller is part of the `role` resource).
* All assignment listings have the detail privilege (e.g., `B2BContactRole:indexAction` is part of the `detail` privilege).
* All actions writing the assignment are part of the assign privilege (e.g. `B2BContactRole:assignAction` is part of the `assign` privilege).
## Automatic generation
You can autogenerate this format with the `RoutingIndexer`.
This service expects a format that is automatically created by the *IndexerService*.
This could be part of your deployment or testing workflow.
```php
require __DIR__ . '/../B2bContact.php';
$indexer = new Shopware\B2B\AclRoute\Framework\RoutingIndexer();
$indexer->generate(\Shopware_Controllers_Frontend_B2bContact::class, __DIR__ . '/my-acl-config.php');
```
The generated file looks like this:
```php
'NOT_MAPPED' => //resource name
array(
'B2bContingentGroup' => // controller name
array(
'index' => 'NOT_MAPPED', // action name => privilege name
[...]
'detail' => 'NOT_MAPPED',
),
),
```
If you spot a privilege or resource that is called `NOT_MAPPED`,
the action is new, and you must update the file to add the correct privilege name.
## Template extension
The ACL implementation is safe at the PHP level.
Any route you have no access to will automatically be blocked, but for a better user experience, you should also extend the template to hide inaccessible actions.
```twig
```
This will add a few vital CSS classes:
Allowed actions:
```html
```
Denied actions:
```html
```
The default behavior is then just to hide the link by setting its display property to `display: none`.
But there are certain specials to this:
* applied to a `form` tag will remove the submit button and disable all form items.
* applied to a table row in the b2b default grid will mute the applied ajax panel action.
## Download
Refer here for [simple example plugin](../example-plugins/B2bAcl.zip).
---
---
url: /docs/v6.5/products/extensions/b2b-suite/guides/storefront/acl-routing.md
---
# ACL and Routing
The ACL Routing component allows you to block Controller Actions for B2B users. It relies on and extends the technologies already defined by the ACL component. To accomplish this, the component directly maps an `action` in a given `controller` to a `resource` (= entity type) and `privilege` (= class of actions). There are two core actions you should know: `index` and `detail`, as you can see in the following acl-config example below.
## Registering routes
All routes that need access rights need to be stored in the database. The B2B Suite provides a service to simplify this process. For it to work correctly, you need an array in a specific format structured like this:
```php
$myAclConfig = [
'contingentgroup' => //resource name
[
'B2bContingentGroup' => // controller name
[
'index' => 'list', // action name => privilege name
[...]
'detail' => 'detail',
],
],
];
```
This configuration array can then be synced to the database by using this service during installation:
```php
Shopware\B2B\AclRoute\Framework\AclRoutingUpdateService::create()
->addConfig($myAclConfig);
```
This way, you can easily create and store the resources. Of course, to show a nice frontend, you must also provide snippets for translation. The snippets get automatically created from resource and privilege names and are prefixed with `_acl_`. So the resource `contingentgroup` needs a translation named `_acl_contingentgroup`.
## Privilege names
The default privileges are:
| Privilege name | What it means |
|:----------------:|:-----------------------------------------------------------------------------------:|
| `list` | Entity listing (e.g. indexActions, gridActions) |
| `detail` | Disabled forms, lists of assignments, but only the inspection, not the modification |
| `create` | Creation of new entities |
| `delete` | Removal of existing entities |
| `update` | Updating/changing existing entities |
| `assign` | Changing the assignment of the entity |
| `free` | No restrictions |
It is quite natural to map CRUD actions like this. However, the assignment is a little less intuitive. This should help:
* All assignment controllers belong to the resource on the right side of the assignment (e.g., the `B2BContactRole` controller is part of the `role` resource).
* All assignment listings have the detail privilege (e.g., `B2BContactRole:indexAction` is part of the `detail` privilege).
* All actions writing the assignment are part of the assign privilege (e.g. `B2BContactRole:assignAction` is part of the `assign` privilege).
## Automatic generation
You can autogenerate this format with the `RoutingIndexer`. This service expects a format that is automatically created by the *IndexerService*.
This could be part of your deployment or testing workflow.
```php
require __DIR__ . '/../B2bContact.php';
$indexer = new Shopware\B2B\AclRoute\Framework\RoutingIndexer();
$indexer->generate(\Shopware_Controllers_Frontend_B2bContact::class, __DIR__ . '/my-acl-config.php');
```
The generated file looks like this:
```php
'NOT_MAPPED' => //resource name
array(
'B2bContingentGroup' => // controller name
array(
'index' => 'NOT_MAPPED', // action name => privilege name
[...]
'detail' => 'NOT_MAPPED',
),
),
```
If you spot a privilege or resource that is called `NOT_MAPPED`,
the action is new, and you must update the file to add the correct privilege name.
## Template extension
The ACL implementation is safe at the PHP level. Any route you have no access to will automatically be blocked, but for a better user experience, you should also extend the template to hide inaccessible actions.
```twig
```
This will add a few vital CSS classes:
Allowed actions:
```html
```
Denied actions:
```html
```
The default behavior is then just to hide the link by setting its display property to `display: none`.
But there are certain specials to this:
* applied to a `form` tag will remove the submit button and disable all form items.
* applied to a table row in the b2b default grid will mute the applied ajax panel action.
## Download
Refer here for [simple example plugin](../../../../../../products/extensions/b2b/b2b-suite/guides/example-plugins/B2bAcl.zip).
---
---
url: /docs/v6.6/products/extensions/b2b-suite/guides/storefront/acl-routing.md
---
# ACL and Routing
The ACL Routing component allows you to block Controller Actions for B2B users.
It relies on and extends the technologies already defined by the ACL component.
To accomplish this, the component directly maps an `action` in a given `controller` to a `resource` (= entity type) and `privilege` (= class of actions).
There are two core actions you should know: `index` and `detail`, as you can see in the following acl-config example below.
## Registering routes
All routes that need access rights need to be stored in the database.
The B2B Suite provides a service to simplify this process.
For it to work correctly, you need an array in a specific format structured like this:
```php
$myAclConfig = [
'contingentgroup' => //resource name
[
'B2bContingentGroup' => // controller name
[
'index' => 'list', // action name => privilege name
[...]
'detail' => 'detail',
],
],
];
```
This configuration array can then be synced to the database by using this service during installation:
```php
Shopware\B2B\AclRoute\Framework\AclRoutingUpdateService::create()
->addConfig($myAclConfig);
```
This way, you can easily create and store the resources.
Of course, to show a nice frontend, you must also provide snippets for translation.
The snippets get automatically created from resource and privilege names and are prefixed with `_acl_`.
So the resource `contingentgroup` needs a translation named `_acl_contingentgroup`.
## Privilege names
The default privileges are:
| Privilege name | What it means |
|:--------------:|:-----------------------------------------------------------------------------------:|
| `list` | Entity listing (e.g. indexActions, gridActions) |
| `detail` | Disabled forms, lists of assignments, but only the inspection, not the modification |
| `create` | Creation of new entities |
| `delete` | Removal of existing entities |
| `update` | Updating/changing existing entities |
| `assign` | Changing the assignment of the entity |
| `free` | No restrictions |
It is quite natural to map CRUD actions like this.
However, the assignment is a little less intuitive.
This should help:
* All assignment controllers belong to the resource on the right side of the assignment (e.g., the `B2BContactRole` controller is part of the `role` resource).
* All assignment listings have the detail privilege (e.g., `B2BContactRole:indexAction` is part of the `detail` privilege).
* All actions writing the assignment are part of the assign privilege (e.g. `B2BContactRole:assignAction` is part of the `assign` privilege).
## Automatic generation
You can autogenerate this format with the `RoutingIndexer`.
This service expects a format that is automatically created by the *IndexerService*.
This could be part of your deployment or testing workflow.
```php
require __DIR__ . '/../B2bContact.php';
$indexer = new Shopware\B2B\AclRoute\Framework\RoutingIndexer();
$indexer->generate(\Shopware_Controllers_Frontend_B2bContact::class, __DIR__ . '/my-acl-config.php');
```
The generated file looks like this:
```php
'NOT_MAPPED' => //resource name
array(
'B2bContingentGroup' => // controller name
array(
'index' => 'NOT_MAPPED', // action name => privilege name
[...]
'detail' => 'NOT_MAPPED',
),
),
```
If you spot a privilege or resource that is called `NOT_MAPPED`,
the action is new, and you must update the file to add the correct privilege name.
## Template extension
The ACL implementation is safe at the PHP level.
Any route you have no access to will automatically be blocked, but for a better user experience, you should also extend the template to hide inaccessible actions.
```twig
```
This will add a few vital CSS classes:
Allowed actions:
```html
```
Denied actions:
```html
```
The default behavior is then just to hide the link by setting its display property to `display: none`.
But there are certain specials to this:
* applied to a `form` tag will remove the submit button and disable all form items.
* applied to a table row in the b2b default grid will mute the applied ajax panel action.
## Download
Refer here for [simple example plugin](../example-plugins/B2bAcl.zip).
---
---
url: /resources/admin-extension-sdk/api-reference/ui/actionButton.md
---
# Action Button
An action button adds a clickable button to an existing area of the Shopware Administration.
Action buttons are typically used to trigger extension-specific actions such as opening a modal, executing a workflow, or navigating to an extension module.
## actionButton.add()
#### Usage
```ts
import { location, ui } from "@shopware-ag/meteor-admin-sdk";
if (location.is(location.MAIN_HIDDEN)) {
ui.actionButton.add({
name: "your-app_customer-detail-action",
entity: "customer",
view: "detail",
label: "Test action",
callback: (entity, entityIds) => {
// TODO: do something
},
});
}
```
#### Parameters
| Name | Required | Description |
| :----------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | true | A unique identifier for your action |
| `entity` | true | The entity this action is for possible values: `product`, `order`, `category`, `promotion`, `customer` or `media`. Value `media` is available in Shopware version 6.7.1 |
| `view` | true | Determines if the action button appears on the listing or detail page, possible values: `detail`,`list` or item. View `item` is only used for entity `media` and in version 6.7.1 |
| `label` | true | The label of your action button |
| `meteorIcon` | false | Meteor icon before label. Available since Shopware v6.7.4.0. Check icon name on https://developer.shopware.com/resources/meteor-icon-kit/ |
| `fileTypes` | false | Media file types you want the action button to be displayed for. Available since Shopware v6.7.6.0. |
| `callback` | true | The callback function where you receive the entity and the entityIds for further processing |
#### Return value
Returns a promise without data.
## Calling app actions
As an app developer you may want to receive the information of the callback function server side.
The following example will render the same action button as the above example but once it gets clicked you will receive a POST request to your app server.
**This will only work for apps. Plugin developers must use an API client directly in their callback.**.
```ts
import { location, ui } from "@shopware-ag/meteor-admin-sdk";
if (location.is(location.MAIN_HIDDEN)) {
ui.actionButton.add({
name: "your-app_customer-detail-action",
entity: "customer",
view: "detail",
label: "Test action",
callback: (entity /* "customer" */, entityIds /* ["..."] */) => {
app.webhook.actionExecute({
url: "http://your-app.com/customer-detail-action",
entityIds,
entity,
});
},
});
}
```
## Example: Add action button in customer detail page

```ts
import { ui } from "@shopware-ag/meteor-admin-sdk";
ui.actionButton.add({
name: "your-app_customer-detail-action",
entity: "customer",
view: "detail",
meteorIcon: "regular-analytics",
label: "Test action",
callback: (entity /* "customer" */, entityIds /* ["..."] */) => {
app.webhook.actionExecute({
url: "http://your-app.com/customer-detail-action",
entityIds,
entity,
});
},
});
```
## Example: Add action button in media item

```ts
import { ui } from "@shopware-ag/meteor-admin-sdk";
ui.actionButton.add({
name: "test-media-button",
entity: "media",
view: "item",
meteorIcon: "regular-tools-alt",
label: "Open in Image editor",
callback: (entity /* "media" */, entityIds /* ["..."] */) => {
// TODO: Navigate to image editor app
},
});
```
---
---
url: /docs/v6.5/guides/plugins/plugins/framework/flow/action-transactions.md
---
# Action transactions
## Overview
In this guide, you will learn how to run your action code inside a transaction. This may be important for you if you want to graciously handle rollbacks in certain scenarios. We have implemented various abstractions to ease this process; however, you need to opt in.
For some more background, please see the ADR [Action Transactions](../../../../../resources/references/adr/2024-02-11-transactional-flow-actions).
## Prerequisites
In order to make your action run inside a database transaction, you will need an existing Flow Action. Therefore, you can refer to the [Add Flow Builder Action Guide.](./add-flow-builder-action)
## Run your action inside a transaction
All you have to do is to implement the `Shopware\Core\Content\Flow\Dispatching\TransactionalAction` interface. It does not have any methods to implement.
When your action implements the interface the Flow Dispatcher will wrap your action in a transaction. If an exception is thrown, it will be caught, the transaction will be rolled back, and an error is logged.
::: code-group
```php [{plugin root}/src/Core/Content/Flow/Dispatching/Action/CreateTagAction.php]
repo->find(...);
} catch (NotFoundException $e) {
throw TransactionFailedException::because($e);
}
}
}
```
## Under what circumstances will the transaction be rolled back?
The transaction will be rollback if either of the following are true:
1. If Doctrine throws an instance of `Doctrine\DBAL\Exception` during commit.
2. If the action throws an instance of `TransactionFailedException` during execution.
3. If another non-handled exception is thrown during the action execution.
If the transaction fails, then an error will be logged.
Also, if the transaction has been performed inside a nested transaction without save points enabled (which is the default in Shopware), the exception will be rethrown.
This is because the calling code knows something went wrong and is able to handle it correctly, by rolling back instead of committing. In this scenario, the connection will be marked as rollback only.
---
---
url: /docs/v6.6/guides/plugins/plugins/framework/flow/action-transactions.md
---
# Action transactions
## Overview
In this guide, you will learn how to run your action code inside a transaction. This may be important for you if you want to graciously handle rollbacks in certain scenarios. We have implemented various abstractions to ease this process; however, you need to opt in.
For some more background, please see the ADR [Action Transactions](../../../../../resources/references/adr/2024-02-11-transactional-flow-actions).
## Prerequisites
In order to make your action run inside a database transaction, you will need an existing Flow Action. Therefore, you can refer to the [Add Flow Builder Action Guide.](./add-flow-builder-action)
## Run your action inside a transaction
All you have to do is to implement the `Shopware\Core\Content\Flow\Dispatching\TransactionalAction` interface. It does not have any methods to implement.
When your action implements the interface the Flow Dispatcher will wrap your action in a transaction. If an exception is thrown, it will be caught, the transaction will be rolled back, and an error is logged.
::: code-group
```php [{plugin root}/src/Core/Content/Flow/Dispatching/Action/CreateTagAction.php]
repo->find(...);
} catch (NotFoundException $e) {
throw TransactionFailedException::because($e);
}
}
}
```
## Under what circumstances will the transaction be rolled back?
The transaction will be rollback if either of the following are true:
1. If Doctrine throws an instance of `Doctrine\DBAL\Exception` during commit.
2. If the action throws an instance of `TransactionFailedException` during execution.
3. If another non-handled exception is thrown during the action execution.
If the transaction fails, then an error will be logged.
Also, if the transaction has been performed inside a nested transaction without save points enabled (which is the default in Shopware), the exception will be rethrown.
This is because the calling code knows something went wrong and is able to handle it correctly, by rolling back instead of committing. In this scenario, the connection will be marked as rollback only.
---
---
url: /docs/resources/references/core-reference/actions-reference.md
---
# Actions Reference
## B2B
| Class | Description | Component |
|:------------------------------------------------|:---------------------------------------------------------------|:--------------------|
| ChangeEmployeeStatusAction | Assigns the configured status to the employee | Employee Management |
| ChangeCustomerSpecificFeaturesAction | Adds or removes the configured b2b components for the customer | Employee Management |
---
---
url: /docs/v6.5/resources/references/core-reference/actions-reference.md
---
# Actions Reference
## B2B
| Class | Description | Component |
|:------------------------------------------------|:---------------------------------------------------------------|:--------------------|
| ChangeEmployeeStatusAction | Assigns the configured status to the employee | Employee Management |
| ChangeCustomerSpecificFeaturesAction | Adds or removes the configured b2b components for the customer | Employee Management |
---
---
url: /docs/v6.6/resources/references/core-reference/actions-reference.md
---
# Actions Reference
## B2B
| Class | Description | Component |
|:------------------------------------------------|:---------------------------------------------------------------|:--------------------|
| ChangeEmployeeStatusAction | Assigns the configured status to the employee | Employee Management |
| ChangeCustomerSpecificFeaturesAction | Adds or removes the configured b2b components for the customer | Employee Management |
---
---
url: /docs/guides/development/testing/e2e-playwright/actor-pattern.md
---
# Actor pattern
The actor pattern is a basic concept that we added to our test suite. It is something not related to Playwright, but similar concepts exist in other testing frameworks. We implemented it to create reusable test logic that can be used in a human-readable form, without abstracting away Playwright as a framework. So you are free to use it or not. Any standard Playwright functionality will still be usable in your tests.
The concept adds two new entities besides the already mentioned [page objects](./page-object.md)
* **Actor**: A specific user with a given context performing actions (tasks) inside the application.
* **Task**: A specific action performed by an actor.
* **Pages**: A page of the application on which an actor performs a task.
## Actors
The Actor class is a lightweight solution to simplify the execution of reusable test logic or navigate to a specific page.
## Properties
* `name`: The human-readable name of the actor.
* `page`: A Playwright page context that the actor is navigating.
## Primary methods
* `goesTo`: Accepts a URL of a page the actor should navigate to.
* `attemptsTo`: Accepts a "task" function with reusable test logic that the actor should perform.
* `expects`: A one-to-one export of the Playwright `expect` method to use it in the actor pattern.
These methods lead to the following pattern:
* The **actor** *goes to* a **page**.
* The **actor** *attempts to* perform a certain **task**.
* The **actor** *expects* a certain result.
Translated into test code, this pattern can look like this:
```typescript
import { test } from "./../BaseTestFile";
test("Product detail test scenario", async ({
ShopCustomer,
StorefrontProductDetail,
TestDataService,
}) => {
const product = await TestDataService.createBasicProduct();
await ShopCustomer.goesTo(StorefrontProductDetail.url(product));
await ShopCustomer.attemptsTo(AddProductToCart(product));
await ShopCustomer.expects(
StorefrontProductDetail.offCanvasSummaryTotalPrice
).toHaveText("β¬99.99*");
});
```
In this example, you can see that this pattern creates very comprehensible tests, even for non-tech people. They also make it easier to abstract simple test logic that might be used in different scenarios into executable tasks, like adding a product to the cart.
The test suite offers two different actors by default:
* `ShopCustomer`: A user that is navigating the Storefront.
* `ShopAdmin`: A user who manages Shopware via the Administration.
## Accessibility methods
* `a11y_checks`: Accepts a locator and verifies if the desired locator is both focused and displays a visible focus indicator. This is automatically called via `presses`, `fillsIn`, and `selectsRadioButton`.
* `presses`: An extension of the Playwright `press` method to include `a11y_checks` as well as automatically apply a keyboard key press per default browser keyboard mappings (which can also be overridden). A keyboard focused alternative to the Playwright `click` method.
* `fillsIn`: An extension of the Playwright `fill` method to include `a11y_checks`.
* `selectsRadioButton`: Selects radio buttons using keyboard navigation in addition to verifying visible focus (via `presses`).
These methods serve as a way to enforce better accessibility practices by using keyboard navigation and checking for visible focus indicators (both of which are WCAG requirements). They can be used both in tests and tasks.
:::info
Be aware that the Playwright `click` method automatically includes a number of [actionability checks](https://playwright.dev/docs/actionability) to combat flakiness. When utilizing the Actor accessibility methods, you may need to adjust your tests to individually assert some of these actionability checks for certain locators yourself.
:::
## Tasks
Tasks are small chunks of reusable test logic that can be passed to the `attemptsTo` method of an actor. They are created via Playwright fixtures and have access to the same dependencies. Every executed task will automatically be wrapped in a test step of Playwright, so you get nicely structured reports of your tests.
**Basic Example**
```typescript
import { test as base } from "@playwright/test";
import type { Task } from "../../../types/Task";
import type { FixtureTypes } from "../../../types/FixtureTypes";
import type { Customer } from "../../../types/ShopwareTypes";
export const Login = base.extend<{ Login: Task }, FixtureTypes>({
Login: async (
{
ShopCustomer,
DefaultSalesChannel,
StorefrontAccountLogin,
StorefrontAccount,
},
use
) => {
const task = (customCustomer?: Customer) => {
return async function Login() {
const customer = customCustomer
? customCustomer
: DefaultSalesChannel.customer;
await ShopCustomer.goesTo(StorefrontAccountLogin.url());
await ShopCustomer.fillsIn(
StorefrontAccountLogin.emailInput,
customer.email
);
await ShopCustomer.fillsIn(
StorefrontAccountLogin.passwordInput,
customer.password
);
await ShopCustomer.presses(StorefrontAccountLogin.loginButton);
await ShopCustomer.expects(
StorefrontAccount.personalDataCardTitle
).toBeVisible();
};
};
await use(task);
},
});
```
This fixture is the "Login" task and performs a simple Storefront login of the default customer via keyboard navigation (automatically includes `a11y_checks` assertions). Every time we need a logged-in shop customer, we can simply reuse this logic in our test.
```typescript
import { test } from "./../BaseTestFile";
test("Customer login test scenario", async ({ ShopCustomer, Login }) => {
await ShopCustomer.attemptsTo(Login());
});
```
To keep tests easily readable, use names for your tasks so that in the test itself, the code line resembles the `Actor.attemptsTo(doSomething)` pattern as closely as possible.
```typescript
// Bad example
await ShopCustomer.attemptsTo(ProductCart);
// Better example
await ShopCustomer.attemptsTo(PutProductIntoCart);
```
**Page Object Model Example**
```typescript
import type { Page, Locator } from "playwright-core";
import type { PageObject } from "../../types/PageObject";
export class CheckoutConfirm implements PageObject {
public readonly paymentMethodRadioGroup: Locator;
public readonly page: Page;
constructor(page: Page) {
this.page = page;
this.paymentMethodRadioGroup = page.locator(".checkout-card", {
hasText: "Payment Method",
});
}
url() {
return "checkout/confirm";
}
}
```
This page object defines the payment method radio group locator.
```typescript
import { test as base } from "@playwright/test";
import type { Task } from "../../../types/Task";
import type { FixtureTypes } from "../../../types/FixtureTypes";
export const SelectPaymentMethod = base.extend<
{ SelectPaymentMethod: Task },
FixtureTypes
>({
SelectPaymentMethod: async (
{ ShopCustomer, StorefrontCheckoutConfirm },
use
) => {
const task = (paymentOptionName: string) => {
return async function SelectPaymentMethod() {
const paymentMethods =
StorefrontCheckoutConfirm.paymentMethodRadioGroup;
const paymentOptionRadioButton = paymentMethods.getByRole("radio", {
name: paymentOptionName,
});
await ShopCustomer.selectsRadioButton(
paymentMethods,
paymentOptionName
);
await ShopCustomer.expects(paymentOptionRadioButton).toBeChecked();
};
};
await use(task);
},
});
```
This fixture is the "SelectPaymentMethod" task, which selects the desired radio button in the `paymentMethodRadioGroup` defined in the page object using keyboard navigation (automatically includes `a11y_checks` assertions).
To use "SelectPaymentMethod" in a test, you simply pass the name of the desired payment option. Here is a sample scenario for a successful checkout that demonstrates how to combine multiple tasks to build your test scenarios.
```typescript
import { test } from "./../BaseTestFile";
test("Customer successfully orders product", async ({
ShopCustomer,
TestDataService,
Login,
StorefrontProductDetail,
AddProductToCart,
ProceedFromProductToCheckout,
SelectPaymentMethod,
ConfirmOrder,
}) => {
const product = await TestDataService.createBasicProduct();
await ShopCustomer.attemptsTo(Login());
await ShopCustomer.goesTo(StorefrontProductDetail.url(product));
await ShopCustomer.attemptsTo(AddProductToCart(product));
await ShopCustomer.attemptsTo(ProceedFromProductToCheckout());
await ShopCustomer.attemptsTo(SelectPaymentMethod("Invoice"));
await ShopCustomer.attemptsTo(ConfirmOrder());
});
```
You can create your tasks in the same way to make them available for the actor pattern. Every task is just a simple Playwright fixture containing a function call with the corresponding test logic. Make sure to merge your task fixtures with other fixtures you created in your base test file. You can use the `mergeTests` method of Playwright to combine several fixtures into one test extension. Use `/src/tasks/shop-customer-tasks.ts` or `/src/tasks/shop-admin-tasks.ts` for that.
---
---
url: /docs/products/extensions/advanced-search/How-to-modify-completion.md
---
# Add / Modify Completion
The Advanced Search does not use the default Elasticsearch completion because it only supports a fixed order and the storage size is high. As an alternative, Advanced Search uses aggregations to find the most important word combinations for your search input.
## Adding completion to your definition mapping
To index our own completion keywords, we need to inject `Shopware\Commercial\AdvancedSearch\Domain\Completion\CompletionDefinitionEnrichment` into your ES definition and call enrich methods in `getMapping` and `fetch` as following example:
Example:
*The definition is from the [previous example](./How-to-define-your-custom-Elasticsearch-definition):*
```php
['includes' => ['id']],
// to add the mapping of completion field in your definition
'properties' => array_merge($properties, $this->completionDefinitionEnrichment->enrichMapping()),
];
}
public function fetch(array $ids, Context $context): array
{
// ...
// to add the completion keywords to the existing data
return $this->completionDefinitionEnrichment->enrichData($this->getEntityDefinition(), $documents);
}
}
```
## Add/modify completion keywords
By default, each of Shopware's ES definitions has a set of `string` fields to be considered as completion keywords. This configuration is realized via the parameter `%advanced_search.completion%`, if the configured fields for your definition are not set, all StringFields of the definition will be used as completion keywords.
For example, you can add or modify this configuration in `config/packages/advanced_search.yaml`:
```yaml
advanced_search:
completion:
your_custom_entity:
- email
- company
```
If you want to have more control over the completion, such as using static texts from files or parsing a field from another data source as completion keywords, you might want to decorate the service `\Shopware\Commercial\AdvancedSearch\Domain\Completion\CompletionDefinitionEnrichment::enrichData` instead.
---
---
url: /docs/v6.5/products/extensions/advanced-search/How-to-modify-completion.md
---
# Add / Modify Completion
The Advanced Search does not use the default Elasticsearch completion because it only supports a fixed order and the storage size is high. As an alternative, Advanced Search uses aggregations to find the most important word combinations for your search input.
## Adding completion to your definition mapping:
To index our own completion keywords, we need to inject `Shopware\Commercial\AdvancedSearch\Domain\Completion\CompletionDefinitionEnrichment` into your ES definition and call enrich methods in `getMapping` and `fetch` as following example:
Example:
*The definition is from the [previous example](./How-to-define-your-custom-Elasticsearch-definition):*
```php
['includes' => ['id']],
// to add the mapping of completion field in your definition
'properties' => array_merge($properties, $this->completionDefinitionEnrichment->enrichMapping()),
];
}
public function fetch(array $ids, Context $context): array
{
// ...
// to add the completion keywords to the existing data
return $this->completionDefinitionEnrichment->enrichData($this->getEntityDefinition(), $documents);
}
}
```
## Add/modify completion keywords
By default, each of Shopware's ES definitions has a set of `string` fields to be considered as completion keywords. This configuration is realized via the parameter `%advanced_search.completion%`, if the configured fields for your definition are not set, all StringFields of the definition will be used as completion keywords.Β
For example, you can add or modify this configuration in `config/packages/advanced_search.yaml`:
```yaml
advanced_search:
completion:
your_custom_entity:
- email
- company
```
If you want to have more control over the completion, such as using static texts from files or parsing a field from another data source as completion keywords, you might want to decorate the service `\Shopware\Commercial\AdvancedSearch\Domain\Completion\CompletionDefinitionEnrichment::enrichData` instead.
---
---
url: /docs/v6.6/products/extensions/advanced-search/How-to-modify-completion.md
---
# Add / Modify Completion
The Advanced Search does not use the default Elasticsearch completion because it only supports a fixed order and the storage size is high. As an alternative, Advanced Search uses aggregations to find the most important word combinations for your search input.
## Adding completion to your definition mapping
To index our own completion keywords, we need to inject `Shopware\Commercial\AdvancedSearch\Domain\Completion\CompletionDefinitionEnrichment` into your ES definition and call enrich methods in `getMapping` and `fetch` as following example:
Example:
*The definition is from the [previous example](./How-to-define-your-custom-Elasticsearch-definition):*
```php
['includes' => ['id']],
// to add the mapping of completion field in your definition
'properties' => array_merge($properties, $this->completionDefinitionEnrichment->enrichMapping()),
];
}
public function fetch(array $ids, Context $context): array
{
// ...
// to add the completion keywords to the existing data
return $this->completionDefinitionEnrichment->enrichData($this->getEntityDefinition(), $documents);
}
}
```
## Add/modify completion keywords
By default, each of Shopware's ES definitions has a set of `string` fields to be considered as completion keywords. This configuration is realized via the parameter `%advanced_search.completion%`, if the configured fields for your definition are not set, all StringFields of the definition will be used as completion keywords.
For example, you can add or modify this configuration in `config/packages/advanced_search.yaml`:
```yaml
advanced_search:
completion:
your_custom_entity:
- email
- company
```
If you want to have more control over the completion, such as using static texts from files or parsing a field from another data source as completion keywords, you might want to decorate the service `\Shopware\Commercial\AdvancedSearch\Domain\Completion\CompletionDefinitionEnrichment::enrichData` instead.
---
---
url: >-
/docs/products/extensions/advanced-search/How-to-add-modify-language-analyzers-stopwords-stemmer.md
---
# Add / Modify language analyzers, stopwords, stemmer
With the introduction of the multi-language index, support for built-in [Elasticsearch language analyzers](https://www.elastic.co/docs/reference/text-analysis/analysis-lang-analyzer) was also introduced.
This would help language-based fields have different analyzers for each language's specific features, like stopwords, stemmers, and normalization, out of the box.
You can also add more or customize the language analyzer by overriding the analyzer parameter in `custom/plugins/SwagCommercial/src/AdvancedSearch/Resources/config/packages/advanced_search.yaml`
For example:
```yaml
advanced_search:
analysis:
analyzer:
sw_your_custom_language_analyzer:
type: custom
tokenizer: standard
filter: ['lowercase', 'my_stopwords_filter', 'my_stemmer_filter']
filter:
my_stopwords_filter:
type: 'stop'
stopwords: ['foo', 'bar']
my_stemmer_filter:
type: 'stemmer'
language: 'english'
# It's important to map your analyzer with the language iso code
language_analyzer_mapping:
custom_iso: sw_your_custom_language_analyzer
```
## Compound-word decomposition
German catalogs often contain closed compound nouns (for example `Lederjacke` or `Akkubohrer`). Without decomposition, a search for `Jacke` does not match a product named `Lederjacke`.
Since Commercial 7.12.0, Advanced Search ships a `dictionary_decompounder` filter that splits compound words into their parts at index time, using an editable, per-language dictionary. A curated German root-word dictionary is seeded by default. Decomposition is applied at index time only, on the technical-term index analyzer (`sw__technical_term_index_analyzer`); the search query itself is never expanded into its parts.
The dictionaries are stored as entities and can be managed through the Admin API:
* `advanced_search_compound_dictionary` β the `wordList` of root words used to split compounds.
* `advanced_search_stopword_dictionary` β custom `stopwords`, which are stripped at both index and search time.
After editing a dictionary, run `bin/console es:index` so the updated analyzer configuration is applied to the index.
---
---
url: >-
/docs/v6.5/products/extensions/advanced-search/How-to-add-modify-language-analyzers-stopwords-stemmer.md
---
# Add / Modify language analyzers, stopwords, stemmer
With the introduction of the multi-language index, support for built-in [Elasticsearch language analyzers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html) was also introduced.
This would help language-based fields have different analyzers for each language's specific features, like stopwords, stemmers, and normalization, out of the box.
You can also add more or customize the language analyzer by overriding the analyzer parameter in `custom/plugins/SwagCommercial/src/AdvancedSearch/Resources/config/packages/advanced_search.yaml`
For example:
```yaml
advanced_search:
analysis:
analyzer:
sw_your_custom_language_analyzer:
type: custom
tokenizer: standard
filter: ['lowercase', 'my_stopwords_filter', 'my_stemmer_filter']
filter:
my_stopwords_filter:
type: 'stop'
stopwords: ['foo', 'bar']
my_stemmer_filter:
type: 'stemmer'
language: 'english'
# It's important to map your analyzer with the language iso code
language_analyzer_mapping:
custom_iso: sw_your_custom_language_analyzer
```
---
---
url: >-
/docs/v6.6/products/extensions/advanced-search/How-to-add-modify-language-analyzers-stopwords-stemmer.md
---
# Add / Modify language analyzers, stopwords, stemmer
With the introduction of the multi-language index, support for built-in [Elasticsearch language analyzers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html) was also introduced.
This would help language-based fields have different analyzers for each language's specific features, like stopwords, stemmers, and normalization, out of the box.
You can also add more or customize the language analyzer by overriding the analyzer parameter in `custom/plugins/SwagCommercial/src/AdvancedSearch/Resources/config/packages/advanced_search.yaml`
For example:
```yaml
advanced_search:
analysis:
analyzer:
sw_your_custom_language_analyzer:
type: custom
tokenizer: standard
filter: ['lowercase', 'my_stopwords_filter', 'my_stemmer_filter']
filter:
my_stopwords_filter:
type: 'stop'
stopwords: ['foo', 'bar']
my_stemmer_filter:
type: 'stemmer'
language: 'english'
# It's important to map your analyzer with the language iso code
language_analyzer_mapping:
custom_iso: sw_your_custom_language_analyzer
```
---
---
url: /docs/guides/plugins/apps/app-scripts/add-api-endpoint.md
---
# Add an API Endpoint
::: info
This guide relies on [app scripts](../app-scripts/index.md), introduced from Shopware 6.4.8.0 version.
:::
## Overview
This guide shows how you can add a custom API endpoint that delivers dynamic data starting from zero.
After reading, you will be able to:
* Create the basic setup of an app.
* Execute app scripts and use them to model custom logic.
* Fetch, filter, and aggregate data from Shopware.
* Consume HTTP parameters and create responses.
## Prerequisites
* A Shopware cloud store
* Basic CLI usage (creating files, directories, running commands)
* Installed and configured [shopware-cli](../../../../products/tools/cli/index.md) tools
* General knowledge of [Twig Syntax](https://twig.symfony.com/)
* A text editor
## Create the app wrapper
We need to create the app "wrapper", the so-called app manifest within a new directory. Let's call that the project directory:
```text
MyApiExtension/
ββ manifest.xml
```
::: info
When using a self-hosted Shopware version, you can also create the project directory in the `custom/apps` directory of your Shopware installation. However, the descriptions in this guide apply to both Shopware cloud and self-hosted stores.
:::
Next, we will put our basic configuration into the file we just created.
::: code-group
```xml [manifest.xml]
MyApiExtensionThis app adds a Topseller API endpointshopware AG(c) shopware AG1.0.0MITorderorder_line_itemproduct
```
:::
Besides some metadata, like a name, description, or version, this file contains permissions that the app requires.
We will need them later on when performing searches.
## Create the script
We will define our new API endpoint in a script file based on [app scripts](../app-scripts/index.md).
There are specific directory conventions that we have to follow to register a new API endpoint script.
The prefix for our API endpoint is one of the following and cannot be changed:
| API | API consumers / callers | Prefix |
|------------|------------------------------|-----------------------|
| Store API | Customer-facing integrations | `/store-api/script/` |
| Admin API | Backend integrations | `/api/script/` |
| Storefront | Default Storefront | `/storefront/script/` |
::: info
You might wonder why the Storefront shows up in that table. In Storefront endpoints, you can render not only JSON but also twig templates.
But use them with care - whenever you create a Storefront endpoint, your app will not be compatible with headless consumers.
Learn more about the different endpoints in [custom endpoints](../app-scripts/custom-endpoints.md)
:::
### Directory structure
In this example, we're going to create a Store API endpoint. We want to provide logic that returns the top-selling products for a specific category.
So let's use the following endpoint naming:
`/store-api/script/swag/topseller`
You see that we have added a custom subdirectory `swag` in the route.
This is a good practice because we can prevent naming collisions between different apps.
Slashes (or subdirectories) in the endpoint path are represented by a hyphen in the name of the directory that contains the script.
```text
MyApiExtension/
ββ Resources/
β ββ scripts/
β β ββ store-api-swag-topseller/ <-- /store-api/script/swag/topseller
β β β ββ topseller-script.twig
ββ manifest.xml
```
This directory naming causes Shopware to expose the script on two routes:
* `/store-api/script/swag/topseller` and
* `/store-api/script/swag-topseller`
### Add custom logic and install
Let's start with a simple script to see it in action:
```twig
// Resources/scripts/store-api-swag-topseller/topseller-script.twig
{% block response %}
{% set response = services.response.json({ test: 'This is my API endpoint' }) %}
{% do hook.setResponse(response) %}
{% endblock %}
```
Next we will install the App using the Shopware CLI.
::: info
If this is your first time using the Shopware CLI, you have to [install](../../../../products/tools/cli/index.md) it first. Next, configure it using the `shopware-cli project config init` command.
:::
Run this command from the root of the project directory.
```shell
shopware-cli project extension upload . --activate
```
This command will create a zip file from the specified extension directory (the one you are in), upload it to your configured store and activate it.
### Call the endpoint
You can call the endpoint using this curl command.
::: info
Follow this guide for more information on using the Store API : [Store API Authentication & Authorization](https://shopware.stoplight.io/docs/store-api/ZG9jOjEwODA3NjQx-authentication-and-authorisation)
:::
```shell
curl --request GET \
--url http:///store-api/script/swag/topseller \
--header 'sw-access-key: insert-your-access-key'
```
which should return something like:
```json
{"apiAlias":"store_api_swag_topseller_response","test":"This is my API endpoint"}
```
However, instead of using curl, we recommend using visual clients to test the API - such as [Postman](https://www.postman.com/downloads/) or [Insomnia](https://insomnia.rest/download).
## Fill in the logic
For now, our script is not really doing anything. Let's change that.
```twig
// Resources/scripts/store-api-swag-topseller/topseller-script.twig
{% block response %}
{% set categoryId = hook.request.categoryId %}
{% set criteria = {
aggregations: [
{
name: "categoryFilter",
type: "filter",
filter: [{
type: "equals",
field: "order.lineItems.product.categoryIds",
value: categoryId
}],
aggregation: {
name: "orderedProducts",
type: "terms",
field: "order.lineItems.productId",
aggregation: {
name: "quantityItemsOrdered",
type : "sum",
field: "order.lineItems.quantity"
}
}
}
]
} %}
{% set orderAggregations = services.repository.aggregate('order', criteria) %}
{% set response = services.response.json(orderAggregations.first.jsonSerialize) %}
{% do hook.setResponse(response) %}
{% endblock %}
```
What happened here?
We wrap everything in a block named `response`. That way, we will get access to useful objects and services, so we can build a response.
### Search criteria and fetching results
We start by reading the requested category id using `hook.request.categoryId`. In general, we can access post body parameters using `hook.request.*`.
In the following lines, we define a search criteria. The criteria contain a description of the data we want to fetch:
1. First, we filter out all products not inside the category that was requested, using a filter aggregation.
2. The following lines contain two further nested aggregations:
1. The first one groups all products from all orders using their id.
2. The second one sums up the number of ordered items in each order.
Ultimately, it gives a result of all products that have been ordered and the total ordered.
::: info
To learn more about the structure of search criteria, check out the [Search Criteria guide](../../../development/integrations-api/search-criteria.md).
:::
We now send a request to the database to retrieve the result using:
```twig
{% set orderAggregations = services.repository.aggregate('order', criteria) %}
```
### Building the response
In the final step, we build the response. We use the `services.response.json()` method to convert the serialized json representation of our aggregation into a json response object named `response`.
```twig
{% set response = services.response.json(orderAggregations.first.jsonSerialize) %}
```
Finally, we just set the response of the hook to the result from above:
```twig
{% do hook.setResponse(response) %}
```
It is important to do all this within the `response` block of the twig script. Otherwise, you will get errors when calling the script.
### Installing the plugin
Next, we re-install our plugin using the same command as before:
```shell
shopware-cli project extension upload . --activate
```
::: warning
Remember, if you made changes to the `manifest.xml` file in the meantime, also pass the `--increase-version` parameter, else Shopware will not pick up the changes:
```shell
shopware-cli project extension upload . --activate --increase-version
```
:::
We can now call our endpoint again:
```shell
curl --request GET \
--url http:///store-api/script/swag/topseller \
--header 'sw-access-key: insert-your-access-key'
```
and receive a different result:
```json
{
"apiAlias": "store_api_swag_topseller_response",
"buckets": [
{
"key": "0060b9b2b3804244bf8ba98cdad50234",
"count": 3,
"quantityItemsOrdered": {
"extensions": [],
"sum": 15
},
"apiAlias": "aggregation_bucket"
},
{
"key": "a65d918f883c47778a65b73548f456ea",
"count": 2,
"quantityItemsOrdered": {
"extensions": [],
"sum": 3
},
"apiAlias": "aggregation_bucket"
},
{
"key": "6b67935063c84bde8e9d86f25a47c69d",
"count": 3,
"quantityItemsOrdered": {
"extensions": [],
"sum": 8
},
"apiAlias": "aggregation_bucket"
}
]
}
```
## Wrap-Up
This tutorial covered the basics of app development using app scripts and some filtering and aggregation logic.
In a proper app, you should consider the following points:
* Input parameter validation
* Format and limit the result
* Define an API contract (endpoint structure) first and build after that
* The search result does not show actual top sellers but just the quantity of products ordered
## Where to continue
* More on adding [custom endpoints](../app-scripts/custom-endpoints.md)
* See how you can use [Twig functions](../app-scripts/index.md#extended-syntax) in app scripts
* Working with [DAL Aggregations](../../../development/troubleshooting/dal-reference/aggregations-reference.md)
---
---
url: /docs/guides/plugins/themes/assets/add-assets-to-theme.md
---
# Add Assets to a Theme
## Overview
Your theme can include custom assets like images. This short guide will show you where to store your custom assets and how you can link them in Twig and SCSS.
## Prerequisites
This guide is built upon the guide on creating a first theme:
## Using custom assets
There are basically two ways of adding custom assets to your theme. The first one is using the `theme.json` to define the path to your custom assets, the second being the default way of using custom assets in plugins. We'll take a closer look at them in the following sections.
### Adding assets in theme.json file
While working with a theme you might have noted the [Theme configuration](../configuration/theme-configuration.md), where you can configure paths to custom assets like images, fonts, etc. Configure your asset path accordingly:
```javascript
// /src/Resources/theme.json
{
...
"asset": [
"app/storefront/src/assets"
]
...
}
```
Next, run the command `bin/console theme:compile`. The assets from the path defined in the `theme.json` file will be copied by the `theme:compile` command to `/public/theme/` along with the compiled CSS and JS, which are stored in a separate folder.
```text
// /public
#
.
βββ theme
βββ
β βββ css
β β βββ all.css
β βββ js
β βββ all.js
βββ
βββ asset
βββ your-image.png <-- Your asset is copied here
```
### Adding assets the plugin way
This way of adding custom assets refers to the default way of dealing with assets. For more details, please check out the article that specifically addresses this topic:
## Linking to assets
You can link to the asset with the Twig [asset](https://symfony.com/doc/current/templates.html#linking-to-css-javascript-and-image-assets) function:
```html
```
In SCSS, you can link to the asset like the following:
```css
body {
background-image: url('#{$app-css-relative-asset-path}/your-image.png');
}
```
## Next steps
Now that you know how to use your assets in a theme, here is a list of other related topics where assets can be used.
* [Customize templates](../../plugins/storefront/templates/customize-templates.md)
---
---
url: /docs/v6.5/guides/plugins/themes/add-assets-to-theme.md
---
# Add Assets to a Theme
## Overview
Your theme can include custom assets like images. This short guide will show you where to store your custom assets and how you can link them in Twig and SCSS.
## Prerequisites
This guide is built upon the guide on creating a first theme:
## Using custom assets
There are basically two ways of adding custom assets to your theme. The first one is using the `theme.json` to define the path to your custom assets, the second being the default way of using custom assets in plugins. We'll take a closer look at them in the following sections.
### Adding assets in theme.json file
While working with your own theme, you might have already come across the [Theme configuration](theme-configuration). In there, you have the possibility to configure your paths to your custom assets like images, fonts, etc. This way, please configure your asset path accordingly.
```javascript
// /src/Resources/theme.json
{
...
"asset": [
"app/storefront/src/assets"
]
...
}
```
Next, run the command `bin/console theme:compile`. The assets from the path defined in the `theme.json` file will be copied by the `theme:compile` command to `/public/theme/` along with the compiled CSS and JS, which are stored in a separate folder.
```text
// /public
#
.
βββ theme
βββ
β βββ css
β β βββ all.css
β βββ js
β βββ all.js
βββ
βββ asset
βββ your-image.png <-- Your asset is copied here
```
### Adding assets the plugin way
This way of adding custom assets refers to the default way of dealing with assets. For more details, please check out the article that specifically addresses this topic:
## Linking to assets
You can link to the asset with the twig [asset](https://symfony.com/doc/current/templates.html#linking-to-css-javascript-and-image-assets) function:
```html
```
In SCSS, you can link to the asset like the following:
```css
body {
background-image: url('#{$app-css-relative-asset-path}/your-image.png');
}
```
## Next steps
Now that you know how to use your assets in a theme, here is a list of other related topics where assets can be used.
* [Customize templates](../plugins/storefront/customize-templates)
---
---
url: /docs/v6.6/guides/plugins/themes/add-assets-to-theme.md
---
# Add Assets to a Theme
## Overview
Your theme can include custom assets like images. This short guide will show you where to store your custom assets and how you can link them in Twig and SCSS.
## Prerequisites
This guide is built upon the guide on creating a first theme:
## Using custom assets
There are basically two ways of adding custom assets to your theme. The first one is using the `theme.json` to define the path to your custom assets, the second being the default way of using custom assets in plugins. We'll take a closer look at them in the following sections.
### Adding assets in theme.json file
While working with your own theme, you might have already come across the [Theme configuration](theme-configuration). In there, you have the possibility to configure your paths to your custom assets like images, fonts, etc. This way, please configure your asset path accordingly.
```javascript
// /src/Resources/theme.json
{
...
"asset": [
"app/storefront/src/assets"
]
...
}
```
Next, run the command `bin/console theme:compile`. The assets from the path defined in the `theme.json` file will be copied by the `theme:compile` command to `/public/theme/` along with the compiled CSS and JS, which are stored in a separate folder.
```text
// /public
#
.
βββ theme
βββ
β βββ css
β β βββ all.css
β βββ js
β βββ all.js
βββ
βββ asset
βββ your-image.png <-- Your asset is copied here
```
### Adding assets the plugin way
This way of adding custom assets refers to the default way of dealing with assets. For more details, please check out the article that specifically addresses this topic:
## Linking to assets
You can link to the asset with the twig [asset](https://symfony.com/doc/current/templates.html#linking-to-css-javascript-and-image-assets) function:
```html
```
In SCSS, you can link to the asset like the following:
```css
body {
background-image: url('#{$app-css-relative-asset-path}/your-image.png');
}
```
## Next steps
Now that you know how to use your assets in a theme, here is a list of other related topics where assets can be used.
* [Customize templates](../plugins/storefront/customize-templates)
---
---
url: /docs/resources/references/adr/2022-09-23-add-bootstrap-util.md
---
# Add bootstrap JS-plugin initialization utility to storefront JS
::: info
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2022-09-23-add-bootstrap-util.md)
:::
## Context
* Some Bootstrap JavaScript plugins have to be initialized manually to the desired DOM elements, see: https://getbootstrap.com/docs/4.3/components/tooltips/#example-enable-tooltips-everywhere
* This is not needed for all Bootstrap plugins. Modals for example work out of the box without extra initialization.
* Currently, we only initialize Tooltips using `src/utility/tooltip/tooltip.util.js`
* On dynamic content changes (listing pagination, ajax OffCanvas cart, etc.) Bootstrap plugins like Tooltip are no longer working.
* For example: It is not possible to show Tooltips in the OffCanvas cart without extra/manual work in JavaScript.
## Decision
* Add a new module `src/utility/bootstrap/bootstrap.util` in favor of `TooltipUtil` to consider more Bootstrap plugins in the future.
* Currently, it initializes `Tooltip` and `Popover` because those are the only Bootstrap plugins which have a documented manual initialization.
* We use the "selector" option in order to initialize Bootstrap plugins on selectors, which are added dynamically to the HTML. See: https://getbootstrap.com/docs/4.3/components/tooltips/#options
## Consequences
* In the main.js, `BootstrapUtil.initBootstrapPlugins()` is used instead of `new TooltipUtil()` to initialize Popovers as well.
* `TooltipUtil` is deprecated.
* Since we use event delegation ("selector" option) inside `BootstrapUtil` we don't need to manually re-initialize the Bootstrap plugins after dynamic content changes,
so it works automatically for all `[data-toogle="tooltip"]` and `[data-toogle="popover"]` selectors.
---
---
url: /docs/v6.5/resources/references/adr/2022-09-23-add-bootstrap-util.md
---
# Add bootstrap JS-plugin initialization utility to storefront JS
::: info
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2022-09-23-add-bootstrap-util.md)
:::
## Context
* Some Bootstrap JavaScript plugins have to be initialized manually to the desired DOM elements, see: https://getbootstrap.com/docs/4.3/components/tooltips/#example-enable-tooltips-everywhere
* This is not needed for all Bootstrap plugins. Modals for example work out of the box without extra initialization.
* Currently, we only initialize Tooltips using `src/utility/tooltip/tooltip.util.js`
* On dynamic content changes (listing pagination, ajax OffCanvas cart, etc.) Bootstrap plugins like Tooltip are no longer working.
* For example: It is not possible to show Tooltips in the OffCanvas cart without extra/manual work in JavaScript.
## Decision
* Add a new module `src/utility/bootstrap/bootstrap.util` in favor of `TooltipUtil` to consider more Bootstrap plugins in the future.
* Currently, it initializes `Tooltip` and `Popover` because those are the only Bootstrap plugins which have a documented manual initialization.
* We use the "selector" option in order to initialize Bootstrap plugins on selectors, which are added dynamically to the HTML. See: https://getbootstrap.com/docs/4.3/components/tooltips/#options
## Consequences
* In the main.js, `BootstrapUtil.initBootstrapPlugins()` is used instead of `new TooltipUtil()` to initialize Popovers as well.
* `TooltipUtil` is deprecated.
* Since we use event delegation ("selector" option) inside `BootstrapUtil` we don't need to manually re-initialize the Bootstrap plugins after dynamic content changes,
so it works automatically for all `[data-toogle="tooltip"]` and `[data-toogle="popover"]` selectors.
---
---
url: /docs/v6.6/resources/references/adr/2022-09-23-add-bootstrap-util.md
---
# Add bootstrap JS-plugin initialization utility to storefront JS
::: info
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository.
You can find the original version [here](https://github.com/shopware/shopware/blob/trunk/adr/2022-09-23-add-bootstrap-util.md)
:::
## Context
* Some Bootstrap JavaScript plugins have to be initialized manually to the desired DOM elements, see: https://getbootstrap.com/docs/4.3/components/tooltips/#example-enable-tooltips-everywhere
* This is not needed for all Bootstrap plugins. Modals for example work out of the box without extra initialization.
* Currently, we only initialize Tooltips using `src/utility/tooltip/tooltip.util.js`
* On dynamic content changes (listing pagination, ajax OffCanvas cart, etc.) Bootstrap plugins like Tooltip are no longer working.
* For example: It is not possible to show Tooltips in the OffCanvas cart without extra/manual work in JavaScript.
## Decision
* Add a new module `src/utility/bootstrap/bootstrap.util` in favor of `TooltipUtil` to consider more Bootstrap plugins in the future.
* Currently, it initializes `Tooltip` and `Popover` because those are the only Bootstrap plugins which have a documented manual initialization.
* We use the "selector" option in order to initialize Bootstrap plugins on selectors, which are added dynamically to the HTML. See: https://getbootstrap.com/docs/4.3/components/tooltips/#options
## Consequences
* In the main.js, `BootstrapUtil.initBootstrapPlugins()` is used instead of `new TooltipUtil()` to initialize Popovers as well.
* `TooltipUtil` is deprecated.
* Since we use event delegation ("selector" option) inside `BootstrapUtil` we don't need to manually re-initialize the Bootstrap plugins after dynamic content changes,
so it works automatically for all `[data-toogle="tooltip"]` and `[data-toogle="popover"]` selectors.
---
---
url: >-
/docs/v6.5/guides/plugins/plugins/framework/store-api/add-caching-for-store-api-route.md
---
# Add caching for Store API route
## Overview
In this guide you will learn how to add a cache layer to your custom Store API route. In this example, we will add a cache layer for the `ExampleRoute`, which is created in the [Add Store API route](./add-store-api-route) guide. For the cache invalidation we will write a invalidation subscriber.
## Prerequisites
In order to add a cache layer for the Store API route, you first need a Store API route as base. Therefore, you can refer to the [Add Store API route](./add-store-api-route) guide.
You also should have a look at our [Adding custom complex data](../data-handling/add-custom-complex-data) guide, since this guide is built upon it.
## Add cache layer
As you might have learned already from the [Add Store API route](./add-store-api-route) guide, we use abstract classes to make our routes more decoratable.
This concept is very advantageous if we now want to include a cache layer for the route. There are of course different ways to do this - but in this guide we show how we implemented it in the core.
### Add cached route class
First, we create an abstract class called `CachedExampleRoute` which extends the `AbstractExampleRoute`.
```php
// /src/Core/Content/Example/SalesChannel/CachedExampleRoute.php
['store-api']])]
class CachedExampleRoute extends AbstractExampleRoute
{
private AbstractExampleRoute $decorated;
private TagAwareAdapterInterface $cache;
private EntityCacheKeyGenerator $generator;
private AbstractCacheTracer $tracer;
private array $states;
private LoggerInterface $logger;
public function __construct(
AbstractExampleRoute $decorated,
TagAwareAdapterInterface $cache,
EntityCacheKeyGenerator $generator,
AbstractCacheTracer $tracer,
LoggerInterface $logger
) {
$this->decorated = $decorated;
$this->cache = $cache;
$this->generator = $generator;
$this->tracer = $tracer;
// declares that this route can not be cached if the customer is logged in
$this->states = [CacheStateSubscriber::STATE_LOGGED_IN];
$this->logger = $logger;
}
public function getDecorated(): AbstractExampleRoute
{
return $this->decorated;
}
#[Route(path: '/store-api/example', name: 'store-api.example.search', methods: ['GET','POST'], defaults: ['_entity' => 'swag_example'])]
public function load(Criteria $criteria, SalesChannelContext $context): ExampleRouteResponse
{
// The context is provided with a state where the route cannot be cached
if ($context->hasState(...$this->states)) {
return $this->getDecorated()->load($criteria, $context);
}
// Fetch item from the cache pool
$item = $this->cache->getItem(
$this->generateKey($criteria, $context)
);
try {
if ($item->isHit() && $item->get()) {
// Use cache compressor to uncompress the cache value
return CacheCompressor::uncompress($item);
}
} catch (\Throwable $e) {
// Something went wrong when uncompress the cache item - we log the error and continue to overwrite the invalid cache item
$this->logger->error($e->getMessage());
}
$name = self::buildName();
// start tracing of nested cache tags and system config keys
$response = $this->tracer->trace($name, function () use ($criteria, $context) {
return $this->getDecorated()->load($criteria, $context);
});
// compress cache content to reduce cache size
$item = CacheCompressor::compress($item, $response);
$item->tag(array_merge(
// get traced tags and configs
$this->tracer->get(self::buildName()),
[self::buildName()]
));
$this->cache->save($item);
return $response;
}
public static function buildName(): string
{
return 'example-route';
}
private function generateKey(SalesChannelContext $context, Criteria $criteria): string
{
$parts = [
self::buildName(),
// generate a hash for the route criteria
$this->generator->getCriteriaHash($criteria),
// generate a hash for the current context
$this->generator->getSalesChannelContextHash($context),
];
return md5(Json::encode($parts));
}
}
```
```xml
// /src/Resources/config/services.xml
```
In the new `CachedExampleRoute` some core classes are used which simplify the caching.
* `TagAwareAdapterInterface` - Used to read, write and tag cache items.
* `EntityCacheKeyGenerator` - Used to generate hashes for the context and/or criteria;
* `AbstractCacheTracer` - Traces all system config keys that were accessed. The data is needed later for cache invalidation.
* `CacheCompressor` - Provides an optimal compression of the cache entries to use as little disk space as possible.
### Add cache invalidation
Cache invalidation is much harder to implement than the actual caching. Finding the right balance between too much and too little invalidation is difficult. Therefore, there is no precise guidance or documentation on when to invalidate what. What and how to invalidate depends on what has been cached. For example, the product routes in the core are always invalidated when the product is written, but also when the product is ordered and reaches the out-of-stock status. The entire cache invalidation in Shopware is controlled via events. On the one hand there is the entity written event and on the other hand the corresponding business events like `ProductNoLongerAvailableEvent`.
```php
// /src/Core/Content/Example/SalesChannel/CacheInvalidationSubscriber.php
cacheInvalidator = $cacheInvalidator;
}
public static function getSubscribedEvents()
{
return [
// The EntityWrittenContainerEvent is a generic event that is always thrown when an entities are written. This contains all changed entities
EntityWrittenContainerEvent::class => [
['invalidate', 2001]
],
];
}
public function invalidate(EntityWrittenContainerEvent $event): void
{
// check if own entity written. In some cases you want to use the primary keys for further cache invalidation
$changes = $event->getPrimaryKeys(ExampleDefinition::ENTITY_NAME);
// no example entity changed? Then the cache does not need to be invalidated
if (empty($changes)) {
return;
}
$this->cacheInvalidator->invalidate([
CachedExampleRoute::buildName()
]);
}
}
```
```xml
// /src/Resources/config/services.xml
```
---
---
url: >-
/docs/v6.6/guides/plugins/plugins/framework/store-api/add-caching-for-store-api-route.md
---
# Add caching for Store API route
## Overview
In this guide you will learn how to add a cache layer to your custom Store API route. In this example, we will add a cache layer for the `ExampleRoute`, which is created in the [Add Store API route](./add-store-api-route) guide. For the cache invalidation we will write a invalidation subscriber.
## Prerequisites
In order to add a cache layer for the Store API route, you first need a Store API route as base. Therefore, you can refer to the [Add Store API route](./add-store-api-route) guide.
You also should have a look at our [Adding custom complex data](../data-handling/add-custom-complex-data) guide, since this guide is built upon it.
## Add cache layer
As you might have learned already from the [Add Store API route](./add-store-api-route) guide, we use abstract classes to make our routes more decoratable.
This concept is very advantageous if we now want to include a cache layer for the route. There are of course different ways to do this - but in this guide we show how we implemented it in the core.
### Add cached route class
First, we create an abstract class called `CachedExampleRoute` which extends the `AbstractExampleRoute`.
```php
// /src/Core/Content/Example/SalesChannel/CachedExampleRoute.php
['store-api']])]
class CachedExampleRoute extends AbstractExampleRoute
{
private AbstractExampleRoute $decorated;
private TagAwareAdapterInterface $cache;
private EntityCacheKeyGenerator $generator;
private AbstractCacheTracer $tracer;
private array $states;
private LoggerInterface $logger;
public function __construct(
AbstractExampleRoute $decorated,
TagAwareAdapterInterface $cache,
EntityCacheKeyGenerator $generator,
AbstractCacheTracer $tracer,
LoggerInterface $logger
) {
$this->decorated = $decorated;
$this->cache = $cache;
$this->generator = $generator;
$this->tracer = $tracer;
// declares that this route can not be cached if the customer is logged in
$this->states = [CacheStateSubscriber::STATE_LOGGED_IN];
$this->logger = $logger;
}
public function getDecorated(): AbstractExampleRoute
{
return $this->decorated;
}
#[Route(path: '/store-api/example', name: 'store-api.example.search', methods: ['GET','POST'], defaults: ['_entity' => 'swag_example'])]
public function load(Criteria $criteria, SalesChannelContext $context): ExampleRouteResponse
{
// The context is provided with a state where the route cannot be cached
if ($context->hasState(...$this->states)) {
return $this->getDecorated()->load($criteria, $context);
}
// Fetch item from the cache pool
$item = $this->cache->getItem(
$this->generateKey($context, $criteria)
);
try {
if ($item->isHit() && $item->get()) {
// Use cache compressor to uncompress the cache value
return CacheCompressor::uncompress($item);
}
} catch (\Throwable $e) {
// Something went wrong when uncompress the cache item - we log the error and continue to overwrite the invalid cache item
$this->logger->error($e->getMessage());
}
$name = self::buildName();
// start tracing of nested cache tags and system config keys
$response = $this->tracer->trace($name, function () use ($criteria, $context) {
return $this->getDecorated()->load($criteria, $context);
});
// compress cache content to reduce cache size
$item = CacheCompressor::compress($item, $response);
$item->tag(array_merge(
// get traced tags and configs
$this->tracer->get(self::buildName()),
[self::buildName()]
));
$this->cache->save($item);
return $response;
}
public static function buildName(): string
{
return 'example-route';
}
private function generateKey(SalesChannelContext $context, Criteria $criteria): string
{
$parts = [
self::buildName(),
// generate a hash for the route criteria
$this->generator->getCriteriaHash($criteria),
// generate a hash for the current context
$this->generator->getSalesChannelContextHash($context),
];
return md5(Json::encode($parts));
}
}
```
```xml
// /src/Resources/config/services.xml
```
In the new `CachedExampleRoute` some core classes are used which simplify the caching.
* `TagAwareAdapterInterface` - Used to read, write and tag cache items.
* `EntityCacheKeyGenerator` - Used to generate hashes for the context and/or criteria;
* `AbstractCacheTracer` - Traces all system config keys that were accessed. The data is needed later for cache invalidation.
* `CacheCompressor` - Provides an optimal compression of the cache entries to use as little disk space as possible.
### Add cache invalidation
Cache invalidation is much harder to implement than the actual caching. Finding the right balance between too much and too little invalidation is difficult. Therefore, there is no precise guidance or documentation on when to invalidate what. What and how to invalidate depends on what has been cached. For example, the product routes in the core are always invalidated when the product is written, but also when the product is ordered and reaches the out-of-stock status. The entire cache invalidation in Shopware is controlled via events. On the one hand there is the entity written event and on the other hand the corresponding business events like `ProductNoLongerAvailableEvent`.
```php
// /src/Core/Content/Example/SalesChannel/CacheInvalidationSubscriber.php
cacheInvalidator = $cacheInvalidator;
}
public static function getSubscribedEvents()
{
return [
// The EntityWrittenContainerEvent is a generic event that is always thrown when an entities are written. This contains all changed entities
EntityWrittenContainerEvent::class => [
['invalidate', 2001]
],
];
}
public function invalidate(EntityWrittenContainerEvent $event): void
{
// check if own entity written. In some cases you want to use the primary keys for further cache invalidation
$changes = $event->getPrimaryKeys(ExampleDefinition::ENTITY_NAME);
// no example entity changed? Then the cache does not need to be invalidated
if (empty($changes)) {
return;
}
$this->cacheInvalidator->invalidate([
CachedExampleRoute::buildName()
]);
}
}
```
```xml
// /src/Resources/config/services.xml
```
---
---
url: >-
/docs/guides/plugins/plugins/storefront/advanced/add-caching-to-custom-controller.md
---
# Add Caching to Custom Controller
In this guide you will learn how to define a controller route as cacheable for the HTTP cache.
## Prerequisites
In order to add a cache to an own controller route, you first need a plugin with a controller. Refer to the [Add custom controller guide](../controllers/add-custom-controller.md).
## Define the controller as cacheable
To define a controller route as cacheable, the default option of the route attribute `_httpCache` must be set to `true`. Once this option is set, the core takes care of everything else. If the route is called several times in the same state, a response is generated only for the first request and the second request gets the same response as the first one. It is also possible to exclude certain states from the cache. Shopware sets two different user states to which the HTTP cache reacts:
* state: `logged-in` - means that the user is logged in.
* state: `cart-filled` - means that there are products in the shopping cart.
```php
// /src/Storefront/Controller/ExampleController.php
[StorefrontRouteScope::ID]])]
class ExampleController extends StorefrontController
{
#[Route(path: '/example', name: 'frontend.example.example', methods: ['GET'], defaults: ['_httpCache' => true])]
public function showExample(): Response
{
return $this->renderStorefront('@SwagBasicExample/storefront/page/example/index.html.twig', [
'example' => 'Hello world'
]);
}
}
```
## Cache invalidation
As soon as a controller route has been defined as cacheable, and the corresponding response is written to the cache, it is tagged accordingly. For this purpose, the core uses all cache tags generated during the request or loaded from existing cache entries. The cache invalidation of the Storefront controller routes is controlled by the cache invalidation of the store API routes.
For more information about Store API cache invalidation, refer to the [Caching Guide](../../framework/caching/index.md).
This is because all data loaded in a controller route, is loaded in the core via the corresponding Store API routes and provided with corresponding cache tags. So the tags of the HTTP cache entries we have in the core consists of the sum of all store api tags generated or loaded during the request. Therefore the invalidation of a controller route that loads all data via the store API, no additional invalidation needs to be written.
---
---
url: >-
/docs/v6.5/guides/plugins/plugins/storefront/add-caching-to-custom-controller.md
---
# Add Caching to Custom Controller
## Overview
In this guide you will learn how to define a controller route as cacheable for the HTTP cache.
## Prerequisites
In order to add a cache to an own controller route, you first need a plugin with a controller. Therefore, you can refer to the [Add custom controller guide](./add-custom-controller).
## Define the controller as cacheable
To define a controller route as cacheable, it must be annotated with `@HttpCache()`. Once this annotation is set, the core takes care of everything else. If the route is called several times in the same state, a response is generated only for the first request and the second request gets the same response as the first one. It is also possible to exclude certain states from the cache. Shopware sets two different user states to which the HTTP cache reacts:
* state: `logged-in` - means that the user is logged in.
* state: `cart-filled` - means that there are products in the shopping cart.
If the controller route is not to be cached for one or both of these states, the annotation can be defined as follows: `@HttpCache(states={"cart-filled", "logged-in"})`
```php
// /src/Storefront/Controller/ExampleController.php
['storefront']])]
class ExampleController extends StorefrontController
{
#[Route(path: '/example', name: 'frontend.example.example', methods: ['GET'], defaults: ['_httpCache' => true])]
public function showExample(): Response
{
return $this->renderStorefront('@SwagBasicExample/storefront/page/example/index.html.twig', [
'example' => 'Hello world'
]);
}
}
```
## Cache invalidation
As soon as a controller route has been defined as cacheable, and the corresponding response is written to the cache, it is tagged accordingly. For this purpose, the core uses all cache tags generated during the request or loaded from existing cache entries. The cache invalidation of the Storefront controller routes is controlled by the cache invalidation of the store API routes.
For more information about Store API cache invalidation, you can refer to the [Add Cache for Store Api Route Guide](../framework/store-api/add-caching-for-store-api-route).
This is because all data loaded in a controller route, is loaded in the core via the corresponding Store API routes and provided with corresponding cache tags. So the tags of the HTTP cache entries we have in the core consists of the sum of all store api tags generated or loaded during the request. Therefore the invalidation of a controller route that loads all data via the store API, no additional invalidation needs to be written.
---
---
url: >-
/docs/v6.6/guides/plugins/plugins/storefront/add-caching-to-custom-controller.md
---
# Add Caching to Custom Controller
## Overview
In this guide you will learn how to define a controller route as cacheable for the HTTP cache.
## Prerequisites
In order to add a cache to an own controller route, you first need a plugin with a controller. Therefore, you can refer to the [Add custom controller guide](./add-custom-controller).
## Define the controller as cacheable
To define a controller route as cacheable, the default option of the route attribute `_httpCache` must be set to `true`. Once this option is set, the core takes care of everything else. If the route is called several times in the same state, a response is generated only for the first request and the second request gets the same response as the first one. It is also possible to exclude certain states from the cache. Shopware sets two different user states to which the HTTP cache reacts:
* state: `logged-in` - means that the user is logged in.
* state: `cart-filled` - means that there are products in the shopping cart.
```php
// /src/Storefront/Controller/ExampleController.php
['storefront']])]
class ExampleController extends StorefrontController
{
#[Route(path: '/example', name: 'frontend.example.example', methods: ['GET'], defaults: ['_httpCache' => true])]
public function showExample(): Response
{
return $this->renderStorefront('@SwagBasicExample/storefront/page/example/index.html.twig', [
'example' => 'Hello world'
]);
}
}
```
## Cache invalidation
As soon as a controller route has been defined as cacheable, and the corresponding response is written to the cache, it is tagged accordingly. For this purpose, the core uses all cache tags generated during the request or loaded from existing cache entries. The cache invalidation of the Storefront controller routes is controlled by the cache invalidation of the store API routes.
For more information about Store API cache invalidation, you can refer to the [Add Cache for Store Api Route Guide](../framework/store-api/add-caching-for-store-api-route).
This is because all data loaded in a controller route, is loaded in the core via the corresponding Store API routes and provided with corresponding cache tags. So the tags of the HTTP cache entries we have in the core consists of the sum of all store api tags generated or loaded during the request. Therefore the invalidation of a controller route that loads all data via the store API, no additional invalidation needs to be written.
---
---
url: /docs/guides/plugins/plugins/checkout/cart/add-cart-processor-collector.md
---
# Add Cart Collector/Processor
## Overview
In order to change the cart at runtime, you can use a custom [collector](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Cart/CartDataCollectorInterface.php)
or a custom [processor](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Cart/CartProcessorInterface.php).
Their main purpose is explained in their respective section.
## Collector class
A collector can and should be used to retrieve additional data for the cart, e.g. by querying the database, hence the name "collector".
This could also be querying an API endpoint, querying the database or fetching data in any other way you can think of.
Very often a collector is used to fetch data necessary for a processor.
It has to implement the interface `Shopware\Core\Checkout\Cart\CartDataCollectorInterface` and therefore implement a `collect` method.
But let's have a look at an example collector class.
```php
collectData();
$data->set('uniqueKey', $newData);
}
}
```
The `collect` method's parameters are the following:
* `CartDataCollection`: Use this object to save your new cart data. You'll most likely use the `set` method here, which expects
a unique key and its value. This object will be available in all processors.
* `Cart`: The current cart and its line items.
* `SalesChannelContext`: The current sales channel context, containing information about the currency, the country, etc.
* `CartBehavior`: It contains a cart state, which describes which actions are allowed. E.g. in the [product processor](https://github.com/shopware/shopware/blob/trunk/src/Core/Content/Product/Cart/ProductCartProcessor.php#L33), there's
a permission to check if the product stock validation should be skipped.
Your collector has to be defined in the service container using the tag `shopware.cart.collector`.
## Processor class
A processor is the class that will actually process the cart and is supposed to apply changes to the cart.
It will most likely use data, that was previously fetched by a collector.
::: warning
Do not query data in the process method, since it may be executed a lot of times. Always use the collect method of a collector for this case!
:::
Your processor class has to implement the interface `Shopware\Core\Checkout\Cart\CartProcessorInterface` and its `process` method.
Let's have a look at an example processor.
```php
get('uniqueKey');
// Do stuff to the `$toCalculate` cart with your new data
foreach ($toCalculate->getLineItems()->getFlat() as $lineItem) {
$lineItem->setPayload($newData['stuff']);
}
}
}
```
The `process` method contains the same parameters as the `collect` method, but there's one main difference:
Next to the `$original` `Cart`, you've got another `Cart` parameter being called `$toCalculate` here.
Make sure to do all the changes on the `$toCalculate` instance, since this is the cart that's going to be considered in the end.
Your processor has to be defined in the service container using the tag `shopware.cart.processor`.
## Next steps
If you want to see a better example on what can be done with a collector and a processor, you might want to have a look at our guide
regarding [Changing the price of an item in the cart](./change-price-of-item.md).
---
---
url: >-
/docs/v6.5/guides/plugins/plugins/checkout/cart/add-cart-processor-collector.md
---
# Add Cart Collector/Processor
## Overview
In order to change the cart at runtime, you can use a custom [collector](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Cart/CartDataCollectorInterface.php)
or a custom [processor](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Cart/CartProcessorInterface.php).
Their main purpose is explained in their respective section.
## Collector class
A collector can and should be used to retrieve additional data for the cart, e.g. by querying the database, hence the name "collector".
This could also be querying an API endpoint, querying the database or fetching data in any other way you can think of.
Very often a collector is used to fetch data necessary for a processor.
It has to implement the interface `Shopware\Core\Checkout\Cart\CartDataCollectorInterface` and therefore implement a `collect` method.
But let's have a look at an example collector class.
```php
collectData();
$data->set('uniqueKey', $newData);
}
}
```
The `collect` method's parameters are the following:
* `CartDataCollection`: Use this object to save your new cart data. You'll most likely use the `set` method here, which expects
a unique key and its value. This object will be available in all processors.
* `Cart`: The current cart and its line items.
* `SalesChannelContext`: The current sales channel context, containing information about the currency, the country, etc.
* `CartBehavior`: It contains a cart state, which describes which actions are allowed. E.g. in the [product processor](https://github.com/shopware/shopware/blob/trunk/src/Core/Content/Product/Cart/ProductCartProcessor.php#L33), there's
a permission to check if the product stock validation should be skipped.
Your collector has to be defined in the service container using the tag `shopware.cart.collector`.
## Processor class
A processor is the class that will actually process the cart and is supposed to apply changes to the cart.
It will most likely use data, that was previously fetched by a collector.
::: warning
Do not query data in the process method, since it may be executed a lot of times. Always use the collect method of a collector for this case!
:::
Your processor class has to implement the interface `Shopware\Core\Checkout\Cart\CartProcessorInterface` and its `process` method.
Let's have a look at an example processor.
```php
get('uniqueKey');
// Do stuff to the `$toCalculate` cart with your new data
foreach ($toCalculate->getLineItems()->getFlat() as $lineItem) {
$lineItem->setPayload($newData['stuff']);
}
}
}
```
The `process` method contains the same parameters as the `collect` method, but there's one main difference:
Next to the `$original` `Cart`, you've got another `Cart` parameter being called `$toCalculate` here.
Make sure to do all the changes on the `$toCalculate` instance, since this is the cart that's going to be considered in the end.
Your processor has to be defined in the service container using the tag `shopware.cart.processor`.
## Next steps
If you want to see a better example on what can be done with a collector and a processor, you might want to have a look at our guide
regarding [Changing the price of an item in the cart](./change-price-of-item).
---
---
url: >-
/docs/v6.6/guides/plugins/plugins/checkout/cart/add-cart-processor-collector.md
---
# Add Cart Collector/Processor
## Overview
In order to change the cart at runtime, you can use a custom [collector](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Cart/CartDataCollectorInterface.php)
or a custom [processor](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Cart/CartProcessorInterface.php).
Their main purpose is explained in their respective section.
## Collector class
A collector can and should be used to retrieve additional data for the cart, e.g. by querying the database, hence the name "collector".
This could also be querying an API endpoint, querying the database or fetching data in any other way you can think of.
Very often a collector is used to fetch data necessary for a processor.
It has to implement the interface `Shopware\Core\Checkout\Cart\CartDataCollectorInterface` and therefore implement a `collect` method.
But let's have a look at an example collector class.
```php
collectData();
$data->set('uniqueKey', $newData);
}
}
```
The `collect` method's parameters are the following:
* `CartDataCollection`: Use this object to save your new cart data. You'll most likely use the `set` method here, which expects
a unique key and its value. This object will be available in all processors.
* `Cart`: The current cart and its line items.
* `SalesChannelContext`: The current sales channel context, containing information about the currency, the country, etc.
* `CartBehavior`: It contains a cart state, which describes which actions are allowed. E.g. in the [product processor](https://github.com/shopware/shopware/blob/trunk/src/Core/Content/Product/Cart/ProductCartProcessor.php#L33), there's
a permission to check if the product stock validation should be skipped.
Your collector has to be defined in the service container using the tag `shopware.cart.collector`.
## Processor class
A processor is the class that will actually process the cart and is supposed to apply changes to the cart.
It will most likely use data, that was previously fetched by a collector.
::: warning
Do not query data in the process method, since it may be executed a lot of times. Always use the collect method of a collector for this case!
:::
Your processor class has to implement the interface `Shopware\Core\Checkout\Cart\CartProcessorInterface` and its `process` method.
Let's have a look at an example processor.
```php
get('uniqueKey');
// Do stuff to the `$toCalculate` cart with your new data
foreach ($toCalculate->getLineItems()->getFlat() as $lineItem) {
$lineItem->setPayload($newData['stuff']);
}
}
}
```
The `process` method contains the same parameters as the `collect` method, but there's one main difference:
Next to the `$original` `Cart`, you've got another `Cart` parameter being called `$toCalculate` here.
Make sure to do all the changes on the `$toCalculate` instance, since this is the cart that's going to be considered in the end.
Your processor has to be defined in the service container using the tag `shopware.cart.processor`.
## Next steps
If you want to see a better example on what can be done with a collector and a processor, you might want to have a look at our guide
regarding [Changing the price of an item in the cart](./change-price-of-item).
---
---
url: /docs/guides/plugins/plugins/checkout/cart/add-cart-discounts.md
---
# Add Cart Discounts
## Overview
This guide explains how to create discounts for your cart. In this example, we will create a discount for products that have 'Example' in their name.
## Prerequisites
Refer to the [Plugin Base Guide](../../plugin-base-guide) for information on creating plugins, and to the [Add custom service](../../services/add-custom-service.md) guide for details on service registration.
## Creating the processor
To add a discount to the cart, you should use the processor pattern. For this you need to create your own cart processor. We'll start with creating a new class called `ExampleProcessor` in the directory `/src/Core/Checkout`. Our class has to implement `Shopware\Core\Checkout\Cart\CartProcessorInterface` and we have to inject `Shopware\Core\Checkout\Cart\Price\PercentagePriceCalculator` in our constructor. All adjustments are done in the `process` method, where the product items already own a name and a price.
Let's start with the actual example code:
```php
// /src/Core/Checkout/ExampleProcessor.php
calculator = $calculator;
}
public function process(CartDataCollection $data, Cart $original, Cart $toCalculate, SalesChannelContext $context, CartBehavior $behavior): void
{
$products = $this->findExampleProducts($toCalculate);
// no example products found? early return
if ($products->count() === 0) {
return;
}
$discountLineItem = $this->createDiscount('EXAMPLE_DISCOUNT');
// declare price definition to define how this price is calculated
$definition = new PercentagePriceDefinition(
-10,
new LineItemRule(LineItemRule::OPERATOR_EQ, $products->getKeys())
);
$discountLineItem->setPriceDefinition($definition);
// calculate price
$discountLineItem->setPrice(
$this->calculator->calculate($definition->getPercentage(), $products->getPrices(), $context)
);
// add discount to new cart
$toCalculate->add($discountLineItem);
}
private function findExampleProducts(Cart $cart): LineItemCollection
{
return $cart->getLineItems()->filter(function (LineItem $item) {
// Only consider products, not custom line items or promotional line items
if ($item->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
return false;
}
$exampleInLabel = stripos($item->getLabel(), 'example') !== false;
if (!$exampleInLabel) {
return false;
}
return $item;
});
}
private function createDiscount(string $name): LineItem
{
$discountLineItem = new LineItem($name, 'example_discount', null, 1);
$discountLineItem->setLabel('Our example discount!');
$discountLineItem->setGood(false);
$discountLineItem->setStackable(false);
$discountLineItem->setRemovable(false);
return $discountLineItem;
}
}
```
As you can see, all line items of type product containing the string 'example' in their name are fetched. Also, a few information are saved into variables, since we'll need them several times. If no product in the cart matches your condition, we can early return in the `process` method. Afterwards we create a new line item for the new discount. For the latter, we don't want that the line item is stackable and it shouldn't be removable either.
So let's get to the important part, which is the price. For a percentage discount, we have to use the `PercentagePriceDefinition`. It consists of an actual value, the currency precision and, if necessary, some rules to apply to. This definition is required for the cart to tell the core how this price can be recalculated even if the plugin would be uninstalled.
Shopware comes with a called `LineItemRule`, which requires two parameters:
* The operator being used, e.g. `LineItemRule::OPERATOR_EQ` (Equals) or `LineItemRule::OPERATOR_NEQ` (Not equals)
* The identifiers to apply the rule to. Pass the line item identifiers here, in this case the identifiers of the previously filtered products
After adding the definition to the line item, we have to calculate the current price of the discount. Therefore we can use the `PercentagePriceCalculator` of the core. The last step is to add the discount to the new cart which is provided as `Cart $toCalculate`.
That's it for the main code of our custom `CartProcessor`. Now we only have to register it in our `services.php` using the tag `shopware.cart.processor` and priority `4500`, which is used to get access to the calculation after the [product processor](https://github.com/shopware/shopware/blob/v6.7.14.0/src/Core/Checkout/DependencyInjection/cart.php#L505-L516) handled the products.
---
---
url: /docs/v6.5/guides/plugins/plugins/checkout/cart/add-cart-discounts.md
---
# Add Cart Discounts
## Overview
In this guide you'll learn how to create discounts for your cart. In this example, we will create a discount for products that have 'Example' in their name.
## Prerequisites
In order to create cart discounts for your plugin, you first need a plugin as base. Therefore, you can refer to the [Plugin Base Guide](../../plugin-base-guide).
Furthermore you should be familiar with the service registration in Shopware, otherwise head over to our [Add custom service](../../plugin-fundamentals/add-custom-service) guide.
## Creating the processor
To add a discount to the cart, you should use the processor pattern. For this you need to create your own cart processor. We'll start with creating a new class called `ExampleProcessor` in the directory `/src/Core/Checkout`. Our class has to implement `Shopware\Core\Checkout\Cart\CartProcessorInterface` and we have to inject `Shopware\Core\Checkout\Cart\Price\PercentagePriceCalculator` in our constructor. All adjustments are done in the `process` method, where the product items already own a name and a price.
Let's start with the actual example code:
```php
// /src/Core/Checkout/ExampleProcessor.php
calculator = $calculator;
}
public function process(CartDataCollection $data, Cart $original, Cart $toCalculate, SalesChannelContext $context, CartBehavior $behavior): void
{
$products = $this->findExampleProducts($toCalculate);
// no example products found? early return
if ($products->count() === 0) {
return;
}
$discountLineItem = $this->createDiscount('EXAMPLE_DISCOUNT');
// declare price definition to define how this price is calculated
$definition = new PercentagePriceDefinition(
-10,
new LineItemRule(LineItemRule::OPERATOR_EQ, $products->getKeys())
);
$discountLineItem->setPriceDefinition($definition);
// calculate price
$discountLineItem->setPrice(
$this->calculator->calculate($definition->getPercentage(), $products->getPrices(), $context)
);
// add discount to new cart
$toCalculate->add($discountLineItem);
}
private function findExampleProducts(Cart $cart): LineItemCollection
{
return $cart->getLineItems()->filter(function (LineItem $item) {
// Only consider products, not custom line items or promotional line items
if ($item->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
return false;
}
$exampleInLabel = stripos($item->getLabel(), 'example') !== false;
if (!$exampleInLabel) {
return false;
}
return $item;
});
}
private function createDiscount(string $name): LineItem
{
$discountLineItem = new LineItem($name, 'example_discount', null, 1);
$discountLineItem->setLabel('Our example discount!');
$discountLineItem->setGood(false);
$discountLineItem->setStackable(false);
$discountLineItem->setRemovable(false);
return $discountLineItem;
}
}
```
As you can see, all line items of type product containing the string 'example' in their name are fetched. Also, a few information are saved into variables, since we'll need them several times. If no product in the cart matches your condition, we can early return in the `process` method. Afterwards we create a new line item for the new discount. For the latter, we don't want that the line item is stackable and it shouldn't be removable either.
So let's get to the important part, which is the price. For a percentage discount, we have to use the `PercentagePriceDefinition`. It consists of an actual value, the currency precision and, if necessary, some rules to apply to. This definition is required for the cart to tell the core how this price can be recalculated even if the plugin would be uninstalled.
Shopware comes with a called `LineItemRule`, which requires two parameters:
* The operator being used, e.g. `LineItemRule::OPERATOR_EQ` (Equals) or `LineItemRule::OPERATOR_NEQ` (Not equals)
* The identifiers to apply the rule to. Pass the line item identifiers here, in this case the identifiers of the previously filtered products
After adding the definition to the line item, we have to calculate the current price of the discount. Therefore we can use the `PercentagePriceCalculator` of the core. The last step is to add the discount to the new cart which is provided as `Cart $toCalculate`.
That's it for the main code of our custom `CartProcessor`. Now we only have to register it in our `services.xml` using the tag `shopware.cart.processor` and priority `4500`, which is used to get access to the calculation after the [product processor](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/DependencyInjection/cart.xml#L223-L231) handled the products.
---
---
url: /docs/v6.6/guides/plugins/plugins/checkout/cart/add-cart-discounts.md
---
# Add Cart Discounts
## Overview
In this guide you'll learn how to create discounts for your cart. In this example, we will create a discount for products that have 'Example' in their name.
## Prerequisites
In order to create cart discounts for your plugin, you first need a plugin as base. Therefore, you can refer to the [Plugin Base Guide](../../plugin-base-guide).
Furthermore you should be familiar with the service registration in Shopware, otherwise head over to our [Add custom service](../../plugin-fundamentals/add-custom-service) guide.
## Creating the processor
To add a discount to the cart, you should use the processor pattern. For this you need to create your own cart processor. We'll start with creating a new class called `ExampleProcessor` in the directory `/src/Core/Checkout`. Our class has to implement `Shopware\Core\Checkout\Cart\CartProcessorInterface` and we have to inject `Shopware\Core\Checkout\Cart\Price\PercentagePriceCalculator` in our constructor. All adjustments are done in the `process` method, where the product items already own a name and a price.
Let's start with the actual example code:
```php
// /src/Core/Checkout/ExampleProcessor.php
calculator = $calculator;
}
public function process(CartDataCollection $data, Cart $original, Cart $toCalculate, SalesChannelContext $context, CartBehavior $behavior): void
{
$products = $this->findExampleProducts($toCalculate);
// no example products found? early return
if ($products->count() === 0) {
return;
}
$discountLineItem = $this->createDiscount('EXAMPLE_DISCOUNT');
// declare price definition to define how this price is calculated
$definition = new PercentagePriceDefinition(
-10,
new LineItemRule(LineItemRule::OPERATOR_EQ, $products->getKeys())
);
$discountLineItem->setPriceDefinition($definition);
// calculate price
$discountLineItem->setPrice(
$this->calculator->calculate($definition->getPercentage(), $products->getPrices(), $context)
);
// add discount to new cart
$toCalculate->add($discountLineItem);
}
private function findExampleProducts(Cart $cart): LineItemCollection
{
return $cart->getLineItems()->filter(function (LineItem $item) {
// Only consider products, not custom line items or promotional line items
if ($item->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
return false;
}
$exampleInLabel = stripos($item->getLabel(), 'example') !== false;
if (!$exampleInLabel) {
return false;
}
return $item;
});
}
private function createDiscount(string $name): LineItem
{
$discountLineItem = new LineItem($name, 'example_discount', null, 1);
$discountLineItem->setLabel('Our example discount!');
$discountLineItem->setGood(false);
$discountLineItem->setStackable(false);
$discountLineItem->setRemovable(false);
return $discountLineItem;
}
}
```
As you can see, all line items of type product containing the string 'example' in their name are fetched. Also, a few information are saved into variables, since we'll need them several times. If no product in the cart matches your condition, we can early return in the `process` method. Afterwards we create a new line item for the new discount. For the latter, we don't want that the line item is stackable and it shouldn't be removable either.
So let's get to the important part, which is the price. For a percentage discount, we have to use the `PercentagePriceDefinition`. It consists of an actual value, the currency precision and, if necessary, some rules to apply to. This definition is required for the cart to tell the core how this price can be recalculated even if the plugin would be uninstalled.
Shopware comes with a called `LineItemRule`, which requires two parameters:
* The operator being used, e.g. `LineItemRule::OPERATOR_EQ` (Equals) or `LineItemRule::OPERATOR_NEQ` (Not equals)
* The identifiers to apply the rule to. Pass the line item identifiers here, in this case the identifiers of the previously filtered products
After adding the definition to the line item, we have to calculate the current price of the discount. Therefore we can use the `PercentagePriceCalculator` of the core. The last step is to add the discount to the new cart which is provided as `Cart $toCalculate`.
That's it for the main code of our custom `CartProcessor`. Now we only have to register it in our `services.xml` using the tag `shopware.cart.processor` and priority `4500`, which is used to get access to the calculation after the [product processor](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/DependencyInjection/cart.xml#L223-L231) handled the products.
---
---
url: /docs/guides/plugins/plugins/checkout/cart/add-cart-items.md
---
# Add Cart Items
## Overview
This guide will show you how to create line items like products, promotion and other types and add them to the cart. It will also cover creating a custom LineItemHandler.
## Prerequisites
Review the [Plugin base guide](../../plugin-base-guide) for general information about plugins. The guides on [Adding a custom page](../../storefront/controllers/add-custom-page.md), which explains how to add a custom Storefront controller, and [Dependency injection](../../services/dependency-injection.md), which addresses registering classes or services to the DI container, are also recommended.
## Adding a simple item
For this guide, we will use an example controller, that is already registered. The process of creating such a controller is not explained here, for that case head over to our guide about [Adding a custom page](../../storefront/controllers/add-custom-page.md).
However, having a controller is not a necessity here, it just comes with the advantage of fetching the current cart by adding `\Shopware\Core\Checkout\Cart\Cart` as a method argument, which will automatically be filled by our argument resolver.
If you're planning to use this guide for something else but a controller, you can fetch the current cart with the `\Shopware\Core\Checkout\Cart\SalesChannel\CartService::getCart` method.
So let's add an example product to the cart using code. For that case, you'll need to have access to both the services `\Shopware\Core\Checkout\Cart\LineItemFactoryRegistry` and `\Shopware\Core\Checkout\Cart\SalesChannel\CartService` supplied to your controller or service via [Dependency injection](../../services/dependency-injection.md).
Let's have a look at an example.
```php
// /src/Service/ExampleController.php
[StorefrontRouteScope::ID]])]
class ExampleController extends StorefrontController
{
private LineItemFactoryRegistry $factory;
private CartService $cartService;
public function __construct(LineItemFactoryRegistry $factory, CartService $cartService)
{
$this->factory = $factory;
$this->cartService = $cartService;
}
#[Route(path: '/cartAdd', name: 'frontend.example', methods: ['GET'])]
public function add(Cart $cart, SalesChannelContext $context): StorefrontResponse
{
// Create product line item
$lineItem = $this->factory->create([
'type' => LineItem::PRODUCT_LINE_ITEM_TYPE, // Results in 'product'
'referencedId' => 'myExampleId', // this is not a valid UUID, change this to your actual ID!
'quantity' => 5,
'payload' => ['key' => 'value']
], $context);
$this->cartService->add($cart, $lineItem, $context);
return $this->renderStorefront('@Storefront/storefront/base.html.twig');
}
}
```
As mentioned earlier, you can just apply the `Cart` argument to your method and it will be automatically filled.
Afterwards you create a line item using the `LineItemFactoryRegistry` and its `create` method. It is mandatory to supply the `type` property, which can be one of the following by default:
* product
* promotion
* credit
* custom
The `LineItemFactoryRegistry` holds a collection of handlers to create a line item of a specific type. Each line item type needs an own handler, which is covered later in this guide. If the type is not supported, it will throw a `\Shopware\Core\Checkout\Cart\Exception\LineItemTypeNotSupportedException` exception.
Other than that, we apply the `referencedId`, which in this case points to the product ID that we want to add. If you were to add a line item of type `promotion`, the `referencedId` would have to point to the respective promotion ID. The `quantity` field just contains the quantity of line items which you want to add to the cart.
Now have a look at the `payload` field, which only contains dummy data in this example. The `payload` field can contain any additional data that you need to attach to a line item in order to properly handle your business logic. E.g. the information about the chosen options of a configurable product are saved in there. Feel free to use this one to apply important information to your line item, that you might have to process later on, e.g. in the template.
You can find a list of all available fields in the [createValidatorDefinition method of the LineItemFactoryRegistry](https://github.com/shopware/shopware/blob/v6.3.5.0/src/Core/Checkout/Cart/LineItemFactoryRegistry.php#L113-L142).
If you now call the route `/cartAdd`, it should add the product with the ID `myExampleId` to the cart, 5 times.
## Create new factory handler
Sometimes you really want to have a custom line item handler, e.g. for your own new entity, such as a bundle entity or alike. For that case, you can create your own line item handler, which will then be available in the `LineItemFactoryRegistry` as a valid `type` option.
You need to create a new class which implements the interface `\Shopware\Core\Checkout\Cart\LineItemFactoryHandler\LineItemFactoryInterface` and it needs to be registered in the DI container with the tag `shopware.cart.line_item.factory`.
```php
// /src/Resources/config/services.php
services();
$services->set(ExampleHandler::class)
->tag('shopware.cart.line_item.factory');
};
```
Let's first have a look at an example handler:
```php
// /src/Service/ExampleHandler.php
setReferencedId($data['referencedId']);
}
}
}
```
Implementing the `LineItemFactoryInterface` will force you to also implement three new methods:
* `supports`: A method that is applied a string `$type`. This method has to return a bool whether or not it supports this type.
In this example, this handler supports the line item type `example`.
* `create`: This method is responsible for actually creating an instance of a `LineItem`. Apply everything necessary for your custom line item type
here, such as fields, that always have to be set for your case. It is called when the method `create` of the `LineItemFactoryRegistry` is called,
just like in the example earlier in this guide.
* `update`: This method is called the method `update` of the `LineItemFactoryRegistry` is called. Just as the name suggests, your line item will be updated.
Here you can define which properties of your line item may actually be updated. E.g. if you really want property X to contain "Y", you can do so here.
Now you'll need to add a processor for your type. Otherwise your item won't be persisted in the cart. A simple processor for our ExampleHandler could look like this:
```php
// /Core/Checkout/Cart/ExampleProcessor.php
getLineItems()->filterFlatByType(ExampleHandler::TYPE);
foreach ($lineItems as $lineItem){
$toCalculate->add($lineItem);
}
}
}
```
As you can see, this processor takes an "original cart" as an input and adds all instances of our example type to a second cart, which will actually be persisted.
Of course you can use processors to do much more than this. Have a look at [adding cart processors and collectors](./add-cart-processor-collector).
Now register this processor in your `services.php` like this:
```php
// /src/Resources/config/services.php
services();
$services->set(ExampleProcessor::class)
->tag('shopware.cart.processor', ['priority' => 4800]);
};
```
And that's it. You should now be able to create line items of type `example`.
## Adding nested line item
When implementing nested line items, the plugins have to implement their own processing logic or alternatively extend Shopware's cart processors.
A plugin that reuses core line items can easily call the other processors to handle the nested line items themselves. Refer to [nested line items](../../../../../resources/references/adr/2021-03-24-nested-line-items.md) section of the guide for more information.
---
---
url: /docs/v6.5/guides/plugins/plugins/checkout/cart/add-cart-items.md
---
# Add Cart Items
## Overview
This guide will show you how to create line items like products, promotion and other types and add them to the cart. It will also cover creating a custom LineItemHandler.
## Prerequisites
As most guides, this guide is also built upon the [Plugin base guide](../../plugin-base-guide), but you don't necessarily need that. It will use an example Storefront controller, so if you don't know how to add a custom Storefront controller yet, have a look at our guide about [Adding a custom page](../../storefront/add-custom-page). Furthermore, registering classes or services to the DI container is also not explained here, but it's covered in our guide about [Dependency injection](../../plugin-fundamentals/dependency-injection), so having this open in another tab won't hurt.
## Adding a simple item
For this guide, we will use an example controller, that is already registered. The process of creating such a controller is not explained here, for that case head over to our guide about [Adding a custom page](../../storefront/add-custom-page).
However, having a controller is not a necessity here, it just comes with the advantage of fetching the current cart by adding `\Shopware\Core\Checkout\Cart\Cart` as a method argument, which will automatically be filled by our argument resolver.
If you're planning to use this guide for something else but a controller, you can fetch the current cart with the `\Shopware\Core\Checkout\Cart\SalesChannel\CartService::getCart` method.
So let's add an example product to the cart using code. For that case, you'll need to have access to both the services `\Shopware\Core\Checkout\Cart\LineItemFactoryRegistry` and `\Shopware\Core\Checkout\Cart\SalesChannel\CartService` supplied to your controller or service via [Dependency injection](../../plugin-fundamentals/dependency-injection).
Let's have a look at an example.
```php
// /src/Service/ExampleController.php
['storefront']])]
class ExampleController extends StorefrontController
{
private LineItemFactoryRegistry $factory;
private CartService $cartService;
public function __construct(LineItemFactoryRegistry $factory, CartService $cartService)
{
$this->factory = $factory;
$this->cartService = $cartService;
}
#[Route(path: '/cartAdd', name: 'frontend.example', methods: ['GET'])]
public function add(Cart $cart, SalesChannelContext $context): StorefrontResponse
{
// Create product line item
$lineItem = $this->factory->create([
'type' => LineItem::PRODUCT_LINE_ITEM_TYPE, // Results in 'product'
'referencedId' => 'myExampleId', // this is not a valid UUID, change this to your actual ID!
'quantity' => 5,
'payload' => ['key' => 'value']
], $context);
$this->cartService->add($cart, $lineItem, $context);
return $this->renderStorefront('@Storefront/storefront/base.html.twig');
}
}
```
As mentioned earlier, you can just apply the `Cart` argument to your method and it will be automatically filled.
Afterwards you create a line item using the `LineItemFactoryRegistry` and its `create` method. It is mandatory to supply the `type` property, which can be one of the following by default:
* product
* promotion
* credit
* custom
The `LineItemFactoryRegistry` holds a collection of handlers to create a line item of a specific type. Each line item type needs an own handler, which is covered later in this guide. If the type is not supported, it will throw a `\Shopware\Core\Checkout\Cart\Exception\LineItemTypeNotSupportedException` exception.
Other than that, we apply the `referencedId`, which in this case points to the product ID that we want to add. If you were to add a line item of type `promotion`, the `referencedId` would have to point to the respective promotion ID. The `quantity` field just contains the quantity of line items which you want to add to the cart.
Now have a look at the `payload` field, which only contains dummy data in this example. The `payload` field can contain any additional data that you need to attach to a line item in order to properly handle your business logic. E.g. the information about the chosen options of a configurable product are saved in there. Feel free to use this one to apply important information to your line item, that you might have to process later on, e.g. in the template.
You can find a list of all available fields in the [createValidatorDefinition method of the LineItemFactoryRegistry](https://github.com/shopware/shopware/blob/v6.3.5.0/src/Core/Checkout/Cart/LineItemFactoryRegistry.php#L113-L142).
If you now call the route `/cartAdd`, it should add the product with the ID `myExampleId` to the cart, 5 times.
## Create new factory handler
Sometimes you really want to have a custom line item handler, e.g. for your own new entity, such as a bundle entity or alike. For that case, you can create your own line item handler, which will then be available in the `LineItemFactoryRegistry` as a valid `type` option.
You need to create a new class which implements the interface `\Shopware\Core\Checkout\Cart\LineItemFactoryHandler\LineItemFactoryInterface` and it needs to be registered in the DI container with the tag `shopware.cart.line_item.factory`.
```xml
// /src/Resources/config/services.xml
```
Let's first have a look at an example handler:
```php
// /src/Service/ExampleHandler.php
setReferencedId($data['referencedId']);
}
}
}
```
Implementing the `LineItemFactoryInterface` will force you to also implement three new methods:
* `supports`: A method that is applied a string `$type`. This method has to return a bool whether or not it supports this type.
In this example, this handler supports the line item type `example`.
* `create`: This method is responsible for actually creating an instance of a `LineItem`. Apply everything necessary for your custom line item type
here, such as fields, that always have to be set for your case. It is called when the method `create` of the `LineItemFactoryRegistry` is called,
just like in the example earlier in this guide.
* `update`: This method is called the method `update` of the `LineItemFactoryRegistry` is called. Just as the name suggests, your line item will be updated.
Here you can define which properties of your line item may actually be updated. E.g. if you really want property X to contain "Y", you can do so here.
Now you'll need to add a processor for your type. Otherwise your item won't be persisted in the cart. A simple processor for our ExampleHandler could look like this:
```php
// /Core/Checkout/Cart/ExampleProcessor.php
getLineItems()->filterFlatByType(ExampleHandler::TYPE);
foreach ($lineItems as $lineItem){
$toCalculate->add($lineItem);
}
}
}
```
As you can see, this processor takes an "original cart" as an input and adds all instances of our example type to a second cart, which will actually be persisted.
Of course you can use processors to do much more than this. Have a look at [adding cart processors and collectors](./add-cart-processor-collector).
Now register this processor in your `services.xml` like this:
```html
// /Resources/config/services.xml
...
...
```
And that's it. You should now be able to create line items of type `example`.
## Adding nested line item
When implementing nested line items, the plugins have to implement their own processing logic or alternatively extend Shopware's cart processors.
A plugin that reuses core line items can easily call the other processors to handle the nested line items themselves. Refer to [nested line items](../../../../../resources/references/adr/2021-03-24-nested-line-items.md) section of the guide for more information.
---
---
url: /docs/v6.6/guides/plugins/plugins/checkout/cart/add-cart-items.md
---
# Add Cart Items
## Overview
This guide will show you how to create line items like products, promotion and other types and add them to the cart. It will also cover creating a custom LineItemHandler.
## Prerequisites
As most guides, this guide is also built upon the [Plugin base guide](../../plugin-base-guide), but you don't necessarily need that. It will use an example Storefront controller, so if you don't know how to add a custom Storefront controller yet, have a look at our guide about [Adding a custom page](../../storefront/add-custom-page). Furthermore, registering classes or services to the DI container is also not explained here, but it's covered in our guide about [Dependency injection](../../plugin-fundamentals/dependency-injection), so having this open in another tab won't hurt.
## Adding a simple item
For this guide, we will use an example controller, that is already registered. The process of creating such a controller is not explained here, for that case head over to our guide about [Adding a custom page](../../storefront/add-custom-page).
However, having a controller is not a necessity here, it just comes with the advantage of fetching the current cart by adding `\Shopware\Core\Checkout\Cart\Cart` as a method argument, which will automatically be filled by our argument resolver.
If you're planning to use this guide for something else but a controller, you can fetch the current cart with the `\Shopware\Core\Checkout\Cart\SalesChannel\CartService::getCart` method.
So let's add an example product to the cart using code. For that case, you'll need to have access to both the services `\Shopware\Core\Checkout\Cart\LineItemFactoryRegistry` and `\Shopware\Core\Checkout\Cart\SalesChannel\CartService` supplied to your controller or service via [Dependency injection](../../plugin-fundamentals/dependency-injection).
Let's have a look at an example.
```php
// /src/Service/ExampleController.php
['storefront']])]
class ExampleController extends StorefrontController
{
private LineItemFactoryRegistry $factory;
private CartService $cartService;
public function __construct(LineItemFactoryRegistry $factory, CartService $cartService)
{
$this->factory = $factory;
$this->cartService = $cartService;
}
#[Route(path: '/cartAdd', name: 'frontend.example', methods: ['GET'])]
public function add(Cart $cart, SalesChannelContext $context): StorefrontResponse
{
// Create product line item
$lineItem = $this->factory->create([
'type' => LineItem::PRODUCT_LINE_ITEM_TYPE, // Results in 'product'
'referencedId' => 'myExampleId', // this is not a valid UUID, change this to your actual ID!
'quantity' => 5,
'payload' => ['key' => 'value']
], $context);
$this->cartService->add($cart, $lineItem, $context);
return $this->renderStorefront('@Storefront/storefront/base.html.twig');
}
}
```
As mentioned earlier, you can just apply the `Cart` argument to your method and it will be automatically filled.
Afterwards you create a line item using the `LineItemFactoryRegistry` and its `create` method. It is mandatory to supply the `type` property, which can be one of the following by default:
* product
* promotion
* credit
* custom
The `LineItemFactoryRegistry` holds a collection of handlers to create a line item of a specific type. Each line item type needs an own handler, which is covered later in this guide. If the type is not supported, it will throw a `\Shopware\Core\Checkout\Cart\Exception\LineItemTypeNotSupportedException` exception.
Other than that, we apply the `referencedId`, which in this case points to the product ID that we want to add. If you were to add a line item of type `promotion`, the `referencedId` would have to point to the respective promotion ID. The `quantity` field just contains the quantity of line items which you want to add to the cart.
Now have a look at the `payload` field, which only contains dummy data in this example. The `payload` field can contain any additional data that you need to attach to a line item in order to properly handle your business logic. E.g. the information about the chosen options of a configurable product are saved in there. Feel free to use this one to apply important information to your line item, that you might have to process later on, e.g. in the template.
You can find a list of all available fields in the [createValidatorDefinition method of the LineItemFactoryRegistry](https://github.com/shopware/shopware/blob/v6.3.5.0/src/Core/Checkout/Cart/LineItemFactoryRegistry.php#L113-L142).
If you now call the route `/cartAdd`, it should add the product with the ID `myExampleId` to the cart, 5 times.
## Create new factory handler
Sometimes you really want to have a custom line item handler, e.g. for your own new entity, such as a bundle entity or alike. For that case, you can create your own line item handler, which will then be available in the `LineItemFactoryRegistry` as a valid `type` option.
You need to create a new class which implements the interface `\Shopware\Core\Checkout\Cart\LineItemFactoryHandler\LineItemFactoryInterface` and it needs to be registered in the DI container with the tag `shopware.cart.line_item.factory`.
```xml
// /src/Resources/config/services.xml
```
Let's first have a look at an example handler:
```php
// /src/Service/ExampleHandler.php
setReferencedId($data['referencedId']);
}
}
}
```
Implementing the `LineItemFactoryInterface` will force you to also implement three new methods:
* `supports`: A method that is applied a string `$type`. This method has to return a bool whether or not it supports this type.
In this example, this handler supports the line item type `example`.
* `create`: This method is responsible for actually creating an instance of a `LineItem`. Apply everything necessary for your custom line item type
here, such as fields, that always have to be set for your case. It is called when the method `create` of the `LineItemFactoryRegistry` is called,
just like in the example earlier in this guide.
* `update`: This method is called the method `update` of the `LineItemFactoryRegistry` is called. Just as the name suggests, your line item will be updated.
Here you can define which properties of your line item may actually be updated. E.g. if you really want property X to contain "Y", you can do so here.
Now you'll need to add a processor for your type. Otherwise your item won't be persisted in the cart. A simple processor for our ExampleHandler could look like this:
```php
// /Core/Checkout/Cart/ExampleProcessor.php
getLineItems()->filterFlatByType(ExampleHandler::TYPE);
foreach ($lineItems as $lineItem){
$toCalculate->add($lineItem);
}
}
}
```
As you can see, this processor takes an "original cart" as an input and adds all instances of our example type to a second cart, which will actually be persisted.
Of course you can use processors to do much more than this. Have a look at [adding cart processors and collectors](./add-cart-processor-collector).
Now register this processor in your `services.xml` like this:
```html
// /Resources/config/services.xml
...
...
```
And that's it. You should now be able to create line items of type `example`.
## Adding nested line item
When implementing nested line items, the plugins have to implement their own processing logic or alternatively extend Shopware's cart processors.
A plugin that reuses core line items can easily call the other processors to handle the nested line items themselves. Refer to [nested line items](../../../../../resources/references/adr/2021-03-24-nested-line-items.md) section of the guide for more information.
---
---
url: /docs/guides/plugins/plugins/checkout/cart/add-cart-validator.md
---
# Add Cart Validator
## Overview
The cart in Shopware is continuously validated by so-called "validators" that check for invalid line items (label missing), shipping addresses, and other attributes.
This guide explains how to add your own custom cart validator.
## Prerequisites
Review the [plugin base guide](../../plugin-base-guide) to create a plugin. Familiarity with the [Dependency Injection container](../../services/dependency-injection.md), which you will use to register your custom validator, is also necessary.
## Adding a custom cart validator
We'll create several things throughout this guide, in that order:
* The validator itself
* A new exception being thrown by the validator if needed
* Snippets to print a proper error message
### The validator
The validator being created in this example is assuming you've got custom payload data in your line items to validate against. This is just an example and will always result in an error, since the data requested doesn't exist by default, until you add them.
A validator should be placed in the proper domain. That means, that an Address validator should be in a directory `/src/Core/Checkout/Cart/Address`. Since the validator in the following example will be called `CustomCartValidator`, its directory will be `/src/Core/Checkout/Cart/Custom`.
Your validator has to implement the interface `Shopware\Core\Checkout\Cart\CartValidatorInterface`. This forces you to also implement a `validate` method.
But let's have a look at the example validator first:
```php
// /src/Core/Checkout/Cart/Custom/CustomCartValidator.php
getLineItems()->getFlat() as $lineItem) {
if (!array_key_exists('customPayload', $lineItem->getPayload()) || $lineItem->getPayload()['customPayload'] !== 'example') {
$errorCollection->add(new CustomCartBlockedError($lineItem->getId()));
return;
}
}
}
}
```
As already said, a cart validator has to implement the `CartValidatorInterface` and therefore implement a `validate` method. This method has access to some important parts of the checkout, such as the cart and the current sales channel context. Also you have access to the error collection, which may or may not contain errors from other earlier validators.
In this example we're dealing with the line items and are validating them, so we're iterating over each line item. This example assumes that your line items got a custom payload, called `customPayload`, and it expects a value in there.
If the condition doesn't match and the line item seems to be invalid, you'll have to add a new error to the error collection. You can't just use any exception here, but a class which has to extend from `Shopware\Core\Checkout\Cart\Error\Error`. Most likely you want to create your own error class here, which will be done in the next step.
Important to note is the `return` statement afterwards. If you wouldn't return here, it would add an error to the error collection for each invalid line item, resulting in several errors displayed on the checkout or the cart page. E.g. if you had four invalid items in your cart, four separate errors would be shown. This way, only one message is shown, so it depends on what you're validating and what you want to happen.
#### Registering the validator
One more thing to do is to register your new validator to the [dependency injection container](../../services/dependency-injection.md).
Your validator has to be registered using the tag `shopware.cart.validator`:
```php
// /src/Resources/config/services.php
services();
$services->set(CustomCartValidator::class)
->tag('shopware.cart.validator');
};
```
### Adding the custom cart error
The custom cart error class will be called `CustomCartBlockedError` and should be located in a `Error` directory in the same domain as the validator. Since the validator was located in the directory `/src/Core/Checkout/Cart/Custom`, the error class will be located in the directory `/src/Core/Checkout/Cart/Custom/Error`.
It has to extend from the abstract class `Shopware\Core\Checkout\Cart\Error\Error`, which asks you to implement a few methods:
* `getId`: Here you have to return a unique ID, since your error will be saved via this ID in the error collection. In this example,
we'll just use the line item ID here.
* `getMessageKey`: The snippet key of the message to be displayed. In this example it will be `custom-line-item-blocked`, which is important
for the next section of this guide, for adding the snippets.
* `getLevel`: The kind of error, available are `notice`, `warning` and `error`. Depending on that decision, the error will be printed in a blue,
yellow or red box respectively. This example will use the error here.
* `blockOrder`: Return a boolean on whether this exception should block the possibility to actually finish the checkout.
In this case it will be `true`, hence the error level defined earlier. It wouldn't make sense to block the checkout, but only display a notice.
* `blockResubmit`: Optional, return a boolean on whether this exception block the user from trying to finish the checkout again.
If you want to use it, add the method `blockResubmit(): bool` to your custom error. If you don't, it is `true` by default.
* `getParameters`: You can add custom payload here. Technically any plugin or code could read the errors of the cart and act accordingly.
If you need extra payload to your error class, this is the place to go.
So now let's have a look at the example error class:
```php
// /src/Core/Checkout/Cart/Custom/Error/CustomCartBlockedError.php
lineItemId = $lineItemId;
parent::__construct();
}
public function getId(): string
{
return $this->lineItemId;
}
public function getMessageKey(): string
{
return self::KEY;
}
public function getLevel(): int
{
// return self::LEVEL_NOTICE;
// return self::LEVEL_WARNING;
return self::LEVEL_ERROR;
}
public function blockOrder(): bool
{
return true;
}
public function getParameters(): array
{
return [ 'lineItemId' => $this->lineItemId ];
}
}
```
The constructor was overridden so we can ask for the line item ID and save it in a property. Since we already used this class in the validator, we're basically done with that part here.
Only the snippets are missing.
### Adding the snippet
Review the guide on [adding storefront snippets](../../storefront/styling/add-translations.md).
You've defined the error key to be `custom-line-item-blocked` in your custom error class `CustomCartBlockedError`. Once your validator finds an invalid line item in your cart, Shopware is going to search for a respective snippet. In the cart, Shopware will be looking for the following snippet key: `checkout.custom-line-item-blocked`. Meanwhile it will be looking for a key `error.custom-line-item-blocked` in the checkout steps. This way you could technically define two different messages for the cart and the following checkout steps.
Now let's have a look at an example snippet file:
```javascript
// /src/Resources/snippet/en\_GB/example.en-GB.json
{
"checkout": {
"custom-line-item-blocked": "Example error message for the cart"
},
"error": {
"custom-line-item-blocked": "Example error message for the checkout"
}
}
```
This way Shopware will find the new snippets in your plugin and display the respective error message.
And that's it, you've now successfully added your own cart validator.
## Next steps
Review the guide on [adding cart items](add-cart-items) for information on custom line-item payloads.
---
---
url: /docs/v6.5/guides/plugins/plugins/checkout/cart/add-cart-validator.md
---
# Add Cart Validator
## Overview
The cart in Shopware is constantly being validated by so called "validators". This way we can check for an invalid cart, e.g. for invalid line items (label missing) or an invalid shipping address.
This guide will cover the subject on how to add your own custom cart validator.
## Prerequisites
For this guide, you will need a working plugin, which you learn to create [here](../../plugin-base-guide). Also, you will have to know the [Dependency Injection container](../../plugin-fundamentals/dependency-injection), since that's going to be used in order to register your custom validator.
## Adding a custom cart validator
We'll create several things throughout this guide, in that order:
* The validator itself
* A new exception being thrown by the validator if needed
* Snippets to print a proper error message
### The validator
The validator being created in this example is assuming you've got custom payload data in your line items to validate against. This is just an example and will always result in an error, since the data requested doesn't exist by default, until you add them.
A validator should be placed in the proper domain. That means, that an Address validator should be in a directory `/src/Core/Checkout/Cart/Address`. Since the validator in the following example will be called `CustomCartValidator`, its directory will be `/src/Core/Checkout/Cart/Custom`.
Your validator has to implement the interface `Shopware\Core\Checkout\Cart\CartValidatorInterface`. This forces you to also implement a `validate` method.
But let's have a look at the example validator first:
```php
// /src/Core/Checkout/Cart/Custom/CustomCartValidator.php
getLineItems()->getFlat() as $lineItem) {
if (!array_key_exists('customPayload', $lineItem->getPayload()) || $lineItem->getPayload()['customPayload'] !== 'example') {
$errorCollection->add(new CustomCartBlockedError($lineItem->getId()));
return;
}
}
}
}
```
As already said, a cart validator has to implement the `CartValidatorInterface` and therefore implement a `validate` method. This method has access to some important parts of the checkout, such as the cart and the current sales channel context. Also you have access to the error collection, which may or may not contain errors from other earlier validators.
In this example we're dealing with the line items and are validating them, so we're iterating over each line item. This example assumes that your line items got a custom payload, called `customPayload`, and it expects a value in there.
If the condition doesn't match and the line item seems to be invalid, you'll have to add a new error to the error collection. You can't just use any exception here, but a class which has to extend from `Shopware\Core\Checkout\Cart\Error\Error`. Most likely you want to create your own error class here, which will be done in the next step.
Important to note is the `return` statement afterwards. If you wouldn't return here, it would add an error to the error collection for each invalid line item, resulting in several errors displayed on the checkout or the cart page. E.g. if you had four invalid items in your cart, four separate errors would be shown. This way, only one message is shown, so it depends on what you're validating and what you want to happen.
#### Registering the validator
One more thing to do is to register your new validator to the [dependency injection container](../../plugin-fundamentals/dependency-injection).
Your validator has to be registered using the tag `shopware.cart.validator`:
```xml
// /src/Resources/config/services.xml
```
### Adding the custom cart error
The custom cart error class will be called `CustomCartBlockedError` and should be located in a `Error` directory in the same domain as the validator. Since the validator was located in the directory `/src/Core/Checkout/Cart/Custom`, the error class will be located in the directory `/src/Core/Checkout/Cart/Custom/Error`.
It has to extend from the abstract class `Shopware\Core\Checkout\Cart\Error\Error`, which asks you to implement a few methods:
* `getId`: Here you have to return a unique ID, since your error will be saved via this ID in the error collection. In this example,
we'll just use the line item ID here.
* `getMessageKey`: The snippet key of the message to be displayed. In this example it will be `custom-line-item-blocked`, which is important
for the next section of this guide, for adding the snippets.
* `getLevel`: The kind of error, available are `notice`, `warning` and `error`. Depending on that decision, the error will be printed in a blue,
yellow or red box respectively. This example will use the error here.
* `blockOrder`: Return a boolean on whether this exception should block the possibility to actually finish the checkout.
In this case it will be `true`, hence the error level defined earlier. It wouldn't make sense to block the checkout, but only display a notice.
* `blockResubmit`: Optional, return a boolean on whether this exception block the user from trying to finish the checkout again.
If you want to use it, add the method `blockResubmit(): bool` to your custom error. If you don't, it is `true` by default.
* `getParameters`: You can add custom payload here. Technically any plugin or code could read the errors of the cart and act accordingly.
If you need extra payload to your error class, this is the place to go.
So now let's have a look at the example error class:
```php
// /src/Core/Checkout/Cart/Custom/Error/CustomCartBlockedError.php
lineItemId = $lineItemId;
parent::__construct();
}
public function getId(): string
{
return $this->lineItemId;
}
public function getMessageKey(): string
{
return self::KEY;
}
public function getLevel(): int
{
// return self::LEVEL_NOTICE;
// return self::LEVEL_WARNING;
return self::LEVEL_ERROR;
}
public function blockOrder(): bool
{
return true;
}
public function getParameters(): array
{
return [ 'lineItemId' => $this->lineItemId ];
}
}
```
The constructor was overridden so we can ask for the line item ID and save it in a property. Since we already used this class in the validator, we're basically done with that part here.
Only the snippets are missing.
### Adding the snippet
First of all you should know our guide about [adding storefront snippets](../../storefront/add-translations), since that won't be explained in detail here.
You've defined the error key to be `custom-line-item-blocked` in your custom error class `CustomCartBlockedError`. Once your validator finds an invalid line item in your cart, Shopware is going to search for a respective snippet. In the cart, Shopware will be looking for the following snippet key: `checkout.custom-line-item-blocked`. Meanwhile it will be looking for a key `error.custom-line-item-blocked` in the checkout steps. This way you could technically define two different messages for the cart and the following checkout steps.
Now let's have a look at an example snippet file:
```js
// /src/Resources/snippet/en\_GB/example.en-GB.json
{
"checkout": {
"custom-line-item-blocked": "Example error message for the cart"
},
"error": {
"custom-line-item-blocked": "Example error message for the checkout"
}
}
```
This way Shopware will find the new snippets in your plugin and display the respective error message.
And that's it, you've now successfully added your own cart validator.
## Next steps
In the examples mentioned above, we're asking for custom line item payloads. This subject is covered in our guide about [adding cart items](add-cart-items), so you might want to have a look at that.
---
---
url: /docs/v6.6/guides/plugins/plugins/checkout/cart/add-cart-validator.md
---
# Add Cart Validator
## Overview
The cart in Shopware is constantly being validated by so called "validators". This way we can check for an invalid cart, e.g. for invalid line items (label missing) or an invalid shipping address.
This guide will cover the subject on how to add your own custom cart validator.
## Prerequisites
For this guide, you will need a working plugin, which you learn to create [here](../../plugin-base-guide). Also, you will have to know the [Dependency Injection container](../../plugin-fundamentals/dependency-injection), since that's going to be used in order to register your custom validator.
## Adding a custom cart validator
We'll create several things throughout this guide, in that order:
* The validator itself
* A new exception being thrown by the validator if needed
* Snippets to print a proper error message
### The validator
The validator being created in this example is assuming you've got custom payload data in your line items to validate against. This is just an example and will always result in an error, since the data requested doesn't exist by default, until you add them.
A validator should be placed in the proper domain. That means, that an Address validator should be in a directory `/src/Core/Checkout/Cart/Address`. Since the validator in the following example will be called `CustomCartValidator`, its directory will be `/src/Core/Checkout/Cart/Custom`.
Your validator has to implement the interface `Shopware\Core\Checkout\Cart\CartValidatorInterface`. This forces you to also implement a `validate` method.
But let's have a look at the example validator first:
```php
// /src/Core/Checkout/Cart/Custom/CustomCartValidator.php
getLineItems()->getFlat() as $lineItem) {
if (!array_key_exists('customPayload', $lineItem->getPayload()) || $lineItem->getPayload()['customPayload'] !== 'example') {
$errorCollection->add(new CustomCartBlockedError($lineItem->getId()));
return;
}
}
}
}
```
As already said, a cart validator has to implement the `CartValidatorInterface` and therefore implement a `validate` method. This method has access to some important parts of the checkout, such as the cart and the current sales channel context. Also you have access to the error collection, which may or may not contain errors from other earlier validators.
In this example we're dealing with the line items and are validating them, so we're iterating over each line item. This example assumes that your line items got a custom payload, called `customPayload`, and it expects a value in there.
If the condition doesn't match and the line item seems to be invalid, you'll have to add a new error to the error collection. You can't just use any exception here, but a class which has to extend from `Shopware\Core\Checkout\Cart\Error\Error`. Most likely you want to create your own error class here, which will be done in the next step.
Important to note is the `return` statement afterwards. If you wouldn't return here, it would add an error to the error collection for each invalid line item, resulting in several errors displayed on the checkout or the cart page. E.g. if you had four invalid items in your cart, four separate errors would be shown. This way, only one message is shown, so it depends on what you're validating and what you want to happen.
#### Registering the validator
One more thing to do is to register your new validator to the [dependency injection container](../../plugin-fundamentals/dependency-injection).
Your validator has to be registered using the tag `shopware.cart.validator`:
```xml
// /src/Resources/config/services.xml
```
### Adding the custom cart error
The custom cart error class will be called `CustomCartBlockedError` and should be located in a `Error` directory in the same domain as the validator. Since the validator was located in the directory `/src/Core/Checkout/Cart/Custom`, the error class will be located in the directory `/src/Core/Checkout/Cart/Custom/Error`.
It has to extend from the abstract class `Shopware\Core\Checkout\Cart\Error\Error`, which asks you to implement a few methods:
* `getId`: Here you have to return a unique ID, since your error will be saved via this ID in the error collection. In this example,
we'll just use the line item ID here.
* `getMessageKey`: The snippet key of the message to be displayed. In this example it will be `custom-line-item-blocked`, which is important
for the next section of this guide, for adding the snippets.
* `getLevel`: The kind of error, available are `notice`, `warning` and `error`. Depending on that decision, the error will be printed in a blue,
yellow or red box respectively. This example will use the error here.
* `blockOrder`: Return a boolean on whether this exception should block the possibility to actually finish the checkout.
In this case it will be `true`, hence the error level defined earlier. It wouldn't make sense to block the checkout, but only display a notice.
* `blockResubmit`: Optional, return a boolean on whether this exception block the user from trying to finish the checkout again.
If you want to use it, add the method `blockResubmit(): bool` to your custom error. If you don't, it is `true` by default.
* `getParameters`: You can add custom payload here. Technically any plugin or code could read the errors of the cart and act accordingly.
If you need extra payload to your error class, this is the place to go.
So now let's have a look at the example error class:
```php
// /src/Core/Checkout/Cart/Custom/Error/CustomCartBlockedError.php
lineItemId = $lineItemId;
parent::__construct();
}
public function getId(): string
{
return $this->lineItemId;
}
public function getMessageKey(): string
{
return self::KEY;
}
public function getLevel(): int
{
// return self::LEVEL_NOTICE;
// return self::LEVEL_WARNING;
return self::LEVEL_ERROR;
}
public function blockOrder(): bool
{
return true;
}
public function getParameters(): array
{
return [ 'lineItemId' => $this->lineItemId ];
}
}
```
The constructor was overridden so we can ask for the line item ID and save it in a property. Since we already used this class in the validator, we're basically done with that part here.
Only the snippets are missing.
### Adding the snippet
First of all you should know our guide about [adding storefront snippets](../../storefront/add-translations), since that won't be explained in detail here.
You've defined the error key to be `custom-line-item-blocked` in your custom error class `CustomCartBlockedError`. Once your validator finds an invalid line item in your cart, Shopware is going to search for a respective snippet. In the cart, Shopware will be looking for the following snippet key: `checkout.custom-line-item-blocked`. Meanwhile it will be looking for a key `error.custom-line-item-blocked` in the checkout steps. This way you could technically define two different messages for the cart and the following checkout steps.
Now let's have a look at an example snippet file:
```javascript
// /src/Resources/snippet/en\_GB/example.en-GB.json
{
"checkout": {
"custom-line-item-blocked": "Example error message for the cart"
},
"error": {
"custom-line-item-blocked": "Example error message for the checkout"
}
}
```
This way Shopware will find the new snippets in your plugin and display the respective error message.
And that's it, you've now successfully added your own cart validator.
## Next steps
In the examples mentioned above, we're asking for custom line item payloads. This subject is covered in our guide about [adding cart items](add-cart-items), so you might want to have a look at that.
---
---
url: /docs/v6.5/guides/plugins/plugins/content/cms/add-cms-block.md
---
# Add CMS Block
## Overview
This guide will teach you how to create your very own CMS block with your plugin.
## Prerequisites
This plugin is built upon our plugin from the [Plugin base guide](../../plugin-base-guide), but the examples mentioned here are applicable to every valid Shopware 6 plugin. Also, you should know how to handle the "Shopping Experiences" module in the Administration first. Furthermore, you definitely need to know how to create a custom component in the Administration, which is covered here [Creating a component](../../administration/add-custom-component).
## Custom block in the Administration
Let's get started with adding your first custom block. By default, Shopware 6 comes with several blocks, such as a block called `image_text`. It renders an image element on the left side and a simple text element on the right side. In this guide, you're going to create a new block to swap those two elements, so the text is on the left side and the image on the right side.
All blocks can be found in the directory [/src/Administration/Resources/app/administration/src/module/sw-cms/blocks](https://github.com/shopware/shopware/tree/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/blocks). In there, they are divided into the categories `commerce`, `form`, `image`, `sidebar`, `text-image`, `text` and `video`.
`commerce` : Blocks using a special template can be found here, e.g. a product slider block.
`form` : A single block displaying a form, mainly the `contact` or the `newsletter` form.
`image` : Only image elements are used by these blocks.
`sidebar` : Blocks for the sidebar, such as the listing filters or the category navigation.
`text-image` : Blocks, that are making use of both, text and images, belong here.
`text` : Blocks only using text elements are located here.
`video` : Our blocks for youtube and vimeo videos reside here.
### Injecting into the Administration
The main entry point to customize the Administration via plugin is the `main.js` file. It has to be placed into a `/src/Resources/app/administration/src` directory in order to be automatically found by Shopware 6.
Create this `main.js` file for now, it will be used later.
### Registering a new block
Your plugin's structure should always match the core's structure. When thinking about creating a new block, you should recreate the directory structure of core blocks in your plugin. The block, which you're going to create, consists of an `image` and a `text` element, so it belongs to the category `text-image`. Thus, create the directory `/src/Resources/app/administration/src/module/sw-cms/blocks/text-image`.
In there, you have to create a new directory for each block you want to create, the directory's name representing the block's name. For this example, the name `my-image-text-reversed` is going to be used, so create this directory in there.
Now create a new file `index.js` inside the `my-image-text-reversed` directory, since it will be automatically loaded when importing this block in your `main.js`. Speaking of that, right after having created the `index.js` file, you can actually import your new block directory in the `main.js` file already:
```javascript
// /src/Resources/app/administration/src/main.js
import './module/sw-cms/blocks/text-image/my-image-text-reversed';
```
Back to your `index.js`, which is still empty. In order to register a new block, you have to call the `registerCmsBlock` method of the [cmsService](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/service/cms.service.js). Since it's available in the Dependency Injection Container, you can fetch it from there.
First of all, access our `Application` wrapper, which will grant you access to the DI container. This `Application` wrapper has access to the DI container, so go ahead and fetch the `cmsService` from it and call the mentioned `registerCmsBlock` method.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
Shopware.Service('cmsService').registerCmsBlock();
```
#### The configuration object
The method `registerCmsBlock` takes a configuration object, containing the following necessary data:
`name` : The technical name of your block. Will be used for the template and component loading later on.
`label` : A name to be shown for your block in the User Interface.
`category` : The category this block belongs to.
`component` : The Vue component to be used when rendering your actual block in the Administration sidebar.
`previewComponent` : The Vue component to be used in the "list of available blocks". Just shows a tiny preview of what your block would look like if it was used.
`defaultConfig` : A default configuration to be applied to this block. Must be an object containing those default values.
`slots` : Key-value pair to configure which element to be shown in which slot. Will be explained in the next few steps when creating a template for this block.
Go ahead and create this configuration object yourself. Here's what it should look like after having set all of those options:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
Shopware.Service('cmsService').registerCmsBlock({
name: 'my-image-text-reversed',
category: 'text-image',
label: 'My Image Text Block!',
component: 'sw-cms-block-my-image-text-reversed',
previewComponent: 'sw-cms-preview-my-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed'
},
slots: {
left: 'text',
right: 'image'
}
});
```
The `component` and `previewComponent` do not exist yet, but they are created later in this guide. The `defaultConfig` just gets some minor margins and the sizing mode 'boxed', which will result in a CSS class [is--boxed](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/component/sw-cms-block/sw-cms-block.scss) being applied to that block later. The slots are defined by an object, where the key represents the slot's name and the value being the technical name of the element to be used in this slot. This will be easier to understand when having a look at the respective template in a few minutes. Also you might want to have a look at the [Vue documentation regarding slots](https://vuejs.org/v2/guide/components-slots.html).
### Rendering the block
You've set the `name` of the component to be used when rendering your block to be 'sw-cms-block-my-image-text-reversed'. This component does not exist yet, so let's create this one real quick. As already mentioned, creating a component is not explained by this guide in detail, so you might want to head over to our guide about [Creating a component](../../administration/add-custom-component) first.
First of all, create a new directory `component` in your block's directory. In there, create a new `index.js` file and register your custom component `sw-cms-block-my-image-text-reversed`.
**Keep in mind: The component name consists of `sw-cms-block-` and the `name` property mentioned in your `index.js`, while registering your cms block component via `registerCmsBlock()`!**
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/component/index.js
import template from './sw-cms-block-my-image-text-reversed.html.twig';
import './sw-cms-block-my-image-text-reversed.scss';
Shopware.Component.register('sw-cms-block-my-image-text-reversed', {
template
});
```
Just like most components, it has a custom template and also some styles. Focus on the template first, create a new file `sw-cms-block-my-image-text-reversed.html.twig`.
This template now has to define the basic structure of your custom block. In this simple case, you only need a parent container and two sub-elements, whatever those are. That's also were the slots come into play: You've used two slots in your block's configuration, `left` and `right`. Make sure to create those slots in the template as well now.
```twig
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/component/sw-cms-block-my-image-text-reversed.html.twig
{% block sw_cms_block_my_image_text_reversed %}
{% endblock %}
```
You've got a parent `div` containing the two required [slots](https://vuejs.org/v2/guide/components-slots.html). If you were to rename the first slot `left` to something else, you'd have to adjust this in your block's configuration as well.
Those slots would be rendered from top to bottom now, instead of from left to right. That's why your block comes with a custom `.scss` file, create it now by adding the file `sw-cms-block-my-image-text-reversed.scss` to your `component` directory.
In there, use a grid to display your elements next to each other. You've set a CSS class for your block, which is the same as its name.
```css
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/component/sw-cms-block-my-image-text-reversed.scss
.sw-cms-block-my-image-text-reversed {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 40px;
}
```
That's it for this component! Make sure to import your `component` directory in your `index.js` file, so your new component actually gets loaded.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
import './component'; // <- Right here!
Shopware.Service('cmsService').registerCmsBlock({
...
});
```
Your block can now be rendered in the designer. Let's continue with the preview component.
### Block preview
You've also set a property `previewComponent` containing the value `sw-cms-preview-my-image-text-reversed`. Time to create this component as well. For this purpose, stick to the core structure again and create a new directory `preview`. In there, again, create an `index.js` file, register your component by its name and load a template and a `.scss` file.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/index.js
import template from './sw-cms-preview-my-image-text-reversed.html.twig';
import './sw-cms-preview-my-image-text-reversed.scss';
Shopware.Component.register('sw-cms-preview-my-image-text-reversed', {
template
});
```
The preview element doesn't have to deal with mobile viewports or anything alike, it's just a simplified preview of your block. Thus, create a template containing a text and an image and use the styles to place them next to each other. Create a `sw-cms-preview-my-image-text-reversed.html.twig` file in your `preview` directory with the following content.
```twig
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/sw-cms-preview-my-image-text-reversed.html.twig
{% block sw_cms_block_my_image_text_reversed_preview %}
Lorem ipsum dolor
Lorem ipsum dolor sit amet, consetetur sadipscing elitr.
{% endblock %}
```
Just a div containing some text and an example image next to that. For the styles, you can simply use the grid property of CSS again. Since you don't have to care about mobile viewports, this is even easier this time.
Now create the styles file `sw-cms-preview-my-image-text-reversed.scss` with the following styles:
```css
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/sw-cms-preview-my-image-text-reversed.scss
.sw-cms-preview-my-image-text-reversed {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 20px;
padding: 15px;
}
```
A two-column layout, some padding and spacing here and there, done.
Now, import this component in your block's `index.js` as well. This is, what your final block's `index.js` file should look like now:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
import './component';
import './preview';
Shopware.Service('cmsService').registerCmsBlock({
name: 'my-image-text-reversed',
category: 'text-image',
label: 'My Image Text Block!',
component: 'sw-cms-block-my-image-text-reversed',
previewComponent: 'sw-cms-preview-my-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed'
},
slots: {
left: 'text',
right: 'image'
}
});
```
In order to test your changes now, you should rebuild your Administration. This can be done with the following command:
```bash
./bin/build-administration.sh
```
```bash
composer run build:js:admin
```
You should now be able to use your new block in the "Shopping Experiences" module.
## Storefront representation
While your new block is fully functional in the Administration already, you've never defined a template for it for the Storefront.
A block's Storefront representation is always expected in the directory [platform/src/Storefront/Resources/views/storefront/block](https://github.com/shopware/shopware/tree/v6.3.4.1/src/Storefront/Resources/views/storefront/block). In there, a twig template named after your block is expected.
So go ahead and re-create that structure in your plugin: `/src/Resources/views/storefront/block/`
Create a new twig template named after your block. The filename convention for this is :
* Starts with the prefix `cms-block-`
* Followed by the technical name of the block `my-image-text-reversed`
* Ends with the extension `.html.twig`
Example : `cms-block-my-image-text-reversed.html.twig`.
Since the [original 'image\_text' file](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Storefront/Resources/views/storefront/block/cms-block-image-text.html.twig) is already perfectly fine, you can go ahead and extend from it in your storefront template.
```twig
// /src/Resources/views/storefront/block/cms-block-my-image-text-reversed.html.twig
{% sw_extends '@Storefront/storefront/block/cms-block-image-text.html.twig' %}
```
And that's it for the Storefront as well in this example! Make sure to have a look at the other original templates to get and understand how the templating for blocks works.
## Next steps
Now you've got your very own CMS block running, what about a custom CMS element? Head over to our guide, which will explain exactly that: [Creating a custom CMS element](add-cms-element)
---
---
url: /docs/v6.6/guides/plugins/plugins/content/cms/add-cms-block.md
---
# Add CMS Block
## Overview
This guide will teach you how to create your very own CMS block with your plugin.
## Prerequisites
This plugin is built upon our plugin from the [Plugin base guide](../../plugin-base-guide), but the examples mentioned here are applicable to every valid Shopware 6 plugin. Also, you should know how to handle the "Shopping Experiences" module in the Administration first. Furthermore, you definitely need to know how to create a custom component in the Administration, which is covered here [Creating a component](../../administration/add-custom-component).
## Custom block in the Administration
Let's get started with adding your first custom block. By default, Shopware 6 comes with several blocks, such as a block called `image_text`. It renders an image element on the left side and a simple text element on the right side. In this guide, you're going to create a new block to swap those two elements, so the text is on the left side and the image on the right side.
All blocks can be found in the directory [/src/Administration/Resources/app/administration/src/module/sw-cms/blocks](https://github.com/shopware/shopware/tree/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/blocks). In there, they are divided into the categories `commerce`, `form`, `image`, `sidebar`, `text-image`, `text` and `video`.
`commerce` : Blocks using a special template can be found here, e.g. a product slider block.
`form` : A single block displaying a form, mainly the `contact` or the `newsletter` form.
`image` : Only image elements are used by these blocks.
`sidebar` : Blocks for the sidebar, such as the listing filters or the category navigation.
`text-image` : Blocks, that are making use of both, text and images, belong here.
`text` : Blocks only using text elements are located here.
`video` : Our blocks for youtube and vimeo videos reside here.
### Injecting into the Administration
The main entry point to customize the Administration via plugin is the `main.js` file. It has to be placed into a `/src/Resources/app/administration/src` directory in order to be automatically found by Shopware 6.
Create this `main.js` file for now, it will be used later.
### Registering a new block
Your plugin's structure should always match the core's structure. When thinking about creating a new block, you should recreate the directory structure of core blocks in your plugin. The block, which you're going to create, consists of an `image` and a `text` element, so it belongs to the category `text-image`. Thus, create the directory `/src/Resources/app/administration/src/module/sw-cms/blocks/text-image`.
In there, you have to create a new directory for each block you want to create, the directory's name representing the block's name. For this example, the name `my-image-text-reversed` is going to be used, so create this directory in there.
Now create a new file `index.js` inside the `my-image-text-reversed` directory, since it will be automatically loaded when importing this block in your `main.js`. Speaking of that, right after having created the `index.js` file, you can actually import your new block directory in the `main.js` file already:
```javascript
// /src/Resources/app/administration/src/main.js
import './module/sw-cms/blocks/text-image/my-image-text-reversed';
```
Back to your `index.js`, which is still empty. In order to register a new block, you have to call the `registerCmsBlock` method of the [cmsService](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/service/cms.service.js). Since it's available in the Dependency Injection Container, you can fetch it from there.
First of all, access our `Application` wrapper, which will grant you access to the DI container. This `Application` wrapper has access to the DI container, so go ahead and fetch the `cmsService` from it and call the mentioned `registerCmsBlock` method.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
Shopware.Service('cmsService').registerCmsBlock();
```
#### The configuration object
The method `registerCmsBlock` takes a configuration object, containing the following necessary data:
`name` : The technical name of your block. Will be used for the template and component loading later on.
`label` : A name to be shown for your block in the User Interface.
`category` : The category this block belongs to.
`component` : The Vue component to be used when rendering your actual block in the Administration sidebar.
`previewComponent` : The Vue component to be used in the "list of available blocks". Just shows a tiny preview of what your block would look like if it was used.
`defaultConfig` : A default configuration to be applied to this block. Must be an object containing those default values.
`slots` : Key-value pair to configure which element to be shown in which slot. Will be explained in the next few steps when creating a template for this block.
Go ahead and create this configuration object yourself. Here's what it should look like after having set all of those options:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
Shopware.Service('cmsService').registerCmsBlock({
name: 'my-image-text-reversed',
category: 'text-image',
label: 'My Image Text Block!',
component: 'sw-cms-block-my-image-text-reversed',
previewComponent: 'sw-cms-preview-my-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed'
},
slots: {
left: 'text',
right: 'image'
}
});
```
The `component` and `previewComponent` do not exist yet, but they are created later in this guide. The `defaultConfig` just gets some minor margins and the sizing mode 'boxed', which will result in a CSS class [is--boxed](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/component/sw-cms-block/sw-cms-block.scss) being applied to that block later. The slots are defined by an object, where the key represents the slot's name and the value being the technical name of the element to be used in this slot. This will be easier to understand when having a look at the respective template in a few minutes. Also you might want to have a look at the [Vue documentation regarding slots](https://vuejs.org/v2/guide/components-slots.html).
### Rendering the block
You've set the `name` of the component to be used when rendering your block to be 'sw-cms-block-my-image-text-reversed'. This component does not exist yet, so let's create this one real quick. As already mentioned, creating a component is not explained by this guide in detail, so you might want to head over to our guide about [Creating a component](../../administration/add-custom-component) first.
First of all, create a new directory `component` in your block's directory. In there, create a new `index.js` file and register your custom component `sw-cms-block-my-image-text-reversed`.
**Keep in mind: The component name consists of `sw-cms-block-` and the `name` property mentioned in your `index.js`, while registering your cms block component via `registerCmsBlock()`!**
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/component/index.js
import template from './sw-cms-block-my-image-text-reversed.html.twig';
import './sw-cms-block-my-image-text-reversed.scss';
Shopware.Component.register('sw-cms-block-my-image-text-reversed', {
template
});
```
Just like most components, it has a custom template and also some styles. Focus on the template first, create a new file `sw-cms-block-my-image-text-reversed.html.twig`.
This template now has to define the basic structure of your custom block. In this simple case, you only need a parent container and two sub-elements, whatever those are. That's also were the slots come into play: You've used two slots in your block's configuration, `left` and `right`. Make sure to create those slots in the template as well now.
```twig
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/component/sw-cms-block-my-image-text-reversed.html.twig
{% block sw_cms_block_my_image_text_reversed %}
{% endblock %}
```
You've got a parent `div` containing the two required [slots](https://vuejs.org/v2/guide/components-slots.html). If you were to rename the first slot `left` to something else, you'd have to adjust this in your block's configuration as well.
Those slots would be rendered from top to bottom now, instead of from left to right. That's why your block comes with a custom `.scss` file, create it now by adding the file `sw-cms-block-my-image-text-reversed.scss` to your `component` directory.
In there, use a grid to display your elements next to each other. You've set a CSS class for your block, which is the same as its name.
```css
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/component/sw-cms-block-my-image-text-reversed.scss
.sw-cms-block-my-image-text-reversed {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 40px;
}
```
That's it for this component! Make sure to import your `component` directory in your `index.js` file, so your new component actually gets loaded.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
import './component'; // <- Right here!
Shopware.Service('cmsService').registerCmsBlock({
...
});
```
Your block can now be rendered in the designer. Let's continue with the preview component.
### Block preview
You've also set a property `previewComponent` containing the value `sw-cms-preview-my-image-text-reversed`. Time to create this component as well. For this purpose, stick to the core structure again and create a new directory `preview`. In there, again, create an `index.js` file, register your component by its name and load a template and a `.scss` file.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/index.js
import template from './sw-cms-preview-my-image-text-reversed.html.twig';
import './sw-cms-preview-my-image-text-reversed.scss';
Shopware.Component.register('sw-cms-preview-my-image-text-reversed', {
template
});
```
The preview element doesn't have to deal with mobile viewports or anything alike, it's just a simplified preview of your block. Thus, create a template containing a text and an image and use the styles to place them next to each other. Create a `sw-cms-preview-my-image-text-reversed.html.twig` file in your `preview` directory with the following content.
```twig
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/sw-cms-preview-my-image-text-reversed.html.twig
{% block sw_cms_block_my_image_text_reversed_preview %}
Lorem ipsum dolor
Lorem ipsum dolor sit amet, consetetur sadipscing elitr.
{% endblock %}
```
Also, you need to create a computed component to access the asset filter in your template.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/index.js
computed: {
assetFilter() {
return Shopware.Filter.getByName('asset');
},
}
```
Just a div containing some text and an example image next to that. For the styles, you can simply use the grid property of CSS again. Since you don't have to care about mobile viewports, this is even easier this time.
Now create the styles file `sw-cms-preview-my-image-text-reversed.scss` with the following styles:
```css
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/preview/sw-cms-preview-my-image-text-reversed.scss
.sw-cms-preview-my-image-text-reversed {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 20px;
padding: 15px;
}
```
A two-column layout, some padding and spacing here and there, done.
Now, import this component in your block's `index.js` as well. This is, what your final block's `index.js` file should look like now:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/my-image-text-reversed/index.js
import './component';
import './preview';
Shopware.Service('cmsService').registerCmsBlock({
name: 'my-image-text-reversed',
category: 'text-image',
label: 'My Image Text Block!',
component: 'sw-cms-block-my-image-text-reversed',
previewComponent: 'sw-cms-preview-my-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed'
},
slots: {
left: 'text',
right: 'image'
}
});
```
In order to test your changes now, you should rebuild your Administration. This can be done with the following command:
```bash
./bin/build-administration.sh
```
```bash
composer run build:js:admin
```
You should now be able to use your new block in the "Shopping Experiences" module.
## Storefront representation
While your new block is fully functional in the Administration already, you've never defined a template for it for the Storefront.
A block's Storefront representation is always expected in the directory [platform/src/Storefront/Resources/views/storefront/block](https://github.com/shopware/shopware/tree/v6.3.4.1/src/Storefront/Resources/views/storefront/block). In there, a twig template named after your block is expected.
So go ahead and re-create that structure in your plugin: `/src/Resources/views/storefront/block/`
Create a new twig template named after your block. The filename convention for this is :
* Starts with the prefix `cms-block-`
* Followed by the technical name of the block `my-image-text-reversed`
* Ends with the extension `.html.twig`
Example : `cms-block-my-image-text-reversed.html.twig`.
Since the [original 'image\_text' file](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Storefront/Resources/views/storefront/block/cms-block-image-text.html.twig) is already perfectly fine, you can go ahead and extend from it in your storefront template.
```twig
// /src/Resources/views/storefront/block/cms-block-my-image-text-reversed.html.twig
{% sw_extends '@Storefront/storefront/block/cms-block-image-text.html.twig' %}
```
And that's it for the Storefront as well in this example! Make sure to have a look at the other original templates to get and understand how the templating for blocks works.
## Next steps
Now you've got your very own CMS block running, what about a custom CMS element? Head over to our guide, which will explain exactly that: [Creating a custom CMS element](add-cms-element)
---
---
url: /docs/guides/plugins/plugins/content/cms/add-cms-block.md
---
# Add CMS Blocks
## Overview
A CMS block in Shopware is a fundamental structural component of the Shopping Experience (CMS) system. Understanding the hierarchy helps clarify what blocks are.
### CMS Hierarchy
* Page - The top-level container (e.g., category page, shop page, product page)
* Section - Horizontal segments within a page (can be single-column or two-column with sidebar)
* **Block - Units that usually span an entire row with custom layout and styling**
* **Slots - A named container inside a block. Each slot represents a designated area that can hold exactly one CMS element**
* Elements - The actual content primitives (text, image, video, product listing, etc.) placed inside slots
A block represents a reusable layout unit that defines how elements are arranged in slots. For example, Shopware's built-in `image-text` block displays an image on the left and text on the right. Blocks are clustered into categories like Text, Images, Commerce, and Video for organizational purposes in the administration interface.
**Key concept**: Blocks define the structure (layout and slots), while elements provide the actual content. This separation allows the same block to display different types of content in its slots.
> **Learn more**: For a deeper understanding of the CMS architecture, see the [Shopping Experience fundamental guide](../../../../../concepts/commerce/content/shopping-experiences-cms.md).
## Where to Find Blocks
Blocks are located in the Shopping Experience module in the Shopware Administration:
* Navigate to Content β Shopping Experience
* Create a new layout or edit an existing one
* In the layout designer, you'll see a sidebar with available blocks organized by category:
* Text - Text-only blocks
* Images - Image-only blocks
* Text & Images - Combined text and image blocks
* Commerce - Product sliders, listings, etc.
* Video - YouTube and Vimeo video blocks
* Form - Contact and newsletter forms
* Sidebar - Category navigation and listing filters
Drag and drop blocks from the sidebar into your layout sections.
You can find related block code here:
* Administration: `src/Administration/Resources/app/administration/src/module/sw-cms/blocks/`
* Storefront: `src/Storefront/Resources/views/storefront/block/`
* Core: `\Shopware\Core\Content\Cms\SalesChannel\SalesChannelCmsPageLoader::load`
## How to Create a Block in the Administration
### Directory Structure
We recommend this structure for CMS blocks:
```TEXT
/src/Resources/app/administration/src/
βββ main.js
βββ module/
βββ sw-cms/
βββ blocks/
βββ text-image/ (category)
βββ image-text-reversed/ (block name)
βββ index.js
βββ component/
β βββ index.js
β βββ cms-block-image-text-reversed.html.twig
β βββ cms-block-image-text-reversed.scss
βββ preview/
βββ index.js
βββ cms-block-preview-image-text-reversed.html.twig
βββ cms-block-preview-image-text-reversed.scss
```
### Step 1: Import Your Block in main.js
```JS
// /src/Resources/app/administration/src/main.js
import './module/sw-cms/blocks/text-image/image-text-reversed';
```
### Step 2: Register the Block
```JS
// /src/Resources/app/administration/src/module/sw-cms/blocks/text-image/image-text-reversed/index.js
import './component';
import './preview';
Shopware.Service('cmsService').registerCmsBlock({
name: 'image-text-reversed',
category: 'text-image',
label: 'cms.blocks.imageTextReversed.label',
component: 'cms-block-image-text-reversed',
previewComponent: 'cms-block-preview-image-text-reversed',
defaultConfig: {
marginBottom: '20px',
marginTop: '20px',
marginLeft: '20px',
marginRight: '20px',
sizingMode: 'boxed',
},
slots: {
left: 'text',
right: 'image',
},
});
```
| Property | Description |
|------------------|----------------------------------------------------------------------------------------------|
| `name` | Technical name of your block |
| `category` | Which category it appears under (`text`, `image`, `text-image`, `commerce`, `form`, `video`, `sidebar`) |
| `label` | Display name in the UI |
| `component` | Vue component for rendering the block in the designer |
| `previewComponent` | Vue component for the block thumbnail preview |
| `defaultConfig` | Default styling values |
| `slots` | Defines which element types go in which slots (key = slot name, value = element type) |
### Step 3: Create the Block Component
It's important to include all slots you defined in the block configuration (Step 2). These are used for configuring elements in the administration interface.
```JS
// image-text-reversed/component/index.js
Shopware.Component.register('cms-block-image-text-reversed', {
template: `
`,
});
```
### Step 4: Create the Preview Component
The preview is shown as a thumbnail when selecting a block from the editor sidebar. You could also display a static image of your final Storefront block here.
```JS
// image-text-reversed/preview/index.js
Shopware.Component.register('cms-block-preview-image-text-reversed', {
template: `
Lorem ipsum dolor sit amet
`,
computed: {
assetFilter() {
return Shopware.Filter.getByName('asset');
},
},
});
```
After this, the block preview should appear in the Shopping Experience block sidebar under the "Text & Images" category and can be added to a layout.
## How to Create a Block in the Storefront
The Storefront template defines how your element appears on the actual storefront. It is expected to be located in the directory `src/Resources/views/storefront/block`. In there, a twig template file has to follow this naming convention:
* **Prefix**: `cms-block-`
* **Technical name**: `image-text-reversed` (The `name` property in Step 2)
* **Extension**: `.html.twig`
The block components are loaded in `src/Storefront/Resources/views/storefront/section/cms-section-block-container.html.twig`. This loader expects the following format for block components: `cms-block-image-text-reversed.html.twig`.
### Basic Template
You can create your own blocks or extend and reuse existing ones. Don't forget to clear the Storefront cache after adding new templates.
```TWIG
{# /src/Resources/views/storefront/block/cms-block-image-text-reversed.html.twig #}
{% set element = block.slots.getSlot('left') %}
{% sw_include '@Storefront/storefront/element/cms-element-' ~ element.type ~ '.html.twig' with {
'element': element
} %}
{% set element = block.slots.getSlot('right') %}
{% sw_include '@Storefront/storefront/element/cms-element-' ~ element.type ~ '.html.twig' with {
'element': element
} %}
```
The `block` is automatically passed to the template and contains meta data and configuration values. See the `CmsBlockDefinition.php` for a full overview.
### How to Render Slots
Slots contain elements that need to be rendered. Here are the key methods:
#### 1. Get a Slot by Name
```TWIG
{% set leftSlot = block.slots.getSlot('left') %}
```
#### 2. Render an Element
Use `sw_include` to dynamically include the correct element template:
```TWIG
{% sw_include "@Storefront/storefront/element/cms-element-" ~ leftSlot.type ~ ".html.twig" with {
'element': leftSlot
} %}
```
This dynamically builds the template path based on the element type. For example:
* If `leftSlot.type` is text, it renders cms-element-text.html.twig
* If `leftSlot.type` is image, it renders cms-element-image.html.twig
#### 3. Loop Through All Slots
```TWIG
{% for slotName, slot in block.slots %}
{% sw_include "@Storefront/storefront/element/cms-element-" ~ slot.type ~ ".html.twig" with {
'element': slot
} %}
{% endfor %}
```
## Next steps
Now you've got your very own CMS block running, what about a custom CMS element? Head over to our guide, which will explain exactly that: [Creating a custom CMS element](add-cms-element.md).
---
---
url: /docs/guides/plugins/apps/administration/add-cms-element-via-admin-sdk.md
---
# Add CMS Element
## Overview
This guide explains how to create a new CMS element using the Meteor Admin SDK. The example plugin is named
`SwagBasicAppCmsElementExample`, following the naming conventions used in other guides.
## Prerequisites
* Familiarity with creating [Plugins](../../plugins/plugin-base-guide.md) or [Apps](../app-base-guide.md)
* Familiarity with [creating custom admin components](../../plugins/administration/module-component-management/add-custom-component.md#creating-a-custom-component)
* Understanding of the [Meteor Admin SDK](meteor-admin-sdk.md)
::: info
This example uses TypeScript, which is recommended but not required to develop Shopware.
:::
## Creating your custom element
Similar to [creating a new custom element via plugin](../../plugins/content/cms/add-cms-element.md), this guide describes how to create a new custom element via an app.
Creating a new element requires the Meteor Admin SDK.
::: info
Apps can also add CMS blocks declaratively via `cms.xml` without the Meteor Admin SDK.
That approach is simpler but limited to reusing existing Shopware elements inside the block's slots.
See [Add custom CMS blocks](../content/cms/add-custom-cms-blocks) for details.
:::
The example demonstrates a scenario where a shop manager can configure a video ID to display a Dailymotion video.
### Target structure
Any file structure works for apps, as everything is loaded via iFrame. Shopware recommends using Vue 3 single-file components (SFCs).
When the app is complete, the file structure will look like this:
```bash
// SwagBasicAppCmsElementExample/src/Resources/app/administration/src
βββ base
β βββ mainCommands.ts
βββ main.ts
βββ viewRenderer.ts
βββ views
βββ swag-dailymotion
βββ swag-dailymotion-config.vue
βββ swag-dailymotion-element.vue
βββ swag-dailymotion-preview.vue
```
## Initial loading of components
The entry point is the `main.ts` file:
```javascript
import { location } from '@shopware-ag/meteor-admin-sdk';
if (location.is(location.MAIN_HIDDEN)) {
// Execute the base commands
import('./base/mainCommands');
} else {
// Render different views
import('./viewRenderer');
}
```
Use `if(location.is(location.MAIN_HIDDEN))` to **load the main commands** defined in `mainCommands.ts`.
This branch loads logic only β no templates are rendered into the Administration here.
The `else` case loads the view templates via `viewRenderer.ts`.
### Loading all required templates
Next, create the `viewRenderer.ts` file, which loads the three required Vue SFCs for a CMS element:
* `swag-dailymotion-config.vue`, which will handle the content of the CMS element configuration
* `swag-dailymotion-element.vue`, which represents the actual target element in the CMS
* `swag-dailymotion-preview.vue`, which is responsible for the preview when selecting the CMS element in its selection
screen
Each file is named after its component and prefixed with `swag-dailymotion` (vendor prefix) to avoid naming conflicts.
The following example shows how component loading via `viewRenderer.ts` is implemented:
```javascript
import { createApp, defineAsyncComponent, h } from 'vue';
import { location } from '@shopware-ag/meteor-admin-sdk';
// watch for height changes
location.startAutoResizer();
const locations = {
'swag-dailymotion-element': defineAsyncComponent(
() => import('./views/swag-dailymotion/swag-dailymotion-element.vue'),
),
'swag-dailymotion-config': defineAsyncComponent(
() => import('./views/swag-dailymotion/swag-dailymotion-config.vue'),
),
'swag-dailymotion-preview': defineAsyncComponent(
() => import('./views/swag-dailymotion/swag-dailymotion-preview.vue'),
),
};
const app = createApp({
render: () => h(locations[location.get()]),
});
app.mount('#app');
```
The `locations` map connects each Shopware-provided location ID to the corresponding Vue component. `location.get()`
returns the current location ID so the correct component is rendered inside the iFrame.
Location IDs are a core concept of the Meteor Admin SDK β Shopware provides dedicated `locationIds` as injection points
for your templates. For CMS elements, these IDs are **auto-generated** from the element name plus the suffixes
`-element`, `-config`, and `-preview`. They become available once the element is registered (see the next section).
> **Learn more**: See the [Meteor Admin SDK locations reference](/resources/admin-extension-sdk/concepts/locations) for a full overview of the concept.
## Registering the block and element
The Shopware CMS distinguishes between two concepts:
* A **block** is the selectable container that appears in the block picker (organised by categories such as *Text*, *Image*, *Video*, etc.).
Users add blocks to a section, and each block contains one or more slots.
* An **element** is the content type that lives inside a slot (e.g., a video player, an image, a text).
Elements can also be swapped inside an existing slot via the element-replacement modal.
The registration method you call determines where your addition is reachable:
| What you call | Where it appears |
|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `registerCmsElement` only | Element-replacement modal only (the icon on an existing slot) |
| `registerCmsBlock` only | Block picker under the chosen category β but the slot renders nothing until an element is also registered |
| Both | Block picker **and** element-replacement modal |
To make your CMS addition fully discoverable and functional, call both.
Go to `mainCommands.ts` and add both registrations:
```javascript
import { cms } from '@shopware-ag/meteor-admin-sdk';
const CMS_ELEMENT_NAME = 'swag-dailymotion';
export const CONSTANTS = {
CMS_ELEMENT_NAME,
PUBLISHING_KEY: `${CMS_ELEMENT_NAME}__config-element`,
};
// Makes the block appear in the block picker under the "Video" category
void cms.registerCmsBlock({
name: CONSTANTS.CMS_ELEMENT_NAME,
label: 'Dailymotion video',
category: 'video',
slots: [{ element: CONSTANTS.CMS_ELEMENT_NAME }],
});
// Registers the element that fills the block's slot
void cms.registerCmsElement({
name: CONSTANTS.CMS_ELEMENT_NAME,
label: 'Dailymotion video',
defaultConfig: {
dailyUrl: {
source: 'static',
value: '',
},
},
});
```
The `category` field of `registerCmsBlock` controls which group the block appears in:
`'video'`, `'text'`, `'image'`, `'text-image'`, `'commerce'`, `'sidebar'`, `'form'`, or a custom string (which creates a new category group).
The `slots` array lists the element types each slot of the block accepts.
As a best practice, use a **constant** for the CMS element name and the publishing key.
The publishing key must be the element name followed by the `__config-element` suffix.
## Templates and communication with the Administration
The remaining files are the Vue single-file components inside the `views` folder. Create a folder with the full
component name containing three files as shown below:
```bash
// SwagBasicAppCmsElementExample/src/Resources/app/administration/src
views
βββ swag-dailymotion
βββ swag-dailymotion-config.vue
βββ swag-dailymotion-element.vue
βββ swag-dailymotion-preview.vue
```
### Element ID
When Shopware renders any of the three CMS iFrames, it automatically appends the ID of the current CMS element instance as an `elementId` query parameter to the iFrame URL:
```http request
https://your-app-server/...?elementId=
```
Use this ID together with the publishing key to address the correct element's data in Shopware:
```javascript
const params = new URLSearchParams(window.location.search);
const elementId = params.get('elementId');
const dataId = `${CONSTANTS.PUBLISHING_KEY}__${elementId}`;
```
### The config file
The following section describes each file, starting with `swag-dailymotion-config.vue`:
```html
Config!
Video-Code:
```
**Key points:**
* `data` is imported from the Meteor Admin SDK and handles all data exchange between the app and Shopware
* `dataId` is derived from the `elementId` query parameter appended by Shopware to the iFrame URL, combined with `CONSTANTS.PUBLISHING_KEY`
* `data.get()` accepts an optional `selectors` array so only the relevant fields are fetched; the result is a flat object keyed by selector path (e.g. `value['config.dailyUrl.value']`)
* `data.update()` sends only the changed config structure back to Shopware β not the entire element
* The current config is fetched via `data.get()` in `onBeforeMount` and linked to the computed property `dailyUrl`; the setter calls `data.update({ id, data })` to persist changes

### The element file
`swag-dailymotion-element.vue` contains the main rendering logic for the CMS element in the Administration:
```html
```
**Key points:**
* `data.get()` fetches the initial element config using the element-specific `dataId`
* `data.subscribe()` keeps the element in sync whenever the config changes β it receives the same flat selector-keyed object as `data.get()` and is called regardless of where the change originates

### The preview file
`swag-dailymotion-preview.vue` is the thumbnail shown in the block picker when a user browses the *Video* category.
In most cases it contains minimal logic β a static image, a skeleton, or a logo is sufficient:
```html
Preview!
```

## Storefront implementation
After completing the admin implementation, you also need a Storefront representation of your blocks. This is similar to typical plugin development, except for the path. All Storefront templates must follow this pattern:
`/Resources/views/storefront/element/.html.twig`
For more details, see the guide on [CMS element development for plugins](../../plugins/content/cms/add-cms-element#storefront-implementation).
Below is an example of how your storefront template
(`SwagBasicAppCmsElementExample/Resources/views/storefront/element/cms-element-swag-dailymotion.html.twig`) could look:
```twig
{% block element_swag_dailymotion %}
{% block element_dailymotion_image_inner %}
{% endblock %}
{% endblock %}
```
---
---
url: /docs/v6.5/guides/plugins/apps/administration/add-cms-element-via-admin-sdk.md
---
# Add CMS Element
## Overview
This article will teach you how to create a new CMS element via the Meteor Admin SDK. The plugin in this example will be named `SwagBasicAppCmsElementExample`, similar to the other guides.
## Prerequisites
* Knowledge on the creation of [Plugins](/docs/guides/plugins/plugins/plugin-base-guide) or [Apps](/docs/guides/plugins/apps/app-base-guide)
* Knowledge on the [creation of custom admin components](/docs/guides/plugins/plugins/administration/add-custom-component#creating-a-custom-component)
* Understanding the [Meteor Admin SDK](https://shopware.github.io/meteor-admin-sdk/docs/guide/getting-started/installation)
::: info
This example uses TypeScript, which is recommended, but not required for developing Shopware.
:::
## Creating your custom element
Similar to [Creating a new custom element via plugin](/docs/guides/plugins/plugins/content/cms/add-cms-element#creating-your-custom-element), this article describes creating a new custom element via app.
Creating a new element requires Meteor Admin SDK.
Consider the same scenario to allow a shop manager configure a link to display the Dailymotion video. That is exactly what you are going to build.
### Target structure
You can decide what approach to use when creating apps since everything here is loaded via iFrame. However, Shopware's best practice is a full Vue.js approach.
When our extension is finished, you will get the following file structure:
```bash
// /src/Resources/app/administration/src
βββ base
βΒ Β βββ mainCommands.ts
βββ main.ts
βββ viewRenderer.ts
βββ views
βββ swag-dailymotion
βββ swag-dailymotion-config.ts
βββ swag-dailymotion-element.ts
βββ swag-dailymotion-preview.ts
```
## Initial loading of components
Everything starts in the `main.ts` file:
```js
import 'regenerator-runtime/runtime';
import { location } from '@shopware-ag/meteor-admin-sdk';
// Only execute extensionSDK commands when
// it is inside a iFrame (only needed for plugins)
if (location.isIframe()) {
if (location.is(location.MAIN_HIDDEN)) {
// Execute the base commands
import('./base/mainCommands');
} else {
// Render different views
import('./viewRenderer');
}
}
```
This is the main file, which is executed first and functions as the entry point.
Start with `if(location.isIframe())` to make sure only content used inside iFrames is loaded. While the SDK is used in apps and plugins, this check ensures the code is executed in the right place.
Next you need `if(location.is(location.MAIN_HIDDEN))` to **load the main commands**, which are defined in the `mainCommands.ts` file. This will only be used to load logic, but not templates into the Administration.
Lastly, the `else` case will be responsible for specific loading of views via `viewRenderer.ts`. This is where the view templates will be loaded.
### Loading all required templates
Now, create the `viewRenderer.ts` file, which includes the three mandatory files needed for a CMS element as below:
* `swag-dailymotion-config.ts`, which will handle the content of the CMS element configuration
* `swag-dailymotion-element.ts`, which represents the actual target element in the CMS
* `swag-dailymotion-preview.ts`, which is responsible for the preview, when selecting the CMS element in its selection screen
Observe that every file is named according to the component and prefixed with `swag-dailymotion`, (vendor prefix) to ensure no other developer accidentally chooses the same name.
Let us see how the component loading via `viewRenderer.ts` looks like:
```js
import Vue from 'vue';
import { location } from '@shopware-ag/meteor-admin-sdk';
// watch for height changes
location.startAutoResizer();
// start app views
const app = new Vue({
el: '#app',
data() {
return { location };
},
components: {
'SwagDailymotionElement':
() => import('./views/swag-dailymotion/swag-dailymotion-element'),
'SwagDailymotionConfig':
() => import('./views/swag-dailymotion/swag-dailymotion-config'),
'SwagDailymotionPreview':
() => import('./views/swag-dailymotion/swag-dailymotion-preview'),
},
template: `
`,
});
```
Really straightforward, isn't it? As you probably know from Vue.js's Options API, you just need to load, register and use the Vue.js component to make them work.
What's especially interesting here is the use of the `location` object. This is a main concept of the Meteor Admin SDK, where Shopware provides dedicated `locationIds` to offer you places to inject your templates into. For further information on that, it is recommend to take a look at the documentation of the [Meteor Admin SDK](https://shopware.github.io/meteor-admin-sdk/docs/guide/concepts/locations) to learn more about its concepts.
In your case, we will get your own **auto-generated** `locationIds`, depending on the name of your CMS element and suffixes, such as `-element`, `-config`, and `-preview`.
Those will be available after **registering the component**, which we will do in the following chapter.
## Registering a new element
For this topic we head to `mainCommands.ts`, since the registration of CMS elements is something to be done in a global scope.
```js
import { cms } from '@shopware-ag/meteor-admin-sdk';
const CMS_ELEMENT_NAME = 'swag-dailymotion';
const CONSTANTS = {
CMS_ELEMENT_NAME,
PUBLISHING_KEY: `${CMS_ELEMENT_NAME}__config-element`,
};
void cms.registerCmsElement({
name: CONSTANTS.CMS_ELEMENT_NAME,
label: 'Dailymotion video',
defaultConfig: {
dailyUrl: {
source: 'static',
value: '',
},
},
});
export default CONSTANTS;
```
At first, you import the Meteor Admin SDK's cms object, used for `cms.registerCmsElement` to register a new element.
That is all about what is required to register your CMS element. As a best practice, it is recommended to create a **constant** for the CMS element name and the publishing key. This makes it easier to maintain and keep track of changes. The publishing key can be predefined since the name must be a combination of CMS element name and the `__config-element` suffix as shown above.
## Templates and communication with the Administration
The last files are the components inside our `views` folder. Just like you know it from typical CMS element loading, we will create a folder with the full component name, containing 3 files as shown below:
```bash
// /src/Resources/app/administration/src
views
βββ swag-dailymotion
βββ swag-dailymotion-config.ts
βββ swag-dailymotion-element.ts
βββ swag-dailymotion-preview.ts
```
You can vary the structure of `swag-dailymotion`'s contents and create folders for each of the three. However, let us keep it simple with single file components.
### The config file
Let's go through each of the files to talk about it's contents, starting with `swag-dailymotion-config.ts`:
```js
import Vue from 'vue'
import { data } from "@shopware-ag/meteor-admin-sdk";
import CONSTANTS from "../../base/mainCommands";
export default Vue.extend({
template: `
Config!
Video-Code:
`,
data(): Object {
return {
element: null
}
},
computed: {
dailyUrl: {
get(): string {
return this.element?.config?.dailyUrl?.value || '';
},
set(value: string): void {
this.element.config.dailyUrl.value = value;
data.update({
id: CONSTANTS.PUBLISHING_KEY,
data: this.element,
});
}
}
},
created() {
this.createdComponent();
},
methods: {
async createdComponent() {
this.element = await data.get({ id: CONSTANTS.PUBLISHING_KEY });
}
}
});
```
This file is the config component used to define every type of configuration for the CMS element. Most of the code will be common for experienced Shopware 6 developers, so here are some important highlights:
* Import `data` from the Meteor Admin SDK, which is required for data handling between this app and Shopware
* The `element` variable contains the typical CMS element object and is also used to manage the element configuration you want to edit
* The `publishingKey` is used to tell the Meteor Admin SDK in Shopware what piece of information you want to fetch. In this case, you need the `element` data
So, now you need a simple input field to get a `dailyUrl` for the Dailymotion video to be displayed. For that, first fetch the element via `data.get()` as seen in `createdComponent` and then link it to the computed property `dailyUrl` with getters and setters to mutate it. Using `data.update({ id, data })` you provide the publishing key `id` as a target and `data` for the data you want to save in Shopware.
With these small additions to typical CMS element behavior, you have already done with the config modal.

### The element file
Now let's have a look at the result of `swag-dailymotion-element.ts`:
```js
import Vue from 'vue'
import { data } from "@shopware-ag/meteor-admin-sdk";
import CONSTANTS from "../../base/mainCommands";
export default Vue.extend({
template: `
Element!
`,
data(): { element: object|null } {
return {
element: null
}
},
computed: {
dailyUrl(): string {
return `https://www.dailymotion.com/embed/video/${this.element?.config?.dailyUrl?.value || ''}`;
}
},
created() {
this.createdComponent();
},
methods: {
async createdComponent() {
this.element = await data.get({ id: CONSTANTS.PUBLISHING_KEY });
data.subscribe(CONSTANTS.PUBLISHING_KEY, this.elementSubscriber);
},
elementSubscriber(response: { data: unknown, id: string }): void {
this.element = response.data;
}
}
});
```
Here, you have the main rendering logic for the Administration's CMS element. This file shows what your element will look like when it's done. So besides a template and the computed `dailyUrl`, used to correctly load the Dailymotion video player, the only interesting part is the `createdComponent` method.
It initially fetches the `element` data, as you've already seen it in the config file. After that, using `data.subscribe(id, method)` it subscribes to the publishing key, which will update the element data automatically if something changes. It doesn't matter if the changes originate from our config modal outside Shopware or from somewhere else inside Shopware.

### The preview file
Lastly, have a look at `swag-dailymotion-preview.ts`. In most cases, not much logic is to be found here, since this is the preview loaded when choosing a CMS element for your block. It makes sense to show an example preview, a miniature skeleton of the result, or just the Dailymotion logo. Therefore, the following code will suffice for your example extension:
```js
import Vue from 'vue'
export default Vue.extend({
template: `
Preview!
`,
});
```

## Storefront implementation
After everything for the admin is done, there is still the need for a storefront representation of your blocks. This works similarly to typical plugin development, with an exception to the path. All Storefront templates must match the following path pattern: `/Resources/views/storefront/element/.html.twig`
Since everything is already described in guide [CMS element development for plugins](/docs/guides/plugins/plugins/content/cms/add-cms-element#storefront-implementation), the following example just shows how your Storefront template (`swag-daiΔΊymotion/Resources/views/storefront/element/cms-element-swag-dailymotion.html.twig`) could look like:
```twig
{% block element_swag_dailymotion %}
{% block element_dailymotion_image_inner %}
{% endblock %}
{% endblock %}
```
---
---
url: /docs/v6.5/guides/plugins/plugins/content/cms/add-cms-element.md
---
# Add CMS Element
## Overview
This article will teach you how to create a new CMS element via plugin. The plugin in this example will be named `SwagBasicExample`, similar to the other guides.
## Prerequisites
You won't learn how to create a plugin in this guide, head over to our Plugin base guide to create your first plugin.
This guide will also not explain how a custom component can be created in general, so head over to the official guide about creating a custom component to learn this first.
## Creating your custom element
Imagine you want to create a new element to display a Dailymotion video. The shop manager can configure the link of the video to be shown. That's exactly what you're going to build in this guide.
Creating a new element requires you to extend the Administration.
### Injecting into the Administration
The main entry point to customize the Administration via plugin is the `main.js` file. It has to be placed into a `/src/Resources/app/administration/src` directory in order to be automatically found by the Shopware platform.
## Registering a new element
Your plugin's structure should always match the core's structure. When thinking about creating a new element, it's a recommendation to recreate the file tree like in the core for your plugin. Thus, recreate this structure in your plugin: `/src/Resources/app/administration/src/module/sw-cms/elements`
In there, you create a directory for each new element you want to create. In this example a directory `dailymotion` is created.
Now create a new file `index.js` inside the `dailymotion` directory, since it will be loaded when importing this element in your `main.js`. Speaking of that, right after having created the `index.js` file, you can actually import your new element's directory in the `main.js` file already:
```javascript
// /src/Resources/app/administration/src/main.js
import './module/sw-cms/elements/dailymotion';
```
Now open up your empty `dailymotion/index.js` file. In order to register a new element to the system, you have to call the method `registerCmsElement` of the [cmsService](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/service/cms.service.js). Since it's available in the Dependency Injection Container, you can fetch it from there.
First of all, access our `Application` wrapper, which will grant you access to the DI container. So go ahead and fetch the `cmsService` from it and call the mentioned `registerCmsElement` method.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
Shopware.Service('cmsService').registerCmsElement();
```
The method `registerCmsElement` takes a configuration object, containing the following necessary data:
| Key | Description |
| :--- | :--- |
| name | The technical name of your element. Will be used for the template loading later on. |
| label | A name to be shown for your element in the User Interface. Preferably as a snippet key. |
| component | The Vue component to be used when rendering your actual element in the Administration. |
| configComponent | The Vue component defining the "configuration detail" page of your element. |
| previewComponent | The Vue component to be used in the "list of available elements". Just shows a tiny preview of what your element would look like if it was used. |
| defaultConfig | A default configuration to be applied to this element. Must be an object containing properties matching the used variable names, containing the default values. |
| hidden (optional) | Hides the element in the replace element modal. |
| removable (optional) | Removes the replace element icon. |
Go ahead and create this configuration object yourself. Here's what it should look like after having set all of those options:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
Shopware.Service('cmsService').registerCmsElement({
name: 'dailymotion',
label: 'sw-cms.elements.customDailymotionElement.label',
component: 'sw-cms-el-dailymotion',
configComponent: 'sw-cms-el-config-dailymotion',
previewComponent: 'sw-cms-el-preview-dailymotion',
defaultConfig: {
dailyUrl: {
source: 'static',
value: ''
}
}
});
```
The property name does not require further explanation. However, you need to create a snippet file in your plugin directory for the label property.
To do this, create a folder with the name snippet in your `sw-cms` folder. After that, create the files for the languages, e.g. `de-DE.json` and `en-GB.json`. The content of your snippet file should look something like this:
```json
{
"sw-cms": {
"elements": {
"customDailymotionElement": {
"label": "Dailymotion video"
}
}
}
}
```
To learn more about adding own snippets, please refer to [Add snippets to Administration](../../administration/adding-snippets) for more information.
For all three fields `component`, `configComponent` and `previewComponent`, components that do not *yet* exist were applied. Those will be created in the next few steps as well. The `defaultConfig` defines the default values for the element's configuration. There will be a text field to enter a Dailymotion video ID called `dailyUrl`.
Now you have to create the three missing components, let's start with the preview component.
## Building the preview
Create a new directory preview in your element's directory dailymotion. In there, create a new file `index.js`, just like for all components. Then register your component, using the `Shopware.Component` wrapper:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/preview/index.js
import template from './sw-cms-el-preview-dailymotion.html.twig';
import './sw-cms-el-preview-dailymotion.scss';
Shopware.Component.register('sw-cms-el-preview-dailymotion', {
template
});
```
Just like most components, it has a custom template and some styles. Focus on the template first, create a new file `sw-cms-el-preview-dailymotion.html.twig`.
So, for instance, if you want to show the default 'mountain' preview image as an example, then copy it from `/public/bundles/administration/static/img/cms/preview_mountain_small.jpg` to your static folder. You can also replace it with something of your own. Additionally, you can place icons `multicolor-action-play`. Head over to [icon library](https://component-library.shopware.com/icons/) to access them.
That means: You'll need a container to contain both the image and the icon. In there, you create an `img` tag and use the [sw-icon component](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/app/component/base/sw-icon/index.js) to display the icon.
```twig
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/preview/sw-cms-el-preview-dailymotion.html.twig
{% block sw_cms_element_dailymotion_preview %}
{% endblock %}
```
The icon would now be displayed beneath the image, so let's add some styles for this by creating the file `sw-cms-el-preview-dailymotion.scss`.
The container needs to have a `position: relative;` style. This is necessary, so the child can be positioned absolutely and will do so relative to the container's position. Thus, the icon receives a `position: absolute; style`, plus some top and left values to center it.
```css
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/preview/sw-cms-el-preview-dailymotion.scss
.sw-cms-el-preview-dailymotion {
position: relative;
.sw-cms-el-preview-dailymotion-img {
display: block;
max-width: 100%;
}
.sw-cms-el-preview-dailymotion-icon {
$icon-height: 50px;
$icon-width: $icon-height;
position: absolute;
height: $icon-height;
width: $icon-width;
left: calc(50% - #{$icon-width/2});
top: calc(50% - #{$icon-height/2});
}
}
```
The centered positioning will be done by translating the elements by 50% via `top` and `left` properties. Since that would be 50% from the upper left corner of the icon, this wouldn't really center the icon yet. Subtract the half of the icon's width and height and then you're fine.
One last thing: Import your preview component in your element's `index.js` file, so it's loaded.
## Rendering the component
The next would be the main component `sw-cms-el-dailymotion`, the one to be rendered when the shop manager actually decided to use your element by clicking on the preview. Now, you want to show the actually configured video here now. Start with the basic again, create a new directory `component`, in there a new file `index.js` and then register your component `sw-cms-el-dailymotion`.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/index.js
import template from './sw-cms-el-dailymotion.html.twig';
import './sw-cms-el-dailymotion.scss';
Shopware.Component.register('sw-cms-el-dailymotion', {
template
});
```
In addition, create the template file `sw-cms-el-dailymotion.html.twig` and the `.scss` file `sw-cms-el-dailymotion.scss`.
The template doesn't have to include a lot. Having a look at how Dailymotion video embedding works, you just have to add an `iframe` with a src attribute pointing to the video.
```twig
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/sw-cms-el-dailymotion.html.twig
{% block sw_cms_element_dailymotion %}
{% endblock %}
```
You can't just use a static `src` here, since the shop manager wants to configure the video he wants to show. Thus, we're fetching that link via VueJS now.
Let's add the code to provide the src for the iframe. For this case you're going to use a [computed property](https://vuejs.org/v2/guide/computed.html).
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/index.js
import template from './sw-cms-el-dailymotion.html.twig';
import './sw-cms-el-dailymotion.scss';
Shopware.Component.register('sw-cms-el-dailymotion', {
template,
computed: {
dailyUrl() {
return `https://www.dailymotion.com/embed/video/${this.element.config.dailyUrl.value}`;
}
},
});
```
The link being used has to follow this pattern: `https://www.dailymotion.com/embed/video/`, so the only variable you need from the shop manager is the video ID. That's what you're doing here - you're building the link like mentioned above and you add the value of `dailyUrl` from the config. This value will be provided by the config component, that you're going to create in the next step.
In order for this to work though, you have to call the method `initElementConfig` from the `cms-element` mixin. This will take care of dealing with the `configComponent` and therefore providing the configured values.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/index.js
import template from './sw-cms-el-dailymotion.html.twig';
import './sw-cms-el-dailymotion.scss';
Shopware.Component.register('sw-cms-el-dailymotion', {
template,
mixins: [
'cms-element'
],
computed: {
dailyUrl() {
return `https://www.dailymotion.com/embed/video/${this.element.config.dailyUrl.value}`;
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.initElementConfig('dailymotion');
}
}
});
```
Now, the method `initElementConfig` is immediately executed once this component is created.
Time to add the last remaining part of this component: The styles to be applied. Since Dailymotion takes care of responsive layouts itself, you just have to scale the iFrame to 100% width and 100% height. Yet, there's a recommended `min-height` of 315px, so add that one as well.
```css
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/sw-cms-el-dailymotion.scss
.sw-cms-el-dailymotion {
height: 100%;
width: 100%;
min-height: 315px;
.sw-cms-el-dailymotion-iframe-wrapper {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
iframe {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
overflow: hidden
}
}
}
```
That's it for this component! Import it in your element's `index.js` file.
## The configuration
Let's head over to the last remaining component. Create a directory `config`, an `index.js` file in there and register your config component `sw-cms-el-config-dailymotion`.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/config/index.js
import template from './sw-cms-el-config-dailymotion.html.twig';
Shopware.Component.register('sw-cms-el-config-dailymotion', {
template,
mixins: [
'cms-element'
],
computed: {
dailyUrl: {
get() {
return this.element.config.dailyUrl.value;
},
set(value) {
this.element.config.dailyUrl.value = value;
}
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.initElementConfig('dailymotion');
},
onElementUpdate(value) {
this.element.config.dailyUrl.value = value;
this.$emit('element-update', this.element);
}
}
});
```
Just like always, it comes with a template, no styles necessary here though. Create the template file now. Also, the `initElementConfig` method has to be called in here as well, just the same way you've done it in your main component. A little spoiler: This file will remain like this already, you can close it now.
Open the template `sw-cms-el-config-dailymotion.html.twig` instead. To be displayed in the config, we just need a text element, so the shop manager can apply a Dailymotion video ID.
```twig
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/config/sw-cms-el-config-dailymotion.html.twig
{% block sw_cms_element_dailymotion_config %}
{% endblock %}
```
The `v-model` takes care of binding the field's values to the values from the config. Don't forget to include your config in your `index.js`:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
import './component';
import './config';
import './preview';
Shopware.Service('cmsService').registerCmsElement({
// ...
});
```
That's it! You could now go ahead and fully test your new element! Install this plugin via `bin/console plugin:install --activate SwagBasicExample`, rebuild the Administration using the following command and then start using your new element in the Administration.
```bash
./bin/build-administration.sh
```
```bash
composer run build:js:admin
```
Of course, the Storefront implementation is still missing, so your element wouldn't be rendered in the Storefront yet.
## Storefront implementation
Just like the CMS blocks, each element's storefront representation is always expected in the directory `platform/src/Storefront/Resources/views/storefront/element`. In there, a twig template named after your custom element is expected, in this case a file named `cms-element-dailymotion.html.twig`.
So go ahead and re-create that structure in your plugin: `/src/Resources/views/storefront/element/`
In there create a new twig template named after your element, so `cms-element-dailymotion.html.twig` that is.
The template for this is super easy though, just like it's been in your main component for the Administration. Just add an iFrame again. Simply apply the same styles like in the Administration, 100% to both height and width that is.
```twig
// platform/src/Storefront/Resources/views/storefront/element/cms-element-dailymotion.html.twig
{% block element_dailymotion %}
{% block element_dailymotion_image_inner %}
{% endblock %}
{% endblock %}
```
The URL is parsed here using the twig variable element, which is automatically available in your element's template.
Once more: That's it! Your element is now fully working! The shop manager can choose your new element in the 'Shopping Experiences' module, he can configure it and even see it being rendered live in the Administration. After saving and applying this layout to e.g. a category, this element will also be rendered into the Storefront.
## Next steps
There are many possibilities to extend Shopware's CMS. If you haven't done so already, consider using your element in a cms block. To learn how to do this, take a look at the guide on [Add custom cms block](add-cms-block).
---
---
url: /docs/v6.6/guides/plugins/apps/administration/add-cms-element-via-admin-sdk.md
---
# Add CMS Element
## Overview
This article will teach you how to create a new CMS element via the Meteor Admin SDK. The plugin in this example will be named `SwagBasicAppCmsElementExample`, similar to the other guides.
## Prerequisites
* Knowledge on the creation of [Plugins](/docs/guides/plugins/plugins/plugin-base-guide) or [Apps](/docs/guides/plugins/apps/app-base-guide)
* Knowledge on the [creation of custom admin components](/docs/guides/plugins/plugins/administration/add-custom-component#creating-a-custom-component)
* Understanding the [Meteor Admin SDK](/resources/admin-extension-sdk/getting-started/installation)
::: info
This example uses TypeScript, which is recommended, but not required for developing Shopware.
:::
## Creating your custom element
Similar to [Creating a new custom element via plugin](/docs/guides/plugins/plugins/content/cms/add-cms-element#creating-your-custom-element), this article describes creating a new custom element via app.
Creating a new element requires Meteor Admin SDK.
Consider the same scenario to allow a shop manager configure a link to display the Dailymotion video. That is exactly what you are going to build.
### Target structure
You can decide what approach to use when creating apps since everything here is loaded via iFrame. However, Shopware's best practice is a full Vue.js approach.
When our extension is finished, you will get the following file structure:
```bash
// /src/Resources/app/administration/src
βββ base
βΒ Β βββ mainCommands.ts
βββ main.ts
βββ viewRenderer.ts
βββ views
βββ swag-dailymotion
βββ swag-dailymotion-config.ts
βββ swag-dailymotion-element.ts
βββ swag-dailymotion-preview.ts
```
## Initial loading of components
Everything starts in the `main.ts` file:
```javascript
// Prior to 6.7
import 'regenerator-runtime/runtime';
import { location } from '@shopware-ag/meteor-admin-sdk';
// Only execute extensionSDK commands when
// it is inside an iFrame
if (location.isIframe()) {
if (location.is(location.MAIN_HIDDEN)) {
// Execute the base commands
import('./base/mainCommands');
} else {
// Render different views
import('./viewRenderer');
}
}
```
```javascript
// 6.7 and above (inside meteor-app folder)
import 'regenerator-runtime/runtime';
import { location } from '@shopware-ag/meteor-admin-sdk';
if (location.is(location.MAIN_HIDDEN)) {
// Execute the base commands
import('./base/mainCommands');
} else {
// Render different views
import('./viewRenderer');
}
```
This is the main file, which is executed first and functions as the entry point.
Use `if(location.is(location.MAIN_HIDDEN))` to **load the main commands**, which are defined in the `mainCommands.ts` file. This will only be used to load logic, but not templates into the Administration.
Lastly, the `else` case will be responsible for specific loading of views via `viewRenderer.ts`. This is where the view templates will be loaded.
### Loading all required templates
Now, create the `viewRenderer.ts` file, which includes the three mandatory files needed for a CMS element as below:
* `swag-dailymotion-config.ts`, which will handle the content of the CMS element configuration
* `swag-dailymotion-element.ts`, which represents the actual target element in the CMS
* `swag-dailymotion-preview.ts`, which is responsible for the preview, when selecting the CMS element in its selection screen
Observe that every file is named according to the component and prefixed with `swag-dailymotion`, (vendor prefix) to ensure no other developer accidentally chooses the same name.
Let us see how the component loading via `viewRenderer.ts` looks like:
```javascript
import Vue from 'vue';
import { location } from '@shopware-ag/meteor-admin-sdk';
// watch for height changes
location.startAutoResizer();
// start app views
const app = new Vue({
el: '#app',
data() {
return { location };
},
components: {
'SwagDailymotionElement':
() => import('./views/swag-dailymotion/swag-dailymotion-element'),
'SwagDailymotionConfig':
() => import('./views/swag-dailymotion/swag-dailymotion-config'),
'SwagDailymotionPreview':
() => import('./views/swag-dailymotion/swag-dailymotion-preview'),
},
template: `
`,
});
```
Really straightforward, isn't it? As you probably know from Vue.js's Options API, you just need to load, register and use the Vue.js component to make them work.
What's especially interesting here is the use of the `location` object. This is a main concept of the Meteor Admin SDK, where Shopware provides dedicated `locationIds` to offer you places to inject your templates into. For further information on that, it is recommend to take a look at the documentation of the [Meteor Admin SDK](/resources/admin-extension-sdk/concepts/locations) to learn more about its concepts.
In your case, we will get your own **auto-generated** `locationIds`, depending on the name of your CMS element and suffixes, such as `-element`, `-config`, and `-preview`.
Those will be available after **registering the component**, which we will do in the following chapter.
## Registering a new element
For this topic we head to `mainCommands.ts`, since the registration of CMS elements is something to be done in a global scope.
```javascript
import { cms } from '@shopware-ag/meteor-admin-sdk';
const CMS_ELEMENT_NAME = 'swag-dailymotion';
const CONSTANTS = {
CMS_ELEMENT_NAME,
PUBLISHING_KEY: `${CMS_ELEMENT_NAME}__config-element`,
};
void cms.registerCmsElement({
name: CONSTANTS.CMS_ELEMENT_NAME,
label: 'Dailymotion video',
defaultConfig: {
dailyUrl: {
source: 'static',
value: '',
},
},
});
export default CONSTANTS;
```
At first, you import the Meteor Admin SDK's cms object, used for `cms.registerCmsElement` to register a new element.
That is all about what is required to register your CMS element. As a best practice, it is recommended to create a **constant** for the CMS element name and the publishing key. This makes it easier to maintain and keep track of changes. The publishing key can be predefined since the name must be a combination of CMS element name and the `__config-element` suffix as shown above.
## Templates and communication with the Administration
The last files are the components inside our `views` folder. Just like you know it from typical CMS element loading, we will create a folder with the full component name, containing 3 files as shown below:
```bash
// /src/Resources/app/administration/src
views
βββ swag-dailymotion
βββ swag-dailymotion-config.ts
βββ swag-dailymotion-element.ts
βββ swag-dailymotion-preview.ts
```
You can vary the structure of `swag-dailymotion`'s contents and create folders for each of the three. However, let us keep it simple with single file components.
### The config file
Let's go through each of the files to talk about it's contents, starting with `swag-dailymotion-config.ts`:
```javascript
import Vue from 'vue'
import { data } from "@shopware-ag/meteor-admin-sdk";
import CONSTANTS from "../../base/mainCommands";
export default Vue.extend({
template: `
Config!
Video-Code:
`,
data(): Object {
return {
element: null
}
},
computed: {
dailyUrl: {
get(): string {
return this.element?.config?.dailyUrl?.value || '';
},
set(value: string): void {
this.element.config.dailyUrl.value = value;
data.update({
id: CONSTANTS.PUBLISHING_KEY,
data: this.element,
});
}
}
},
created() {
this.createdComponent();
},
methods: {
async createdComponent() {
this.element = await data.get({ id: CONSTANTS.PUBLISHING_KEY });
}
}
});
```
This file is the config component used to define every type of configuration for the CMS element. Most of the code will be common for experienced Shopware 6 developers, so here are some important highlights:
* Import `data` from the Meteor Admin SDK, which is required for data handling between this app and Shopware
* The `element` variable contains the typical CMS element object and is also used to manage the element configuration you want to edit
* The `publishingKey` is used to tell the Meteor Admin SDK in Shopware what piece of information you want to fetch. In this case, you need the `element` data
So, now you need a simple input field to get a `dailyUrl` for the Dailymotion video to be displayed. For that, first fetch the element via `data.get()` as seen in `createdComponent` and then link it to the computed property `dailyUrl` with getters and setters to mutate it. Using `data.update({ id, data })` you provide the publishing key `id` as a target and `data` for the data you want to save in Shopware.
With these small additions to typical CMS element behavior, you have already done with the config modal.

### The element file
Now let's have a look at the result of `swag-dailymotion-element.ts`:
```javascript
import Vue from 'vue'
import { data } from "@shopware-ag/meteor-admin-sdk";
import CONSTANTS from "../../base/mainCommands";
export default Vue.extend({
template: `
Element!
`,
data(): { element: object|null } {
return {
element: null
}
},
computed: {
dailyUrl(): string {
return `https://www.dailymotion.com/embed/video/${this.element?.config?.dailyUrl?.value || ''}`;
}
},
created() {
this.createdComponent();
},
methods: {
async createdComponent() {
this.element = await data.get({ id: CONSTANTS.PUBLISHING_KEY });
data.subscribe(CONSTANTS.PUBLISHING_KEY, this.elementSubscriber);
},
elementSubscriber(response: { data: unknown, id: string }): void {
this.element = response.data;
}
}
});
```
Here, you have the main rendering logic for the Administration's CMS element. This file shows what your element will look like when it's done. So besides a template and the computed `dailyUrl`, used to correctly load the Dailymotion video player, the only interesting part is the `createdComponent` method.
It initially fetches the `element` data, as you've already seen it in the config file. After that, using `data.subscribe(id, method)` it subscribes to the publishing key, which will update the element data automatically if something changes. It doesn't matter if the changes originate from our config modal outside Shopware or from somewhere else inside Shopware.

### The preview file
Lastly, have a look at `swag-dailymotion-preview.ts`. In most cases, not much logic is to be found here, since this is the preview loaded when choosing a CMS element for your block. It makes sense to show an example preview, a miniature skeleton of the result, or just the Dailymotion logo. Therefore, the following code will suffice for your example extension:
```javascript
import Vue from 'vue'
export default Vue.extend({
template: `
Preview!
`,
});
```

## Storefront implementation
After everything for the admin is done, there is still the need for a storefront representation of your blocks. This works similarly to typical plugin development, with an exception to the path. All Storefront templates must match the following path pattern: `/Resources/views/storefront/element/.html.twig`
Since everything is already described in guide [CMS element development for plugins](/docs/guides/plugins/plugins/content/cms/add-cms-element#storefront-implementation), the following example just shows how your Storefront template (`swag-daiΔΊymotion/Resources/views/storefront/element/cms-element-swag-dailymotion.html.twig`) could look like:
```twig
{% block element_swag_dailymotion %}
{% block element_dailymotion_image_inner %}
{% endblock %}
{% endblock %}
```
---
---
url: /docs/v6.6/guides/plugins/plugins/content/cms/add-cms-element.md
---
# Add CMS Element
## Overview
This article will teach you how to create a new CMS element via plugin.
The plugin in this example will be named `SwagBasicExample`, similar to the other guides.
## Prerequisites
You won't learn how to create a plugin in this guide, head over to our Plugin base guide to create your first plugin.
This guide will also not explain how a custom component can be created in general, so head over to the official guide about creating a custom component to learn this first.
## Creating your custom element
Imagine you want to create a new element to display a Dailymotion video.
The shop managers can configure the link of the video to be shown. That's exactly what you're going to build in this guide.
Creating a new element requires you to extend the Administration.
### Injecting into the Administration
The main entry point to customize the Administration via plugin is the `main.js` file.
It has to be placed into a `/src/Resources/app/administration/src` directory in order to be automatically found by the Shopware platform.
## Registering a new element
Your plugin's structure should always match the core's structure.
When thinking about creating a new element, it's a recommendation to recreate the file tree like in the core for your plugin.
Thus, recreate this structure in your plugin: `/src/Resources/app/administration/src/module/sw-cms/elements`
In there, you create a directory for each new element you want to create. In this example a directory `dailymotion` is created.
Now create a new file `index.js` inside the `dailymotion` directory, since it will be loaded when importing this element in your `main.js`.
Speaking of that, right after having created the `index.js` file, you can actually import your new element's directory in the `main.js` file already:
```javascript
// /src/Resources/app/administration/src/main.js
import './module/sw-cms/elements/dailymotion';
```
Now open up your empty `dailymotion/index.js` file.
In order to register a new element to the system, you have to call the method `registerCmsElement` of the [cmsService](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/module/sw-cms/service/cms.service.js).
Since it's available in the Dependency Injection Container, you can fetch it from there.
First of all, access our `Application` wrapper, which will grant you access to the DI container.
So go ahead and fetch the `cmsService` from it and call the mentioned `registerCmsElement` method.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
Shopware.Service('cmsService').registerCmsElement();
```
The method `registerCmsElement` takes a configuration object, containing the following necessary data:
| Key | Description |
|:---------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The technical name of your element. Will be used for the template loading later on. |
| label | A name to be shown for your element in the User Interface. Preferably as a snippet key. |
| component | The Vue component to be used when rendering your actual element in the Administration. |
| configComponent | The Vue component defining the "configuration detail" page of your element. |
| previewComponent | The Vue component to be used in the "list of available elements". Just shows a tiny preview of what your element would look like if it was used. |
| defaultConfig | A default configuration to be applied to this element. Must be an object containing properties matching the used variable names, containing the default values. |
| hidden (optional) | Hides the element in the replace element modal. |
| removable (optional) | Removes the replace element icon. |
Go ahead and create this configuration object yourself.
Here's what it should look like after having set all of those options:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
Shopware.Service('cmsService').registerCmsElement({
name: 'dailymotion',
label: 'sw-cms.elements.customDailymotionElement.label',
component: 'sw-cms-el-dailymotion',
configComponent: 'sw-cms-el-config-dailymotion',
previewComponent: 'sw-cms-el-preview-dailymotion',
defaultConfig: {
dailyUrl: {
source: 'static',
value: ''
}
}
});
```
The property name does not require further explanation.
However, you need to create a snippet file in your plugin directory for the label property.
To do this, create a folder with the name snippet in your `sw-cms` folder.
After that, create the files for the languages, e.g. `de-DE.json` and `en-GB.json`.
The content of your snippet file should look something like this:
```json
{
"sw-cms": {
"elements": {
"customDailymotionElement": {
"label": "Dailymotion video"
}
}
}
}
```
To learn more about adding own snippets, please refer to [Add snippets to Administration](../../administration/adding-snippets) for more information.
For all three fields `component`, `configComponent` and `previewComponent`, components that do not *yet* exist were applied.
Those will be created in the next few steps as well. The `defaultConfig` defines the default values for the element's configuration.
There will be a text field to enter a Dailymotion video ID called `dailyUrl`.
Now you have to create the three missing components, let's start with the preview component.
## Building the preview
Create a new directory preview in your element's directory dailymotion. In there, create a new file `index.js`, just like for all components.
Then register your component, using the `Shopware.Component` wrapper:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/preview/index.js
import template from './sw-cms-el-preview-dailymotion.html.twig';
import './sw-cms-el-preview-dailymotion.scss';
Shopware.Component.register('sw-cms-el-preview-dailymotion', {
template
});
```
Just like most components, it has a custom template and some styles.
Focus on the template first, create a new file `sw-cms-el-preview-dailymotion.html.twig`.
So, for instance, if you want to show the default 'mountain' preview image as an example, then copy it from `/public/bundles/administration/static/img/cms/preview_mountain_small.jpg` to your static folder.
You can also replace it with something of your own. Additionally, you can place icons `multicolor-action-play`.
Head over to [icon library](https://component-library.shopware.com/icons/) to access them.
That means: You'll need a container to contain both the image and the icon.
In there, you create an `img` tag and use the [sw-icon component](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Administration/Resources/app/administration/src/app/component/base/sw-icon/index.js) to display the icon.
```twig
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/preview/sw-cms-el-preview-dailymotion.html.twig
{% block sw_cms_element_dailymotion_preview %}
{% endblock %}
```
The icon would now be displayed beneath the image, so let's add some styles for this by creating the file `sw-cms-el-preview-dailymotion.scss`.
The container needs to have a `position: relative;` style.
This is necessary, so the child can be positioned absolutely and will do so relative to the container's position.
Thus, the icon receives a `position: absolute;` style, plus some top and left values to center it.
```css
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/preview/sw-cms-el-preview-dailymotion.scss
.sw-cms-el-preview-dailymotion {
position: relative;
.sw-cms-el-preview-dailymotion-img {
display: block;
max-width: 100%;
}
.sw-cms-el-preview-dailymotion-icon {
$icon-height: 50px;
$icon-width: $icon-height;
position: absolute;
height: $icon-height;
width: $icon-width;
left: calc(50% - #{$icon-width/2});
top: calc(50% - #{$icon-height/2});
}
}
```
The centered positioning will be done by translating the elements by 50% via `top` and `left` properties.
Since that would be 50% from the upper left corner of the icon, this wouldn't really center the icon yet.
Subtract the half of the icon's width and height and then you're fine.
One last thing: Import your preview component in your element's `index.js` file, so it's loaded.
## Rendering the component
The next would be the main component `sw-cms-el-dailymotion`, the one to be rendered when the shop managers actually decided to use your element by clicking on the preview.
Now, you want to show the actually configured video here now.
Start with the basic again, create a new directory `component`, in there a new file `index.js` and then register your component `sw-cms-el-dailymotion`.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/index.js
import template from './sw-cms-el-dailymotion.html.twig';
import './sw-cms-el-dailymotion.scss';
Shopware.Component.register('sw-cms-el-dailymotion', {
template
});
```
In addition, create the template file `sw-cms-el-dailymotion.html.twig` and the `.scss` file `sw-cms-el-dailymotion.scss`.
The template doesn't have to include a lot.
Having a look at how Dailymotion video embedding works, you just have to add an `iframe` with a src attribute pointing to the video.
```twig
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/sw-cms-el-dailymotion.html.twig
{% block sw_cms_element_dailymotion %}
{% endblock %}
```
You can't just use a static `src` here, since the shop managers want to configure the video they want to show.
Thus, we're fetching that link via Vue.js now.
Let's add the code to provide the src for the iframe. For this case you're going to use a [computed property](https://vuejs.org/v2/guide/computed.html).
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/index.js
import template from './sw-cms-el-dailymotion.html.twig';
import './sw-cms-el-dailymotion.scss';
Shopware.Component.register('sw-cms-el-dailymotion', {
template,
computed: {
dailyUrl() {
return `https://www.dailymotion.com/embed/video/${this.element.config.dailyUrl.value}`;
}
},
});
```
The link being used has to follow this pattern: `https://www.dailymotion.com/embed/video/`, so the only variable you need from the shop managers is the video ID.
That's what you're doing here - you're building the link like mentioned above, and you add the value of `dailyUrl` from the config.
This value will be provided by the config component, that you're going to create in the next step.
In order for this to work though, you have to call the method `initElementConfig` from the `cms-element` mixin.
This will take care of dealing with the `configComponent` and therefore providing the configured values.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/index.js
import template from './sw-cms-el-dailymotion.html.twig';
import './sw-cms-el-dailymotion.scss';
Shopware.Component.register('sw-cms-el-dailymotion', {
template,
mixins: [
'cms-element'
],
computed: {
dailyUrl() {
return `https://www.dailymotion.com/embed/video/${this.element.config.dailyUrl.value}`;
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.initElementConfig('dailymotion');
}
}
});
```
Now, the method `initElementConfig` is immediately executed once this component is created.
Time to add the last remaining part of this component: The styles to be applied.
Since Dailymotion takes care of responsive layouts itself, you just have to scale the iFrame to 100% width and 100% height.
Yet, there's a recommended `min-height` of 315px, so add that one as well.
```css
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/component/sw-cms-el-dailymotion.scss
.sw-cms-el-dailymotion {
height: 100%;
width: 100%;
min-height: 315px;
.sw-cms-el-dailymotion-iframe-wrapper {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
iframe {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
overflow: hidden
}
}
}
```
That's it for this component! Import it in your element's `index.js` file.
## The configuration
Let's head over to the last remaining component. Create a directory `config`, an `index.js` file in there and register your config component `sw-cms-el-config-dailymotion`.
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/config/index.js
import template from './sw-cms-el-config-dailymotion.html.twig';
Shopware.Component.register('sw-cms-el-config-dailymotion', {
template,
mixins: [
'cms-element'
],
computed: {
dailyUrl: {
get() {
return this.element.config.dailyUrl.value;
},
set(value) {
this.element.config.dailyUrl.value = value;
}
}
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.initElementConfig('dailymotion');
},
onElementUpdate(value) {
this.element.config.dailyUrl.value = value;
this.$emit('element-update', this.element);
}
}
});
```
Just like always, it comes with a template, no styles necessary here though.
Create the template file now. Also, the `initElementConfig` method has to be called in here as well, just the same way you've done it in your main component.
A little spoiler: This file will remain like this already, you can close it now.
Open the template `sw-cms-el-config-dailymotion.html.twig` instead.
To be displayed in the config, we just need a text element, so the shop managers can apply a Dailymotion video ID.
```twig
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/config/sw-cms-el-config-dailymotion.html.twig
{% block sw_cms_element_dailymotion_config %}
{% endblock %}
```
The `v-model` takes care of binding the field's values to the values from the config.
Don't forget to include your config in your `index.js`:
```javascript
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
import './component';
import './config';
import './preview';
Shopware.Service('cmsService').registerCmsElement({
// ...
});
```
That's it! You could now go ahead and fully test your new element!
Install this plugin via `bin/console plugin:install --activate SwagBasicExample`, rebuild the Administration using the following command and then start using your new element in the Administration.
```bash
./bin/build-administration.sh
```
```bash
composer run build:js:admin
```
Of course, the Storefront implementation is still missing, so your element wouldn't be rendered in the Storefront yet.
## Storefront implementation
Just like the CMS blocks, each element's storefront representation is always expected in the directory `platform/src/Storefront/Resources/views/storefront/element`.
In there, a twig template named after your custom element is expected, in this case a file named `cms-element-dailymotion.html.twig`.
So go ahead and re-create that structure in your plugin: `/src/Resources/views/storefront/element/`
In there create a new twig template named after your element, so `cms-element-dailymotion.html.twig` that is.
The template for this is super easy though, just like it's been in your main component for the Administration.
Just add an iFrame again. Simply apply the same styles as in the Administration, 100% to both height and width that is.
```twig
// platform/src/Storefront/Resources/views/storefront/element/cms-element-dailymotion.html.twig
{% block element_dailymotion %}
{% block element_dailymotion_image_inner %}
{% endblock %}
{% endblock %}
```
The URL is parsed here using the twig variable element, which is automatically available in your element's template.
Once more: That's it! Your element is now fully working!
The shop managers can choose your new element in the 'Shopping Experiences' module, they can configure it and even see it being rendered live in the Administration.
After saving and applying this layout to e.g. a category, this element will also be rendered into the Storefront.
## Next steps
There are many possibilities to extend Shopware's CMS.
If you haven't done so already, consider using your element in a cms block.
To learn how to do this, take a look at the guide on [Add custom cms block](add-cms-block).
---
---
url: /docs/guides/plugins/plugins/content/cms/add-cms-element.md
---
# Add CMS Elements
## Overview
A CMS element in Shopware is the smallest content unit in the Shopping Experience (CMS) system. Understanding the hierarchy helps clarify what elements are.
### CMS Hierarchy
* Page - The top-level container (e.g., category page, shop page, product page)
* Section - Horizontal segments within a page (can be single-column or two-column with sidebar)
* Block - Units that usually span an entire row with custom layout and styling
* Slots - A named container inside a block. Each slot represents a designated area that can hold exactly one CMS element
* **Elements - The actual content primitives (text, image, video, product listing, etc.) placed inside slots**
Elements are the "primitives" in the CMS hierarchy. They have no knowledge of their context and contain minimal markup. Elements are always rendered inside slots of their parent blocks.
**Key concept**: Elements provide the actual content, while blocks define the structure and layout. This separation allows different element types to be placed in the same block slot.
> **Learn more**: For a deeper understanding of the CMS architecture, see the [Shopping Experience fundamental guide](../../../../../concepts/commerce/content/shopping-experiences-cms.md).
## Where to find elements
Elements are added to blocks within the Shopping Experience module:
* Navigate to Content β Shopping Experience
* Create a new layout or edit an existing one
* Add a block to your layout (blocks contain slots)
* Blocks usually contain one or more predefined elements
* Click on the arrow icon on a slot within a block to see available elements
* Select an element to place it in the slot
You can find related element code here:
* Administration: `src/Administration/Resources/app/administration/src/module/sw-cms/elements/`
* Storefront: `src/Storefront/Resources/views/storefront/element/`
* Core: `\Shopware\Core\Content\Cms\SalesChannel\SalesChannelCmsPageLoader::load`
## How to create an element in the Administration
We recommend this structure for CMS elements:
```TEXT
/src/Resources/app/administration/src/
βββ main.js
βββ module/
βββ sw-cms/
βββ elements/
βββ dailymotion/ (element name)
βββ index.js
βββ component/
β βββ index.js
β βββ cms-el-dailymotion.html.twig
β βββ cms-el-dailymotion.scss
βββ config/
β βββ index.js
β βββ cms-el-config-dailymotion.html.twig
βββ preview/
βββ index.js
βββ cms-el-preview-dailymotion.html.twig
βββ cms-el-preview-dailymotion.scss
```
### Step 1: Import your element in main.js
```JS
// /src/Resources/app/administration/src/main.js
import './module/sw-cms/elements/dailymotion';
```
### Step 2: Register the element
```JS
// /src/Resources/app/administration/src/module/sw-cms/elements/dailymotion/index.js
import './component';
import './config';
import './preview';
Shopware.Service('cmsService').registerCmsElement({
name: 'dailymotion',
label: 'cms.elements.dailymotion.label',
component: 'cms-el-dailymotion',
configComponent: 'cms-el-config-dailymotion',
previewComponent: 'cms-el-preview-dailymotion',
defaultConfig: {
url: {
source: 'static',
value: '',
},
},
});
```
| Property | Description |
|------------------|--------------------------------------------------------------------------------------------|
| name | Technical name of your element |
| label | Display name in the UI (preferably as a snippet key) |
| component | Vue component for rendering the element in the Administration |
| configComponent | Vue component for the configuration panel |
| previewComponent | Vue component for the element thumbnail in the element selector |
| defaultConfig | Default configuration values (key = config property, value = object with source and value) |
| hidden | (Optional) Hides the element in the "replace element" modal |
| removable | (Optional) Prevents the element from being removed from a slot via UI |
### Step 3: Create the preview component
The preview is shown as a thumbnail when selecting or swapping elements in block slots. You could also display a static image of your final Storefront element here.
```JS
// dailymotion/preview/index.js
Shopware.Component.register('cms-el-preview-dailymotion', {
template: `
Dailymotion Embed
`,
});
```
### Step 4: Create the main component
The main component is displayed in the editor layout. It should provide a good representation of the final Storefront element.
```JS
// dailymotion/component/index.js
Shopware.Component.register('cms-el-dailymotion', {
template: `
Dailymotion
`,
mixins: [
'cms-element'
],
computed: {
embedUrl() {
return `https://www.dailymotion.com/embed/video/${this.element.config.url.value}`;
},
},
created() {
this.initElementConfig('dailymotion');
},
});
```
**Key points**:
* The `cms-element` mixin provides common props and data-mapping for config objects
* Use fallback content to avoid invisible elements in the editor (for example when missing an `iframe` or `img` `src`)
### Step 5: Create the configuration component
This component will be displayed in a modal and should provide form fields for all options defined in Step 2 (`defaultConfig`).
```JS
// dailymotion/config/index.js
Shopware.Component.register('cms-el-config-dailymotion', {
template: `
`,
mixins: [
'cms-element'
],
created() {
this.initElementConfig('dailymotion');
},
});
```
**Key points**:
* The `cms-element` mixin provides common props and data-mapping for config objects
* Use [Shopware Meteor components](https://shopware.design/meteor-components/) for a consistent UI
### Step 6: Inheritance support for elements
Inheritance in the CMS provides fine-grained control over how content is customized between the base layout and content pages (category, product, landing page, ..) they are assigned to.
Similar to how product variants work, content managers can choose to either inherit the content from the base layout or override it with custom content for each page.
By default, configuration will be inherited unless explicitly overridden though the UI may not be as clear.
For an improved user experience when working with inherited fields, we provide a [wrapper component](https://github.com/shopware/shopware/blob/trunk/src/Administration/Resources/app/administration/src/module/sw-cms/component/sw-cms-inherit-wrapper/index.ts) that handles removing and restoring inherited values and displaying proper UI states. To use this is in your own elements, you can add the `sw-cms-inherit-wrapper` component to individual fields in your element.
```VUE
```
You can find more references in the standard CMS elements located in `src/Administration/Resources/app/administration/src/module/sw-cms/elements/`.
## How to Create an element in the Storefront
The Storefront template defines how your element appears on the actual storefront. It is expected to be located in the directory `src/Resources/views/storefront/element`. In there, a twig template file has to follow this naming convention:
* **Prefix**: `cms-element-`
* **Technical name**: `dailymotion` (the `name` property defined in Step 2)
* **Extension**: `.html.twig`
**Shopware is expecting the prefix as part of the full filename.**
Full example: `cms-element-dailymotion.html.twig`
### Basic template
You can create your own elements or extend and reuse existing ones. Don't forget to clear the Storefront cache after adding new templates.
```TWIG
{# /src/Resources/views/storefront/element/cms-element-dailymotion.html.twig #}
```
The `element` is automatically passed to the template and contains meta data and configuration values. See the `CmsSlotDefinition.php` for a full overview.
## Next steps
There are many possibilities to extend Shopware's CMS.
If you haven't done so already, consider using your element in a cms block.
To learn how to do this, take a look at the guide on [Add custom CMS block](add-cms-block.md).
---
---
url: /docs/guides/plugins/plugins/storefront/advanced/add-cookie-to-manager.md
---
# Add Cookie to Manager
## Overview
Since the GDPR was introduced, every website has to be shipped with some sort of a cookie consent manager. This is also the case for Shopware 6 of course, which comes with a cookie consent manager by default. In this guide you will learn how you can add your own cookies to the cookie consent manager of Shopware 6.
::: info
For a comprehensive understanding of Shopware's cookie consent system, see the [Cookie Consent Management Concept](../../../../../concepts/commerce/content/cookie-consent-management.md).
:::
## Prerequisites
Review the [Plugin base guide](../../plugin-base-guide.md) and create a running plugin. Also, you will need to know how to [create your own service](../../services/add-custom-service.md) and [subscribe to an event](../../framework/event/listening-to-events.md), so you might want to take a look at those guides as well.
## Extend the cookie consent manager
Adding custom cookies requires you to listen to the `CookieGroupCollectEvent` and add your custom cookies to the collection.
::: tip
It is recommended to use an event listener if you're listening to a single event. If you need to react to multiple events, an event subscriber is the better choice.
:::
### Registering your event listener
Start with creating the `services.php` and registering your event listener.
```php
// /src/Resources/config/services.php
services();
$services->set(CookieListener::class)
->tag('kernel.event_listener', ['event' => 'Shopware\Core\Content\Cookie\Event\CookieGroupCollectEvent']);
};
```
In the next step we'll create the actual listener class.
### Creating the listener
We need to create a class called `CookieListener` with an `__invoke` method. This method will be executed once the `CookieGroupCollectEvent` is dispatched.
The event object that is passed to our listener method contains the cookie groups collection, which we can use to add our custom cookies.
::: warning
Since Shopware 6.7.3.0, cookies use structured objects (`CookieEntry` and `CookieGroup`) instead of arrays for better type safety and consistency. The array format is deprecated.
:::
Let's have a look at an example:
```php
// /src/Listener/CookieListener.php
cookieGroupCollection->get(CookieProvider::SNIPPET_NAME_COOKIE_GROUP_COMFORT_FEATURES);
if (!$comfortFeaturesCookieGroup) {
return;
}
$entries = $comfortFeaturesCookieGroup->getEntries();
if ($entries === null) {
$entries = new CookieEntryCollection();
$comfortFeaturesCookieGroup->setEntries($entries);
}
$cookieEntry = new CookieEntry('my-cookie-key');
$cookieEntry->name = 'cookie.myCookieName';
$cookieEntry->value = '1';
$cookieEntry->expiration = 30;
$entries->add($cookieEntry);
}
}
```
This will add your cookie to the existing "Comfort Features" group in the cookie consent manager.
And that's basically it already. After loading your Storefront, you should now see your new cookie in the cookie consent manager.
## Parameter Reference
For a complete list of available parameters and their types, refer to the source code:
* [`CookieEntry`](https://github.com/shopware/shopware/blob/trunk/src/Core/Content/Cookie/Struct/CookieEntry.php) - Individual cookie definition
* [`CookieGroup`](https://github.com/shopware/shopware/blob/trunk/src/Core/Content/Cookie/Struct/CookieGroup.php) - Cookie group definition
::: info
Cookie groups should not have the `cookie`, `value`, `expiration`, or `isRequired` parameters. These only apply to individual `CookieEntry` objects within the group's `entries`.
:::
## Migrating from CookieProviderInterface (Shopware 6.7.2 and earlier)
If you are upgrading from an older version, you might have used the `CookieProviderInterface` to add custom cookies. This interface is now deprecated and should be replaced with the `CookieGroupCollectEvent`.
For backward compatibility, you can still use the `CookieProviderInterface` to provide cookies in the old array syntax. However, it is highly recommended to use the new event-based system to provide the new object structure.
## Cookie Configuration Changes and Re-Consent
Since Shopware 6.7.3.0, cookie configurations include a hash that tracks changes. When you modify cookie configurations through your plugin (add/remove/change cookies), the hash changes automatically, triggering a re-consent flow for users.
This helps maintain transparency by re-prompting users when cookie handling changes, supporting GDPR compliance requirements. The hash is automatically calculated from all cookie configurations provided by the `CookieProvider`.
::: info
**Hash Storage Format**: The configuration hash is stored in the browser's `cookie-config-hash` cookie as an object where the language ID is the key and the cookie hash is the value, for example: `{"019ada128cfb711aa7a0d00f476d5961":"998cdcc090e92b3ecdd057241d0fd01f"}`. This enables per-language consent tracking. Since cookies are stored per domain by the browser, installations using different domains for different languages don't encounter tracking conflicts. The language ID is specifically used when multiple languages are served from the same domain.
:::
::: info
While this feature helps with GDPR compliance, shop owners are responsible for ensuring their overall cookie usage, privacy policies, and data handling practices comply with GDPR and other applicable regulations.
:::
### How it works
1. Your plugin adds/modifies cookies via the `CookieGroupCollectEvent`
2. Shopware calculates a hash of the entire cookie configuration
3. The hash is stored in the user's browser as an object where the language ID is the key and the hash is the value (e.g., `{"019ada128cfb711aa7a0d00f476d5961":"998cdcc090e92b3ecdd057241d0fd01f"}`)
4. On the next visit, if the hash differs for the current language, the consent banner appears again
5. Users are informed about changes and can make new choices
This automatic re-consent mechanism helps shop owners maintain transparency about cookie changes.
::: info
The configuration hash is exposed via the Store API endpoint `/store-api/cookie/groups`. For API documentation, see [Fetch all cookie groups](https://shopware.stoplight.io/docs/store-api/f9c70be044a15-fetch-all-cookie-groups).
:::
## Video Platform Cookies
YouTube and Vimeo cookies are now handled separately in Shopware's cookie management. If you're adding video functionality to your plugin, ensure you register the appropriate cookie for your video platform or reuse existing ones.
## Next steps
Those changes will mainly just show your new cookies in the cookie consent manager, but without much function. Head over to our guide about [Reacting to cookie consent changes](reacting-to-cookie-consent-changes) to see how you can implement your custom logic once your cookie got accepted or declined.
---
---
url: /docs/v6.5/guides/plugins/plugins/storefront/add-cookie-to-manager.md
---
# Add Cookie to Manager
## Overview
Since the GDPR was introduced, every website has to be shipped with some sort of a cookie consent manager. This is also the case for Shopware 6 of course, which comes with a cookie consent manager by default. In this guide you will learn how you can add your own cookies to the cookie consent manager of Shopware 6.
## Prerequisites
This guide is built upon the [Plugin base guide](../plugin-base-guide), so have a look at that first if you're lacking a running plugin. Also you will have to know how to [create your own service](../plugin-fundamentals/add-custom-service) and [decorations](../plugin-fundamentals/adjusting-service#decorating-the-service), so you might want to have a look at those guides as well.
## Extend the cookie consent manager
Adding custom cookies basically requires you to decorate a service, the `CookieProvider` to be precise. Neither decorations, nor adding a service via a `services.xml` is explained here, so make sure to have a look at the previously mentioned guides first, if you're lacking this knowledge.
### Registering your decoration
Start with creating the `services.xml` entry and with decorating the `CookieProviderInterface`. The `CookieProvider` service was already built before we decided to use abstract classes for decorations, so don't be confused here.
```xml
// /src/Resources/config/services.xml
```
In the next step we'll create the actual decorated class.
### Creating the decorated service
We need to create a class called `CustomCookieProvider`, which implements the `CookieProviderInterface`. Our constructor parameter is the original `CookieProviderInterface` instance, which we need to call to get all other cookies as well.
The interface mentioned above requires you to implement a method called `getCookieGroups`, which has to return an array of cookie groups and their respective cookies. You need to call the original method now, receive the default cookie groups and then merge your custom group, if there's any, and your custom cookies into it.
Let's have a look at an example:
```php
// /src/Framework/Cookie/CustomCookieProvider.php
originalService = $service;
}
private const singleCookie = [
'snippet_name' => 'cookie.name',
'snippet_description' => 'cookie.description ',
'cookie' => 'cookie-key',
'value' => 'cookie value',
'expiration' => '30'
];
// cookies can also be provided as a group
private const cookieGroup = [
'snippet_name' => 'cookie.group_name',
'snippet_description' => 'cookie.group_description ',
'entries' => [
[
'snippet_name' => 'cookie.first_child_name',
'cookie' => 'cookie-key-1',
'value'=> 'cookie value',
'expiration' => '30'
],
[
'snippet_name' => 'cookie.second_child_name',
'cookie' => 'cookie-key-2',
'value'=> 'cookie value',
'expiration' => '60'
]
],
];
public function getCookieGroups(): array
{
return array_merge(
$this->originalService->getCookieGroups(),
[
self::cookieGroup,
self::singleCookie
]
);
}
}
```
As already mentioned, we're overwriting the method `getCookieGroups` and in there we're calling the original method first. We then proceed to merge our own custom group into it, as well as a custom cookie.
This will eventually lead to a new group being created, containing two new cookies, as well as a new cookie without a group.
And that's basically it already. After loading your Storefront, you should now see your new cookies and the cookie-group.
### Cookie array keys
Here's a list of attributes, that you can apply to a cookie array:
| Attribute | Data type | Required | Description |
| :--- | :--- | :--- | :--- |
| snippet\_name | String | Yes | Key of a snippet containing the display name of a cookie or cookie group. |
| snippet\_description | String | No | Key of a snippet containing a short description of a cookie or cookie group. |
| cookie | String | Yes | The internal cookie name used to save the cookie. |
| value | String | No | If unset, the cookie will not be updated (set active or inactive) by Shopware, but passed to the update event only. |
| expiration | String | No | Cookie lifetime in days. **If unset, the cookie expires with the session**. |
| entries | Array | No | An array of cookie objects. Used to create grouped cookies. Nested groups are not supported. If using this, **the group itself should not have the attributes** ***cookie*****,** ***value*** **and** ***expiration*****.**. |
## Next steps
Those changes will mainly just show your new cookies in the cookie consent manager, but without much function. Head over to our guide about [Reacting to cookie consent changes](reacting-to-cookie-consent-changes) to see how you can implement your custom logic once your cookie got accepted or declined.
---
---
url: /docs/v6.6/guides/plugins/plugins/storefront/add-cookie-to-manager.md
---
# Add Cookie to Manager
## Overview
Since the GDPR was introduced, every website has to be shipped with some sort of a cookie consent manager. This is also the case for Shopware 6 of course, which comes with a cookie consent manager by default. In this guide you will learn how you can add your own cookies to the cookie consent manager of Shopware 6.
## Prerequisites
This guide is built upon the [Plugin base guide](../plugin-base-guide), so have a look at that first if you're lacking a running plugin. Also you will have to know how to [create your own service](../plugin-fundamentals/add-custom-service) and [decorations](../plugin-fundamentals/adjusting-service#decorating-the-service), so you might want to have a look at those guides as well.
## Extend the cookie consent manager
Adding custom cookies basically requires you to decorate a service, the `CookieProvider` to be precise. Neither decorations, nor adding a service via a `services.xml` is explained here, so make sure to have a look at the previously mentioned guides first, if you're lacking this knowledge.
### Registering your decoration
Start with creating the `services.xml` entry and with decorating the `CookieProviderInterface`. The `CookieProvider` service was already built before we decided to use abstract classes for decorations, so don't be confused here.
```xml
// /src/Resources/config/services.xml
```
In the next step we'll create the actual decorated class.
### Creating the decorated service
We need to create a class called `CustomCookieProvider`, which implements the `CookieProviderInterface`. Our constructor parameter is the original `CookieProviderInterface` instance, which we need to call to get all other cookies as well.
The interface mentioned above requires you to implement a method called `getCookieGroups`, which has to return an array of cookie groups and their respective cookies. You need to call the original method now, receive the default cookie groups and then merge your custom group, if there's any, and your custom cookies into it.
Let's have a look at an example:
```php
// /src/Framework/Cookie/CustomCookieProvider.php
originalService = $service;
}
private const singleCookie = [
'snippet_name' => 'cookie.name',
'snippet_description' => 'cookie.description ',
'cookie' => 'cookie-key',
'value' => 'cookie value',
'expiration' => '30'
];
// cookies can also be provided as a group
private const cookieGroup = [
'snippet_name' => 'cookie.group_name',
'snippet_description' => 'cookie.group_description ',
'entries' => [
[
'snippet_name' => 'cookie.first_child_name',
'cookie' => 'cookie-key-1',
'value'=> 'cookie value',
'expiration' => '30'
],
[
'snippet_name' => 'cookie.second_child_name',
'cookie' => 'cookie-key-2',
'value'=> 'cookie value',
'expiration' => '60'
]
],
];
public function getCookieGroups(): array
{
return array_merge(
$this->originalService->getCookieGroups(),
[
self::cookieGroup,
self::singleCookie
]
);
}
}
```
As already mentioned, we're overwriting the method `getCookieGroups` and in there we're calling the original method first. We then proceed to merge our own custom group into it, as well as a custom cookie.
This will eventually lead to a new group being created, containing two new cookies, as well as a new cookie without a group.
And that's basically it already. After loading your Storefront, you should now see your new cookies and the cookie-group.
### Cookie array keys
Here's a list of attributes, that you can apply to a cookie array:
| Attribute | Data type | Required | Description |
| :--- | :--- | :--- | :--- |
| snippet\_name | String | Yes | Key of a snippet containing the display name of a cookie or cookie group. |
| snippet\_description | String | No | Key of a snippet containing a short description of a cookie or cookie group. |
| cookie | String | Yes | The internal cookie name used to save the cookie. |
| value | String | No | If unset, the cookie will not be updated (set active or inactive) by Shopware, but passed to the update event only. |
| expiration | String | No | Cookie lifetime in days. **If unset, the cookie expires with the session**. |
| entries | Array | No | An array of cookie objects. Used to create grouped cookies. Nested groups are not supported. If using this, **the group itself should not have the attributes** ***cookie*****,** ***value*** **and** ***expiration*****.**. |
## Next steps
Those changes will mainly just show your new cookies in the cookie consent manager, but without much function. Head over to our guide about [Reacting to cookie consent changes](reacting-to-cookie-consent-changes) to see how you can implement your custom logic once your cookie got accepted or declined.
---
---
url: /docs/v6.5/guides/plugins/apps/storefront/cookies-with-apps.md
---
# Add cookies to the consent manager
## Prerequisites
You should be familiar with the concept of apps.
## Create a single cookie
To add new cookies to the cookie consent manager, you can add a `cookies` section to your `manifest.xml`. Inside this section, you can add new `cookie` elements, as shown in the following example. Note that you don't need a `setup` section in your `manifest.xml` since extending the Storefront doesn't need a registration nor an own server to run.
```xml
// manifest.xml
ExampleAppWithCookies1.0.0my-cookieexample-app-with-cookies.my-cookie.nameexample-app-with-cookies.my-cookie.descriptiona static value for the cookie1
```
Cookie elements can be configured by adding the following child elements:
* `cookie` (required): The technical name of the cookie. The value is used to store the cookie in the customer's cookie jar.
* `snippet-name` (required): A string that represents the label of the cookie in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
* `value` (optional): A fixed value that is set as the cookie's value when the customer accepts your cookie. **If unset, the cookie will not be updated (set active or inactive) by Shopware, but passed to the update event.**
* `expiration` (optional): Cookie lifetime in days. **If unset, the cookie expires with the session.**
* `snippet-description` (optional): A string that represents the description of the cookie in the cookie consent manager. To provide translations, this should be the key of a Storefront snippet.
For a complete reference of the structure of the manifest file, take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference).
## Create a cookie group
When adding multiple cookies through your app it may become handy to group them. This makes it possible for the customer to accept all of your cookies at once and additionally enhances the readability of the cookie consent manager.
To add a cookie group, you can add a `groups` section within your `cookies` section in your `manifest.xml`. In the following example, we use the cookie that we created in the previous section but display it in a cookie group:
```xml
// manifest.xml
ExampleAppWithCookies1.0.0example-app-with-cookies.cookie-group.nameexample-app-with-cookies.cookie-group.descriptionmy-cookieexample-app-with-cookies.my-cookie.nameexample-app-with-cookies.my-cookie.descriptiona static value for the cookie1
```
A `group` element consists of three child elements to configure the cookie group. Here is a description of all of them:
* `snippet-name` (required): A string that represents the label of the cookie group in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
* `entries` (required): Contains the grouped cookies. It is a collection of `cookie` elements described in the previous section.
* `snippet-description` (optional): A string that represents the description of the cookie group in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
For a complete reference of the structure of the manifest file, take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference).
## Snippet handling
As already mentioned in the previous sections, both the `cookie` and the `group` elements can contain `snippet-name` and `snippet-description` child elements. Although their values can be strings that will be displayed in the Storefront, the preferred way to set up cookie names and descriptions is to provide Storefront snippets. It gives you and the shop owner the possibility to add translations for your cookie's name and description.
If you are not familiar with setting up Storefront snippets, please refer to our snippet guide.
## Reacting to cookie consent changes
As described in the previous section, `cookie` elements without a `value` element will not be set automatically. Instead, you have to react to cookie consent changes within your JavaScript. Find out how to [respond to cookie consent changes](../../../plugins/plugins/storefront/reacting-to-cookie-consent-changes).
---
---
url: /docs/v6.6/guides/plugins/apps/storefront/cookies-with-apps.md
---
# Add cookies to the consent manager
## Prerequisites
You should be familiar with the concept of apps.
## Create a single cookie
To add new cookies to the cookie consent manager, you can add a `cookies` section to your `manifest.xml`. Inside this section, you can add new `cookie` elements, as shown in the following example. Note that you don't need a `setup` section in your `manifest.xml` since extending the Storefront doesn't need a registration nor an own server to run.
::: code-group
```xml [manifest.xml]
ExampleAppWithCookies1.0.0my-cookieexample-app-with-cookies.my-cookie.nameexample-app-with-cookies.my-cookie.descriptiona static value for the cookie1
```
:::
Cookie elements can be configured by adding the following child elements:
* `cookie` (required): The technical name of the cookie. The value is used to store the cookie in the customer's cookie jar.
* `snippet-name` (required): A string that represents the label of the cookie in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
* `value` (optional): A fixed value that is set as the cookie's value when the customer accepts your cookie. **If unset, the cookie will not be updated (set active or inactive) by Shopware, but passed to the update event.**
* `expiration` (optional): Cookie lifetime in days. **If unset, the cookie expires with the session.**
* `snippet-description` (optional): A string that represents the description of the cookie in the cookie consent manager. To provide translations, this should be the key of a Storefront snippet.
For a complete reference of the structure of the manifest file, take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference).
## Create a cookie group
When adding multiple cookies through your app it may become handy to group them. This makes it possible for the customer to accept all of your cookies at once and additionally enhances the readability of the cookie consent manager.
To add a cookie group, you can add a `groups` section within your `cookies` section in your `manifest.xml`. In the following example, we use the cookie that we created in the previous section but display it in a cookie group:
::: code-group
```xml [manifest.xml]
ExampleAppWithCookies1.0.0example-app-with-cookies.cookie-group.nameexample-app-with-cookies.cookie-group.descriptionmy-cookieexample-app-with-cookies.my-cookie.nameexample-app-with-cookies.my-cookie.descriptiona static value for the cookie1
```
:::
A `group` element consists of three child elements to configure the cookie group. Here is a description of all of them:
* `snippet-name` (required): A string that represents the label of the cookie group in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
* `entries` (required): Contains the grouped cookies. It is a collection of `cookie` elements described in the previous section.
* `snippet-description` (optional): A string that represents the description of the cookie group in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
For a complete reference of the structure of the manifest file, take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference).
## Snippet handling
As already mentioned in the previous sections, both the `cookie` and the `group` elements can contain `snippet-name` and `snippet-description` child elements. Although their values can be strings that will be displayed in the Storefront, the preferred way to set up cookie names and descriptions is to provide Storefront snippets. It gives you and the shop owner the possibility to add translations for your cookie's name and description.
If you are not familiar with setting up Storefront snippets, please refer to our snippet guide.
## Reacting to cookie consent changes
As described in the previous section, `cookie` elements without a `value` element will not be set automatically. Instead, you have to react to cookie consent changes within your JavaScript. Find out how to [respond to cookie consent changes](../../../plugins/plugins/storefront/reacting-to-cookie-consent-changes).
---
---
url: /docs/guides/plugins/apps/storefront/cookies-with-apps.md
---
# Add Cookies to the Consent Manager
## Overview
Before proceeding, review the [App Base Guide](../app-base-guide.md).
The [Cookie Consent Management Concept](../../../../concepts/commerce/content/cookie-consent-management.md) provides a comprehensive guide to Shopware's cookie consent system.
## Create a single cookie
To add new cookies to the cookie consent manager, you can add a `cookies` section to your `manifest.xml`. Inside this section, you can add new `cookie` elements, as shown in the following example. Note that you don't need a `setup` section in your `manifest.xml` since extending the Storefront doesn't need a registration nor an own server to run.
```XML
ExampleAppWithCookies1.0.0my-cookieexample-app-with-cookies.my-cookie.nameexample-app-with-cookies.my-cookie.descriptiona static value for the cookie1
```
Cookie elements can be configured by adding the following child elements:
* `cookie` (required): The technical name of the cookie. The value is used to store the cookie in the customer's cookie jar.
* `snippet-name` (required): A string that represents the label of the cookie in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
* `value` (optional): A fixed value that is set as the cookie's value when the customer accepts your cookie. **If unset, the cookie will not be updated (set active or inactive) by Shopware, but passed to the update event.**
* `expiration` (optional): Cookie lifetime in days. **If unset, the cookie expires with the session.**
* `snippet-description` (optional): A string that represents the description of the cookie in the cookie consent manager. To provide translations, this should be the key of a Storefront snippet.
For a complete reference of the structure of the manifest file, take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference.md).
## Create a cookie group
When adding multiple cookies through your app, it may become handy to group them. This makes it possible for the customer to accept all of your cookies at once and additionally enhances the readability of the cookie consent manager.
To add a cookie group, you can add a `groups` section within your `cookies` section in your `manifest.xml`. In the following example, we use the cookie that we created in the previous section but display it in a cookie group:
```XML
ExampleAppWithCookies1.0.0example-app-with-cookies.cookie-group.nameexample-app-with-cookies.cookie-group.descriptionmy-cookieexample-app-with-cookies.my-cookie.nameexample-app-with-cookies.my-cookie.descriptiona static value for the cookie1
```
A `group` element consists of three child elements to configure the cookie group. Here is a description of all of them:
* `snippet-name` (required): A string that represents the label of the cookie group in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
* `entries` (required): Contains the grouped cookies. It is a collection of `cookie` elements described in the previous section.
* `snippet-description` (optional): A string that represents the description of the cookie group in the cookie consent manager. To provide translations this should be the key of a Storefront snippet.
The [Manifest reference](../../../../resources/references/app-reference/manifest-reference.md) provides comprehensive information about manifest file structure.
## Assigning Cookies to Standard Cookie Groups
You can assign your app's cookies to Shopware's standard cookie groups by using one of the built-in snippet names in your `manifest.xml`: `cookie.groupRequired`, `cookie.groupComfortFeatures`, `cookie.groupStatistical`, and `cookie.groupMarketing`.
The following example shows how to assign cookies to the **Marketing group**:
```XML
MyApp1.0.0Your Namecookie.groupMarketingmyapp_conversion_trackingmyapp.cookie.conversionTrackingmyapp.cookie.conversionTrackingDescription190myapp_ad_targetingmyapp.cookie.adTargeting1365
```
## Snippet handling
As already mentioned in the previous sections, both the `cookie` and the `group` elements can contain `snippet-name` and `snippet-description` child elements. Although their values can be strings that will be displayed in the Storefront, the preferred way to set up cookie names and descriptions is to provide Storefront snippets. It gives you and the shop owner the possibility to add translations for your cookie's name and description.
To learn how to set up Storefront snippets, refer to the snippet guide.
## Automatic Configuration Change Detection
Any changes made to the cookie definitions in your app's `manifest.xml` are automatically detected by Shopware's consent system. This will trigger a re-consent flow for users, ensuring they are always prompted about the latest cookie settings.
This process is handled by a configuration hash mechanism, which is explained in detail in the [Cookie Consent Management Concept](../../../../concepts/commerce/content/cookie-consent-management.md#configuration-hash-mechanism).
## Reacting to cookie consent changes
As described in the previous section, `cookie` elements without a `value` element will not be set automatically. Instead, you have to react to cookie consent changes within your JavaScript. Find out how to [respond to cookie consent changes](../../../plugins/plugins/storefront/advanced/reacting-to-cookie-consent-changes.md).
---
---
url: /docs/guides/plugins/apps/administration/add-custom-action-button.md
---
# Add custom action button
## Overview
This guide covers how to add custom action buttons to the Shopware Administration using the manifest file. This works for simple applications; however, to write more advanced applications, the [Meteor Admin SDK](meteor-admin-sdk.md) is recommended. It has many more features and is more flexible.
For further details and guidance on custom action buttons, refer to the documentation provided on the Meteor Admin SDK's [action button](https://developer.shopware.com/resources/admin-extension-sdk/api-reference/ui/actionButton.html) section.
One extension possibility in the Administration is the ability to add custom action buttons to the smartbar. For now, you can add them in the smartbar of detail and list views:

To get those buttons, you start in the `admin` section of your manifest file. There you can define `` elements in order to add your button, as seen as below:
::: code-group
```xml [manifest.xml]
...
```
:::
For a complete reference of the structure of the manifest file take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference.md).
An action button must have the following attributes:
* `action`: Unique identifier for the action, can be set freely.
* `entity`: Here you define which entity you're working on.
* `view`: `detail`or `list`; to set the view the button should be added to. Currently, you can choose between detail and listing view.
When the user clicks on the action button your app receives a request similar to the one generated by a [webhook](../lifecycle/webhook.md).
The main difference is that it contains the name of the entity and an array of ids that the user selected (or an array containing only a single id if the action button was executed on a detail page).
A sample payload may look like the following:
```json
{
"source":{
"url":"http:\/\/localhost:8000",
"appVersion":"1.0.0",
"shopId":"F0nWInXj5Xyr"
},
"data":{
"ids":[
"2132f284f71f437c9da71863d408882f"
],
"entity":"product",
"action":"restockProduct"
},
"meta":{
"timestamp":1592403610,
"reference":"9e968471797b4f29be3e3cf09f52d8da",
"language":"2fbb5fe2e29a4d70aa5854ce7ce3e20b"
}
}
```
```php
// injected or build by yourself
$shopResolver = new ShopResolver($repository);
$contextResolver = new ContextResolver();
$shop = $shopResolver->resolveShop($serverRequest);
$actionButton = $contextResolver->assembleActionButton($serverRequest, $shop);
```
```php
use Shopware\App\SDK\Context\ActionButton\ActionButtonAction;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Psr\Http\Message\ResponseInterface;
#[AsController]
class ActionButtonController {
#[Route('/action/product/detail')]
public function handle(ActionButtonAction $button): ResponseInterface
{
// handle button
return ActionButtonResponse::notification('success', 'Success message');
}
}
```
::: info
Starting from Shopware version 6.4.1.0, the current shopware version will be sent as a `sw-version` header.
:::
Again you can verify the authenticity of the incoming request, like with [webhooks](../lifecycle/webhook.md), by checking the `shopware-shop-signature`. It also contains the SHA256 HMAC of the request body, that is signed with the secret your app assigned the shop during the [registration](../lifecycle/app-registration-setup.md#setup).
## Providing feedback in the Administration
::: info
This feature was added in Shopware 6.4.3.0, previous versions will ignore the response content.
:::
::: info
Starting from Shopware version 6.4.8.0, the requests of the [tab](#opening-a-new-tab-for-the-user) and [custom modal](#open-a-custom-modal) have the following additional query parameters:
* `shop-id`
* `shop-url`
* `timestamp`
* `sw-context-language`
* `sw-user-language`
* `shopware-shop-signature`
You **must** make sure to verify the authenticity of the incoming request by checking the `shopware-shop-signature`, which is a hash of the request's query part, signed with the shop's secret key.
:::
If you want to trigger an action inside the Administration upon completing the action, the app should return a response with a valid body and the header `shopware-app-signature` containing the SHA256 HMAC of the whole response body signed with the app secret.
If you do not need to trigger any actions, a response with an empty body is also always valid.
### Opening a new tab for the user
Examples response body:
To open a new tab in the user browser you can use the `openNewTab` action type. You need to pass the url that should be opened as the `redirectUrl` property inside the payload.
```txt
Content-Type: application/json
{
"actionType": "openNewTab",
"payload": {
"redirectUrl": "http://google.com"
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::openNewTab('https://www.shopware.com');
```
### Show a notification to the user
To send a notification, you can use the `notification` action type. You need to pass the `status` property and the content of the notification as `message` property inside the payload.
```json
{
"actionType": "notification",
"payload": {
"status": "success",
"message": "This is the successful message"
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::notification('success', 'foo');
```
### Reload the current page
To reload the data in the user's current page you can use the `reload` action type with an empty payload.
```json
{
"actionType": "reload",
"payload": {}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::reload();
```
### Open a custom modal
To open a modal with the embedded link in the iframe, you can use the `openModal` action type. You need to pass the url that should be opened as the `iframeUrl` property and the `size` property inside the payload.
```json
{
"actionType": "openModal",
"payload": {
"iframeUrl": "http://google.com",
"size": "medium",
"expand": true
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::modal('https://shopware.com', size: 'medium', expand: true)
```
### General structure
* `actionType`: The type of action the app want to be triggered, including `notification`, `reload`, `openNewTab`, `openModal`
* `payload`: The needed data to perform the action.
* `redirectUrl`: The url to open new tab
* `iframeUrl`: The embedded link in modal iframe
* `status`: Notification status, including `success`, `error`, `info`, `warning`
* `message`: The content of the notification
* `size`: The size of the modal in `openModal` type, including `small`, `medium`, `large`, `fullscreen`, default `medium`
* `expand`: The expansion of the modal in `openModal` type, including `true`, `false`, default `false`
## Using Custom Endpoints as target
It is also possible to use [custom endpoints](../app-scripts/custom-endpoints.md) as target for action buttons.
::: info
This feature was added in Shopware 6.4.10.0, previous versions don't support relative target urls for action buttons.
:::
To use custom endpoints as the target url for action buttons you can define the target url as a relative url in your apps manifest.xml:
::: code-group
```xml [manifest.xml]
...
```
:::
And then add the corresponding app script that should be executed when the user clicks the action button.
```twig
// Resources/scripts/api-action-button/action-button-script.twig
{% set ids = hook.request.ids %}
{% set response = services.response.json({
"actionType": "notification",
"payload": {
"status": "success",
"message": "You selected " ~ ids|length ~ " products."
}
}) %}
{% do hook.setResponse(response) %}
```
As you can see it is possible to provide a [`JsonResponse`](../../../../resources/references/app-reference/script-reference/custom-endpoint-script-services-reference.md#json) to give [feedback to the user in the Administration](#providing-feedback-in-the-administration).
---
---
url: /docs/v6.5/guides/plugins/apps/administration/add-custom-action-button.md
---
# Add custom action button
:::info
This guide will show you how to add custom action buttons to the Shopware Administration using your manifest file. This works for simple applications; however, if you want to write more advanced applications, the [Meteor Admin SDK](https://shopware.github.io/meteor-admin-sdk/) is recommended. It has many more features and is more flexible.
For further details and guidance on custom action buttons, refer to the documentation provided on the Meteor Admin SDK's [action button](https://shopware.github.io/meteor-admin-sdk/docs/guide/api-reference/ui/actionButton) section.
:::
One extension possibility in the Administration is the ability to add custom action buttons to the smartbar. For now, you can add them in the smartbar of detail and list views:

To get those buttons, you start in the `admin` section of your manifest file. There you can define `` elements in order to add your button, as seen as below:
```xml
// manifest.xml
...
```
For a complete reference of the structure of the manifest file take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference).
An action button must have the following attributes:
* `action`: Unique identifier for the action, can be set freely.
* `entity`: Here you define which entity you're working on.
* `view`: `detail`or `list`; to set the view the button should be added to. Currently, you can choose between detail and listing view.
When the user clicks on the action button your app receives a request similar to the one generated by a [webhook](../app-base-guide#webhooks).
The main difference is that it contains the name of the entity and an array of ids that the user selected (or an array containing only a single id if the action button was executed on a detail page).
A sample payload may look like the following:
```json
{
"source":{
"url":"http:\/\/localhost:8000",
"appVersion":"1.0.0",
"shopId":"F0nWInXj5Xyr"
},
"data":{
"ids":[
"2132f284f71f437c9da71863d408882f"
],
"entity":"product",
"action":"restockProduct"
},
"meta":{
"timestamp":1592403610,
"reference":"9e968471797b4f29be3e3cf09f52d8da",
"language":"2fbb5fe2e29a4d70aa5854ce7ce3e20b"
}
}
```
```php
// injected or build by yourself
$shopResolver = new ShopResolver($repository);
$contextResolver = new ContextResolver();
$shop = $shopResolver->resolveShop($serverRequest);
$actionButton = $contextResolver->assembleActionButton($serverRequest, $shop);
```
```php
use Shopware\App\SDK\Context\ActionButton\ActionButtonAction;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
use Psr\Http\Message\ResponseInterface;
#[AsController]
class ActionButtonController {
#[Route('/action/product/detail')]
public function handle(ActionButtonAction $button): ResponseInterface
{
// handle button
return ActionButtonResponse::notification('success', 'Success message');
}
}
```
::: info
Starting from Shopware version 6.4.1.0, the current shopware version will be sent as a `sw-version` header.
:::
Again you can verify the authenticity of the incoming request, like with [webhooks](../app-base-guide#webhooks), by checking the `shopware-shop-signature` it too contains the SHA256 HMAC of the request body, that is signed with the secret your app assigned the shop during the [registration](../app-base-guide#setup).
## Providing feedback in the Administration
::: info
This feature was added in Shopware 6.4.3.0, previous versions will ignore the response content.
:::
::: info
Starting from Shopware version 6.4.8.0, the requests of the [tab](#opening-a-new-tab-for-the-user) and [custom modal](#open-a-custom-modal) have the following additional query parameters:
* `shop-id`
* `shop-url`
* `timestamp`
* `sw-context-language`
* `sw-user-language`
* `shopware-shop-signature`
You **must** make sure to verify the authenticity of the incoming request by checking the `shopware-shop-signature`, which is a hash of the request's query part, signed with the shop's secret key.
:::
If you want to trigger an action inside the Administration upon completing the action, the app should return a response with a valid body and the header `shopware-app-signature` containing the SHA256 HMAC of the whole response body signed with the app secret.
If you do not need to trigger any actions, a response with an empty body is also always valid.
### Opening a new tab for the user
Examples response body:
To open a new tab in the user browser you can use the `openNewTab` action type. You need to pass the url that should be opened as the `redirectUrl` property inside the payload.
```txt
Content-Type: application/json
{
"actionType": "openNewTab",
"payload": {
"redirectUrl": "http://google.com"
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::openNewTab('https://www.shopware.com');
```
### Show a notification to the user
To send a notification, you can use the `notification` action type. You need to pass the `status` property and the content of the notification as `message` property inside the payload.
```json
{
"actionType": "notification",
"payload": {
"status": "success",
"message": "This is the successful message"
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::notification('success', 'foo');
```
### Reload the current page
To reload the data in the user's current page you can use the `reload` action type with an empty payload.
```json
{
"actionType": "reload",
"payload": {}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::reload();
```
### Open a custom modal
To open a modal with the embedded link in the iframe, you can use the `openModal` action type. You need to pass the url that should be opened as the `iframeUrl` property and the `size` property inside the payload.
```json
{
"actionType": "openModal",
"payload": {
"iframeUrl": "http://google.com",
"size": "medium",
"expand": true
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::modal('https://shopware.com', size: 'medium', expand: true)
```
### General structure
* `actionType`: The type of action the app want to be triggered, including `notification`, `reload`, `openNewTab`, `openModal`
* `payload`: The needed data to perform the action.
* `redirectUrl`: The url to open new tab
* `iframeUrl`: The embedded link in modal iframe
* `status`: Notification status, including `success`, `error`, `info`, `warning`
* `message`: The content of the notification
* `size`: The size of the modal in `openModal` type, including `small`, `medium`, `large`, `fullscreen`, default `medium`
* `expand`: The expansion of the modal in `openModal` type, including `true`, `false`, default `false`
## Using Custom Endpoints as target
It is also possible to use [custom endpoints](../app-scripts/custom-endpoints) as target for action buttons.
::: info
This feature was added in Shopware 6.4.10.0, previous versions don't support relative target urls for action buttons.
:::
To use custom endpoints as the target url for action buttons you can define the target url as a relative url in your apps manifest.xml:
```xml
// manifest.xml
...
```
And then add the corresponding app script that should be executed when the user clicks the action button.
```twig
// Resources/scripts/api-action-button/action-button-script.twig
{% set ids = hook.request.ids %}
{% set response = services.response.json({
"actionType": "notification",
"payload": {
"status": "success",
"message": "You selected " ~ ids|length ~ " products."
}
}) %}
{% do hook.setResponse(response) %}
```
As you can see it is possible to provide a [`JsonResponse`](../../../../resources/references/app-reference/script-reference/custom-endpoint-script-services-reference#json) to give [feedback to the user in the administration](#providing-feedback-in-the-administration).
---
---
url: /docs/v6.6/guides/plugins/apps/administration/add-custom-action-button.md
---
# Add custom action button
:::info
This guide will show you how to add custom action buttons to the Shopware Administration using your manifest file. This works for simple applications; however, if you want to write more advanced applications, the [Meteor Admin SDK](/resources/admin-extension-sdk/) is recommended. It has many more features and is more flexible.
For further details and guidance on custom action buttons, refer to the documentation provided on the Meteor Admin SDK's [action button](/resources/admin-extension-sdk/api-reference/ui/actionButton) section.
:::
One extension possibility in the Administration is the ability to add custom action buttons to the smartbar. For now, you can add them in the smartbar of detail and list views:

To get those buttons, you start in the `admin` section of your manifest file. There you can define `` elements in order to add your button, as seen as below:
::: code-group
```xml [manifest.xml]
...
```
:::
For a complete reference of the structure of the manifest file take a look at the [Manifest reference](../../../../resources/references/app-reference/manifest-reference).
An action button must have the following attributes:
* `action`: Unique identifier for the action, can be set freely.
* `entity`: Here you define which entity you're working on.
* `view`: `detail`or `list`; to set the view the button should be added to. Currently, you can choose between detail and listing view.
When the user clicks on the action button your app receives a request similar to the one generated by a [webhook](../app-base-guide#webhooks).
The main difference is that it contains the name of the entity and an array of ids that the user selected (or an array containing only a single id if the action button was executed on a detail page).
A sample payload may look like the following:
```json
{
"source":{
"url":"http:\/\/localhost:8000",
"appVersion":"1.0.0",
"shopId":"F0nWInXj5Xyr"
},
"data":{
"ids":[
"2132f284f71f437c9da71863d408882f"
],
"entity":"product",
"action":"restockProduct"
},
"meta":{
"timestamp":1592403610,
"reference":"9e968471797b4f29be3e3cf09f52d8da",
"language":"2fbb5fe2e29a4d70aa5854ce7ce3e20b"
}
}
```
```php
// injected or build by yourself
$shopResolver = new ShopResolver($repository);
$contextResolver = new ContextResolver();
$shop = $shopResolver->resolveShop($serverRequest);
$actionButton = $contextResolver->assembleActionButton($serverRequest, $shop);
```
```php
use Shopware\App\SDK\Context\ActionButton\ActionButtonAction;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Psr\Http\Message\ResponseInterface;
#[AsController]
class ActionButtonController {
#[Route('/action/product/detail')]
public function handle(ActionButtonAction $button): ResponseInterface
{
// handle button
return ActionButtonResponse::notification('success', 'Success message');
}
}
```
::: info
Starting from Shopware version 6.4.1.0, the current shopware version will be sent as a `sw-version` header.
:::
Again you can verify the authenticity of the incoming request, like with [webhooks](../app-base-guide#webhooks), by checking the `shopware-shop-signature` it too contains the SHA256 HMAC of the request body, that is signed with the secret your app assigned the shop during the [registration](../app-base-guide#setup).
## Providing feedback in the Administration
::: info
This feature was added in Shopware 6.4.3.0, previous versions will ignore the response content.
:::
::: info
Starting from Shopware version 6.4.8.0, the requests of the [tab](#opening-a-new-tab-for-the-user) and [custom modal](#open-a-custom-modal) have the following additional query parameters:
* `shop-id`
* `shop-url`
* `timestamp`
* `sw-context-language`
* `sw-user-language`
* `shopware-shop-signature`
You **must** make sure to verify the authenticity of the incoming request by checking the `shopware-shop-signature`, which is a hash of the request's query part, signed with the shop's secret key.
:::
If you want to trigger an action inside the Administration upon completing the action, the app should return a response with a valid body and the header `shopware-app-signature` containing the SHA256 HMAC of the whole response body signed with the app secret.
If you do not need to trigger any actions, a response with an empty body is also always valid.
### Opening a new tab for the user
Examples response body:
To open a new tab in the user browser you can use the `openNewTab` action type. You need to pass the url that should be opened as the `redirectUrl` property inside the payload.
```txt
Content-Type: application/json
{
"actionType": "openNewTab",
"payload": {
"redirectUrl": "http://google.com"
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::openNewTab('https://www.shopware.com');
```
### Show a notification to the user
To send a notification, you can use the `notification` action type. You need to pass the `status` property and the content of the notification as `message` property inside the payload.
```json
{
"actionType": "notification",
"payload": {
"status": "success",
"message": "This is the successful message"
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::notification('success', 'foo');
```
### Reload the current page
To reload the data in the user's current page you can use the `reload` action type with an empty payload.
```json
{
"actionType": "reload",
"payload": {}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::reload();
```
### Open a custom modal
To open a modal with the embedded link in the iframe, you can use the `openModal` action type. You need to pass the url that should be opened as the `iframeUrl` property and the `size` property inside the payload.
```json
{
"actionType": "openModal",
"payload": {
"iframeUrl": "http://google.com",
"size": "medium",
"expand": true
}
}
```
```php
use Shopware\App\SDK\Response\ActionButtonResponse;
ActionButtonResponse::modal('https://shopware.com', size: 'medium', expand: true)
```
### General structure
* `actionType`: The type of action the app want to be triggered, including `notification`, `reload`, `openNewTab`, `openModal`
* `payload`: The needed data to perform the action.
* `redirectUrl`: The url to open new tab
* `iframeUrl`: The embedded link in modal iframe
* `status`: Notification status, including `success`, `error`, `info`, `warning`
* `message`: The content of the notification
* `size`: The size of the modal in `openModal` type, including `small`, `medium`, `large`, `fullscreen`, default `medium`
* `expand`: The expansion of the modal in `openModal` type, including `true`, `false`, default `false`
## Using Custom Endpoints as target
It is also possible to use [custom endpoints](../app-scripts/custom-endpoints) as target for action buttons.
::: info
This feature was added in Shopware 6.4.10.0, previous versions don't support relative target urls for action buttons.
:::
To use custom endpoints as the target url for action buttons you can define the target url as a relative url in your apps manifest.xml:
::: code-group
```xml [manifest.xml]
...
```
:::
And then add the corresponding app script that should be executed when the user clicks the action button.
```twig
// Resources/scripts/api-action-button/action-button-script.twig
{% set ids = hook.request.ids %}
{% set response = services.response.json({
"actionType": "notification",
"payload": {
"status": "success",
"message": "You selected " ~ ids|length ~ " products."
}
}) %}
{% do hook.setResponse(response) %}
```
As you can see it is possible to provide a [`JsonResponse`](../../../../resources/references/app-reference/script-reference/custom-endpoint-script-services-reference#json) to give [feedback to the user in the administration](#providing-feedback-in-the-administration).
---
---
url: /docs/guides/plugins/plugins/storefront/styling/add-custom-assets.md
---
# Add Custom Assets
## Overview
When working with an own plugin, the usage of own custom images or other assets is a natural requirement. So of course you can do that in Shopware. In this guide we will discover together how it's possible to add and use custom assets in your Shopware plugin.
## Prerequisites
In order to be able to start with this guide, you need to have an own plugin running. As to most guides, this guide is also built upon the [Plugin base guide](../../plugin-base-guide.md).
Needless to say, you should have your image or another asset at hand to work with.
## Adding custom assets to your plugin
In order to add custom assets to your theme, you need to create a new folder called public inside the `src/Resources` directory of your plugin. Here you're able to store your assets files, so please feel free to save your image there - we'll do the same thing in our example plugin.
```bash
# PluginRoot
.
βββ composer.json
βββ src
βββ Resources
β βββ public
β β βββ your-image.png <-- Asset file here
βββ SwagBasicExample.php
```
Afterwards, you need to make sure your plugin assets are copied over to the public/bundles folder. However, don't to this by hand - the command `bin/console assets:install` will take care of it.
```text
# shopware-root/public/bundles
.
βββ administration
βββ framework
βββ storefront
βββ swagbasicexample
βββ your-image.png <-- Your asset is copied here
```
## Linking to assets
### Using custom assets in your template
Let's think about a simple example, displaying our image right in the base template of the Storefront. In there we're able to link our assets by simply using the [asset](https://symfony.com/doc/current/templates.html#linking-to-css-javascript-and-image-assets) function Symfony provides:
```twig
// /src/Resources/views/storefront/base.html.twig
{% sw_extends '@Storefront/storefront/base.html.twig' %}
{% block base_main %}
Asset:
{# Using asset function to display our custom asset #}
{{ parent() }}
{% endblock %}
```
That's basically all you need to do to link your plugin's custom assets.
### Using custom assets in your CSS files
There's one more interesting possibility though. If you want, you can use your custom asset in your CSS files. Look at the following example:
```css
// /src/Resources/app/storefront/src/scss/base.scss
body {
background-image: url("#{$sw-asset-public-url}/bundles/swagbasicexample/image.png");
}
```
You see, we can use our custom assets by using the asset path provided by the `bundle` directory.
### Adding custom assets in themes
Of course, you're able to use custom assets in themes as well. In this context there's another way on integration custom assets into your theme. Please take a look on the guide about adding assets to a theme for further detail:
## Next steps
One of the said custom assets are medias. For more information on that, refer to [Media and thumbnails](../howto/use-media-thumbnails.md).
---
---
url: /docs/v6.5/guides/plugins/plugins/storefront/add-custom-assets.md
---
# Add Custom Assets
## Overview
When working with an own plugin, the usage of own custom images or other assets is a natural requirement. So of course you can do that in Shopware. In this guide we will discover together how it's possible to add and use custom assets in your Shopware plugin.
## Prerequisites
In order to be able to start with this guide, you need to have an own plugin running. As to most guides, this guide is also built upon the [Plugin base guide](../plugin-base-guide)
Needless to say, you should have your image or another asset at hand to work with.
## Adding custom assets to your plugin
In order to add custom assets to your theme, you need to create a new folder called public inside the `src/Resources` directory of your plugin. Here you're able to store your assets files, so please feel free to save your image there - we'll do the same thing in our example plugin.
```bash
# PluginRoot
.
βββ composer.json
βββ src
βββ Resources
β βββ public
β β βββ your-image.png <-- Asset file here
βββ SwagBasicExample.php
```
Afterwards, you need to make sure your plugin assets are copied over to the public/bundles folder. However, don't to this by hand - the command `bin/console assets:install` will take care of it.
```text
# shopware-root/public/bundles
.
βββ administration
βββ framework
βββ storefront
βββ swagbasicexample
βββ your-image.png <-- Your asset is copied here
```
## Linking to assets
### Using custom assets in your template
Let's think about a simple example, displaying our image right in the base template of the Storefront. In there we're able to link our assets by simply using the [asset](https://symfony.com/doc/current/templates.html#linking-to-css-javascript-and-image-assets) function Symfony provides:
```twig
// /src/Resources/views/storefront/base.html.twig
{% sw_extends '@Storefront/storefront/base.html.twig' %}
{% block base_main %}
Asset:
{# Using asset function to display our custom asset #}
{{ parent() }}
{% endblock %}
```
That's basically all you need to do to link your plugin's custom assets.
### Using custom assets in your CSS files
There's one more interesting possibility though. If you want, you can use your custom asset in your CSS files. Look at the following example:
```css
// /src/Resources/app/storefront/src/scss/base.scss
body {
background-image: url("#{$sw-asset-public-url}/bundles/swagbasicexample/image.png");
}
```
You see, we can use our custom assets by using the asset path provided by the `bundle` directory.
### Adding custom assets in themes
Of course, you're able to use custom assets in themes as well. In this context there's another way on integration custom assets into your theme. Please take a look on the guide about adding assets to a theme for further detail:
## Next steps
One of the said custom assets are medias. For more information on that, refer to [Media and thumbnails](use-media-thumbnails).
---
---
url: /docs/v6.6/guides/plugins/plugins/storefront/add-custom-assets.md
---
# Add Custom Assets
## Overview
When working with an own plugin, the usage of own custom images or other assets is a natural requirement. So of course you can do that in Shopware. In this guide we will discover together how it's possible to add and use custom assets in your Shopware plugin.
## Prerequisites
In order to be able to start with this guide, you need to have an own plugin running. As to most guides, this guide is also built upon the [Plugin base guide](../plugin-base-guide)
Needless to say, you should have your image or another asset at hand to work with.
## Adding custom assets to your plugin
In order to add custom assets to your theme, you need to create a new folder called public inside the `src/Resources` directory of your plugin. Here you're able to store your assets files, so please feel free to save your image there - we'll do the same thing in our example plugin.
```bash
# PluginRoot
.
βββ composer.json
βββ src
βββ Resources
β βββ public
β β βββ your-image.png <-- Asset file here
βββ SwagBasicExample.php
```
Afterwards, you need to make sure your plugin assets are copied over to the public/bundles folder. However, don't to this by hand - the command `bin/console assets:install` will take care of it.
```text
# shopware-root/public/bundles
.
βββ administration
βββ framework
βββ storefront
βββ swagbasicexample
βββ your-image.png <-- Your asset is copied here
```
## Linking to assets
### Using custom assets in your template
Let's think about a simple example, displaying our image right in the base template of the Storefront. In there we're able to link our assets by simply using the [asset](https://symfony.com/doc/current/templates.html#linking-to-css-javascript-and-image-assets) function Symfony provides:
```twig
// /src/Resources/views/storefront/base.html.twig
{% sw_extends '@Storefront/storefront/base.html.twig' %}
{% block base_main %}
Asset:
{# Using asset function to display our custom asset #}
{{ parent() }}
{% endblock %}
```
That's basically all you need to do to link your plugin's custom assets.
### Using custom assets in your CSS files
There's one more interesting possibility though. If you want, you can use your custom asset in your CSS files. Look at the following example:
```css
// /src/Resources/app/storefront/src/scss/base.scss
body {
background-image: url("#{$sw-asset-public-url}/bundles/swagbasicexample/image.png");
}
```
You see, we can use our custom assets by using the asset path provided by the `bundle` directory.
### Adding custom assets in themes
Of course, you're able to use custom assets in themes as well. In this context there's another way on integration custom assets into your theme. Please take a look on the guide about adding assets to a theme for further detail:
## Next steps
One of the said custom assets are medias. For more information on that, refer to [Media and thumbnails](use-media-thumbnails).
---
---
url: /docs/v6.5/guides/plugins/plugins/storefront/add-custom-captcha.md
---
# Add custom captcha
## Overview
You can add your custom captcha to the Shopware 6 core. This guide will show you how to do that.
## Prerequisites
In order to be able to start with this guide, you need to have an own plugin running. As to most guides, this guide is also built upon the [Plugin base guide](../plugin-base-guide)
## Adding custom captcha to your plugin
In order to add custom captcha to your plugin, create a new folder called `Captcha` inside the `src/Framework` directory of your plugin. This is optional, but it's a good practice to keep your plugin files organized.
Take a look at the AbstractCaptcha class. This class is the base class for all captcha types. It contains the following methods:
* `supports(string $type): bool` - This method is used to check if the captcha type is supported by the plugin.
* `isValid(string $code): bool` - This method is used to check if the captcha code is valid.
* `getName(): string` - This method is used to get the name of the captcha type.
* `shouldBreak(): bool` - This method is used to check if the captcha should break the validation.
* `getData(): array` - This method is used to get the data of the captcha type.
- `getViolations(): ConstraintViolationListInterface` - This method is used to get the violations of the captcha type.
Simply extend the AbstractCaptcha class and implement the methods isValid and getName. The isValid method should return true if the captcha code is valid, false otherwise. The getName method should return the name of the captcha type.
```php
get(self::CAPTCHA_REQUEST_PARAMETER)) {
return false;
}
try {
$response = $this->client->request('POST', self::GOOGLE_CAPTCHA_VERIFY_ENDPOINT, [
'form_params' => [
'response' => $request->get(self::CAPTCHA_REQUEST_PARAMETER),
'remoteip' => $request->getClientIp(),
],
]);
$responseRaw = $response->getBody()->getContents();
$response = json_decode($responseRaw, true);
return $response && (bool) $response['success'];
} catch (ClientExceptionInterface) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return self::CAPTCHA_NAME;
}
}
```
## Google reCAPTCHA v3 example
You might want to check out the example [GoogleReCaptchaV3](https://github.com/shopware/shopware/blob/trunk/src/Storefront/Framework/Captcha/GoogleReCaptchaV3.php) class from the Shopware 6 core. It's a good example of how to implement a custom captcha type.
---
---
url: /docs/v6.6/guides/plugins/plugins/storefront/add-custom-captcha.md
---
# Add custom captcha
## Overview
You can add your custom captcha to the Shopware 6 core. This guide will show you how to do that.
## Prerequisites
In order to be able to start with this guide, you need to have an own plugin running. As to most guides, this guide is also built upon the [Plugin base guide](../plugin-base-guide)
## Adding custom captcha to your plugin
In order to add custom captcha to your plugin, create a new folder called `Captcha` inside the `src/Framework` directory of your plugin. This is optional, but it's a good practice to keep your plugin files organized.
Take a look at the AbstractCaptcha class. This class is the base class for all captcha types. It contains the following methods:
* `supports(string $type): bool` - This method is used to check if the captcha type is supported by the plugin.
* `isValid(string $code): bool` - This method is used to check if the captcha code is valid.
* `getName(): string` - This method is used to get the name of the captcha type.
* `shouldBreak(): bool` - This method is used to check if the captcha should break the validation.
* `getData(): array` - This method is used to get the data of the captcha type.
* `getViolations(): ConstraintViolationListInterface` - This method is used to get the violations of the captcha type.
Extend the AbstractCaptcha class and implement the methods isValid and getName. The isValid method should return true if the captcha code is valid, false otherwise. The getName method should return the name of the captcha type.
```php
get(self::CAPTCHA_REQUEST_PARAMETER)) {
return false;
}
try {
$response = $this->client->request('POST', self::GOOGLE_CAPTCHA_VERIFY_ENDPOINT, [
'form_params' => [
'response' => $request->get(self::CAPTCHA_REQUEST_PARAMETER),
'remoteip' => $request->getClientIp(),
],
]);
$responseRaw = $response->getBody()->getContents();
$response = json_decode($responseRaw, true);
return $response && (bool) $response['success'];
} catch (ClientExceptionInterface) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return self::CAPTCHA_NAME;
}
}
```
## Google reCAPTCHA v3 example
You might want to check out the example [GoogleReCaptchaV3](https://github.com/shopware/shopware/blob/trunk/src/Storefront/Framework/Captcha/GoogleReCaptchaV3.php) class from the Shopware 6 core. It's a good example of how to implement a custom captcha type.
---
---
url: /docs/guides/plugins/plugins/storefront/howto/add-custom-captcha.md
---
# Add Custom Captcha
## Overview
Add your custom captcha to the Shopware 6 core. This guide shows you how.
## Prerequisites
A running plugin is required. Review the [Plugin base guide](../../plugin-base-guide.md) for guidance on creating one.
## Adding custom captcha to your plugin
In order to add custom captcha to your plugin, create a new folder called `Captcha` inside the `src/Framework` directory of your plugin. This is optional, but it's a good practice to keep your plugin files organized.
Take a look at the AbstractCaptcha class. This class is the base class for all captcha types. It contains the following methods:
* `supports(string $type): bool` - This method is used to check if the captcha type is supported by the plugin.
* `isValid(string $code): bool` - This method is used to check if the captcha code is valid.
* `getName(): string` - This method is used to get the name of the captcha type.
* `shouldBreak(): bool` - This method is used to check if the captcha should break the validation.
* `getData(): array` - This method is used to get the data of the captcha type.
* `getViolations(): ConstraintViolationListInterface` - This method is used to get the violations of the captcha type.
Extend the AbstractCaptcha class and implement the methods isValid and getName. The isValid method should return true if the captcha code is valid, false otherwise. The getName method should return the name of the captcha type.
```php
get(self::CAPTCHA_REQUEST_PARAMETER)) {
return false;
}
try {
$response = $this->client->request('POST', self::GOOGLE_CAPTCHA_VERIFY_ENDPOINT, [
'form_params' => [
'response' => $request->get(self::CAPTCHA_REQUEST_PARAMETER),
'remoteip' => $request->getClientIp(),
],
]);
$responseRaw = $response->getBody()->getContents();
$response = json_decode($responseRaw, true);
return $response && (bool) $response['success'];
} catch (ClientExceptionInterface) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return self::CAPTCHA_NAME;
}
}
```
## Google reCAPTCHA v3 example
You might want to check out the example [GoogleReCaptchaV3](https://github.com/shopware/shopware/blob/trunk/src/Storefront/Framework/Captcha/GoogleReCaptchaV3.php) class from the Shopware 6 core. It's a good example of how to implement a custom captcha type.
---
---
url: /docs/guides/plugins/plugins/framework/system-check/add-custom-check.md
---
# Add Custom Check
## Overview
In this guide, we will be building a dummy example of a custom system check that verifies if the local system has enough disk space to operate normally.
## Add a new Custom Check
First, you need to add a new `LocalDiskSpaceCheck` class that extends the `Shopware\Core\Framework\SystemCheck\BaseCheck` and implement the essential categorization methods.
### Fill the categorization methods
Each check contains a set of categorization methods that help to classify the check, and determine when and where it should be executed.
```php
class LocalDiskSpaceCheck extends BaseCheck
{
public function category(): Category
{
// crucial for the system to function at all.
return Category::SYSTEM;
}
public function name(): string
{
return 'LocalDiskSpaceCheck';
}
protected function allowedSystemCheckExecutionContexts(): array
{ // a potentially long-running check, because it has an IO operation.
return SystemCheckExecutionContext::longRunning();
}
}
```
### Create the check logic
The next step is to implement the actual check logic. We will check if the disk space is below a certain threshold and return the appropriate result.
```php
class LocalDiskSpaceCheck extends BaseCheck
{
public function __construct(
private readonly string $adapterType,
private readonly string $installationPath,
private readonly int $warningThresholdInMb
)
{
}
public function run(): Result
{
if ($this->adapterType !== 'local') {
return new Result(name: $this->name(), status: Status::SKIPPED, message: 'Disk space check is only available for local file systems.', healthy: true)
}
$availableSpaceInMb = $this->getFreeDiskSpaceInMegaBytes();
if ($availableSpaceInMb < $this->warningThresholdInMb) {
return new Result(name: $this->name(), status: Status::WARNING, message: sprintf('Available disk space is below the warning threshold of %s.', $this->warningThresholdInMb), healthy: true);
}
return new Result(name: $this->name(), status: Status::OK, message: 'Disk space is sufficient.', healthy: true);
}
private function getFreeDiskSpaceInMegaBytes()
{
$freeSpace = disk_free_space($this->installationPath);
$totalSpace = disk_total_space($this->installationPath);
$availableSpace = $totalSpace - $freeSpace;
return $availableSpace / 1024 / 1024;
}
...
...
}
```
> An important consideration is the healthy flag, which is subjective and can vary depending on the specific shop's criteria. For example, if the disk space threshold is set high, the system can still function normally, so the healthy flag could be true. Conversely, if the threshold is too low for normal operation, the healthy flag could be false.
### Register the custom check
Finally, you need to register the custom check as a service resource.
```php
$services->set(YourNameSpace\LocalDiskSpaceCheck::class)
->args([
'%shopware.filesystem.public.type%',
'%shopware.filesystem.public.config.root%',
'%warning_threshold_in_mb%',
])
->tag('shopware.system_check');
```
### Trigger the check
The system check is now part of the system check collection and will be executed when the system check is triggered. Refer to the [System Check](index.md) guide for more information.
---
---
url: /docs/guides/plugins/plugins/plugin-fundamentals/add-custom-commands.md
---
# Add Custom CLI Commands
Shopware CLI commands are based on [Symfony Console](https://symfony.com/doc/current/console.html). This means that creating custom commands in Shopware plugins follows the standard Symfony approach.
To add a custom command in a Shopware plugin, you must register it as a service in your plugin's `src/Resources/config/services.php` and tag it with `console.command`:
```php
$services->set(Swag\BasicExample\Command\ExampleCommand::class)
->tag('console.command');
```
Commands registered as services in a Shopware plugin are automatically available via `bin/console`.
A minimal command class:
```php
// /src/Command/ExampleCommand.php
writeln('Hello from ExampleCommand');
return Command::SUCCESS;
}
}
```
## Next steps
[Adding a scheduled task](add-scheduled-task.md)
---
---
url: /docs/v6.5/guides/plugins/plugins/plugin-fundamentals/add-custom-commands.md
---
# Add Custom CLI Commands
To ease development tasks, Shopware contains the Symfony commands functionality. This allows (plugin-) developers to define new commands executable via the Symfony console at `bin/console`. The best thing about commands is, that they're more than just simple standalone PHP scripts - they integrate into Symfony and Shopware, so you've got access to all the functionality offered by both of them.
Creating a command for Shopware 6 via a plugin works exactly like you would add a command to Symfony. Make sure to have a look at the Symfony commands guide:
## Prerequisites
This guide **does not** explain how to create a new plugin for Shopware 6. Head over to our plugin base guide to learn how to create a plugin at first:
The main requirement here is to have a `services.xml` file loaded in your plugin. This can be achieved by placing the file into a `Resources/config` directory relative to your plugin's base class location.
::: info
Refer to this video on custom **[Creating a CLI command](https://www.youtube.com/watch?v=OL_qNVLLyaI)**. Also available on our free online training ["Shopware 6 Backend Development"](https://academy.shopware.com/courses/shopware-6-backend-development-with-jisse-reitsma).
:::
## Registering your command
From here on, everything works exactly like in Symfony itself. Commands are recognised by Shopware, once they're tagged with the `console.command` tag in the [dependency injection](dependency-injection) container. So to register a new command, just add it to your plugin's `services.xml` and specify the `console.command` tag:
```html
```
Here's a full example `services.xml` which registers your custom command:
```xml
// /src/Resources/config/services.xml
```
Your command's class should extend from the `Symfony\Component\Console\Command\Command` class, here's an example:
```php
// /src/Command/ExampleCommand.php
setDescription('Does something very special.');
}
// Actual code executed in the command
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('It works!');
// Exit code 0 for success
return 0;
}
}
```
This command is of course only a basic example, so feel free to experiment. As stated above, you now have access to all the functionality offered by Symfony and Shopware.
::: info
For inspiration, maybe have a look at the Symfony documentation - you may for example use [tables](https://symfony.com/doc/current/components/console/helpers/table.html), [progress bars](https://symfony.com/doc/current/components/console/helpers/progressbar.html), or [custom formats](https://symfony.com/doc/current/components/console/helpers/formatterhelper.html).
:::
### Running commands
Commands are run via the `bin/console` executable. To list all available commands, run `bin/console list`:
```text
$: php bin/console list
Symfony 4.4.4 (env: dev, debug: true)
Usage:
command [options] [arguments]
Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-e, --env=ENV The Environment name. [default: "dev"]
--no-debug Switches off debug mode.
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
about Displays information about the current project
help Displays help for a command
list Lists commands
feature
feature:dump Creating json file with feature config for js testing and hot reloading capabilities.
assets
assets:install
bundle
bundle:dump Creates a json file with the configuration for each active Shopware bundle.
cache
cache:clear Clears the cache
cache:pool:clear Clears cache pools
cache:pool:delete Deletes an item from a cache pool
cache:pool:list List available cache pools
cache:pool:prune Prunes cache pools
cache:warmup Warms up an empty cache
[...]
```
Each command usually has a namespace like `cache`, so to clear the cache you would execute `php bin/console cache:clear`. If you would like to learn more about commands in general, have a look at [this article](https://symfony.com/doc/current/console.html) in the Symfony documentation.
## More interesting topics
* [Adding a scheduled task](add-scheduled-task)
---
---
url: /docs/v6.6/guides/plugins/plugins/plugin-fundamentals/add-custom-commands.md
---
# Add Custom CLI Commands
To ease development tasks, Shopware contains the Symfony commands functionality. This allows (plugin-) developers to define new commands executable via the Symfony console at `bin/console`. The best thing about commands is, that they're more than just simple standalone PHP scripts - they integrate into Symfony and Shopware, so you've got access to all the functionality offered by both of them.
Creating a command for Shopware 6 via a plugin works exactly like you would add a command to Symfony. Make sure to have a look at the Symfony commands guide:
## Prerequisites
This guide **does not** explain how to create a new plugin for Shopware 6. Head over to our plugin base guide to learn how to create a plugin at first:
The main requirement here is to have a `services.xml` file loaded in your plugin. This can be achieved by placing the file into a `Resources/config` directory relative to your plugin's base class location.
::: info
Refer to this video on custom **[Creating a CLI command](https://www.youtube.com/watch?v=OL_qNVLLyaI)**. Also available on our free online training ["Shopware 6 Backend Development"](https://academy.shopware.com/courses/shopware-6-backend-development-with-jisse-reitsma).
:::
## Registering your command
From here on, everything works exactly like in Symfony itself. Commands are recognised by Shopware, once they're tagged with the `console.command` tag in the [dependency injection](dependency-injection) container. So to register a new command, just add it to your plugin's `services.xml` and specify the `console.command` tag:
```html
```
Here's a full example `services.xml` which registers your custom command:
```xml
// /src/Resources/config/services.xml
```
Your command's class should extend from the `Symfony\Component\Console\Command\Command` class, here's an example:
```php
// /src/Command/ExampleCommand.php
setDescription('Does something very special.');
}
// Actual code executed in the command
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('It works!');
return Command::SUCCESS;
}
}
```
This command is of course only a basic example, so feel free to experiment. As stated above, you now have access to all the functionality offered by Symfony and Shopware.
::: info
For inspiration, maybe have a look at the Symfony documentation - you may for example use [tables](https://symfony.com/doc/current/components/console/helpers/table.html), [progress bars](https://symfony.com/doc/current/components/console/helpers/progressbar.html), or [custom formats](https://symfony.com/doc/current/components/console/helpers/formatterhelper.html).
:::
### Running commands
Commands are run via the `bin/console` executable. To list all available commands, run `bin/console list`:
```text
$: php bin/console list
Symfony 4.4.4 (env: dev, debug: true)
Usage:
command [options] [arguments]
Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-e, --env=ENV The Environment name. [default: "dev"]
--no-debug Switches off debug mode.
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
about Displays information about the current project
help Displays help for a command
list Lists commands
feature
feature:dump Creating json file with feature config for js testing and hot reloading capabilities.
assets
assets:install
bundle
bundle:dump Creates a json file with the configuration for each active Shopware bundle.
cache
cache:clear Clears the cache
cache:pool:clear Clears cache pools
cache:pool:delete Deletes an item from a cache pool
cache:pool:list List available cache pools
cache:pool:prune Prunes cache pools
cache:warmup Warms up an empty cache
[...]
```
Each command usually has a namespace like `cache`, so to clear the cache you would execute `php bin/console cache:clear`. If you would like to learn more about commands in general, have a look at [this article](https://symfony.com/doc/current/console.html) in the Symfony documentation.
## More interesting topics
* [Adding a scheduled task](add-scheduled-task)
---
---
url: /docs/v6.5/guides/plugins/apps/content/cms/add-custom-cms-blocks.md
---
# Add custom CMS blocks
::: info
This functionality is available starting with Shopware 6.4.4.0.
You can [add custom CMS blocks](../../../plugins/content/cms/add-cms-block) using the plugin system, however these will not be available in Shopware cloud stores.
:::
Didn't get in touch with Shopware's Shopping Experiences (CMS) yet? Check out the concept behind it first:
## Prerequisites
This guide is based on our [App Base Guide](../../app-base-guide) and assumes you have already set up an app.
## Overview
Adding custom CMS blocks from an app works a bit differently than [adding them from a plugin](../../../plugins/content/cms/add-cms-block).
Custom CMS blocks are added by providing a `cms.xml` in the `Resources/` directory of your app.
The basic directory structure looks as follows:
```text
βββ Resources
β βββ app
β β βββ storefront
β β βββ src
β β βββ scss
β β βββ base.scss
β βββ cms
β β βββ blocks
β β βββ swag-image-text-reversed
β β βββ preview.html
β β βββ styles.css
β βββ views
β β βββ storefront
β β βββ block
β β βββ cms-block-swag-image-text-reversed-component.html.twig
β βββ cms.xml
βββ manifest.xml
```
Each CMS block defined within your `cms.xml` must have a directory matching the block's name in `Resources/cms/blocks/`.
In those directories you shape your blocks for the CMS module in the Administration by supplying a `preview.html` containing the template used for displaying a preview.
Styling the preview in the sidebar and the component in the CMS editor is possible from the `styles.css`.
::: info
Due to technical limitations it's not possible to use templating engines (like Twig) or preprocessors (like Sass) for rendering and styling the preview.
:::
The Storefront representations of your blocks reside in `Resources/views/storefront/block/`.
## Defining blocks
As already mentioned above and similar to an app's `manifest.xml`, CMS blocks also require some definition done in the `cms.xml`.
In this example we will define a custom CMS block that will extend the default block `image-text` and reverse its elements:
```xml
// /Resources/cms.xml
swag-image-text-reversedtext-image20px20px20px20pxboxed
```
Let's have a look at how to configure a CMS block from your app's `cms.xml`:
`` : A **unique** technical name for your block.
`` : Blocks are divided into categories. Available categories can be found in the [plugin guide](../../../plugins/content/cms/add-cms-block#custom-block-in-the-administration).
`