# Benny's Shopware Notebook > A practical notebook supplementing the official Shopware 6 documentation with real-world insights on setup, hosting, deployment, development and migration. A practical notebook supplementing the official Shopware 6 documentation with real-world insights on setup, hosting, deployment, development and migration, by Benny Poensgen. # Introduction # Introduction Welcome to my little black Shopware notebook! Here I collect technical knowledge and details about (mainly) Shopware 6. My sketchbook is not intended to replace the Shopware documentation, but to supplement it. From the perspective of a Shopware user and developer. My experience with Shopware 6 comes from working on numerous Shopware 6 projects. I support both small and medium sized shops as a lone fighter, as well as larger shops as a member of larger teams. The notebook covers a wide range of aspects of designing, setting up and running a Shopware 6 shop. Instead of reading it from cover to cover, use the search function or have a look at the table of contents. Something not working as described? Further questions or comments? [Let's get in touch!](https://notebook.vanwittlaer.de/introduction/how-to-get-in-touch/index.md) **Editor** Benny Poensgen\ Manteuffelstr. 5, 65197 Wiesbaden\ E-Mail: [poensgen@vanwittlaer.de](mailto:poensgen@vanwittlaer.de) # How to Get in Touch You can reach me - by phone [+49 (611) 950 123 98](tel:+4961195012398) (Mo thru Fr from 09:00 to 17:30 CET/CEST) - by email [benjamin@poensgen.de](mailto:benjamin@poensgen.de) - via [Github](https://github.com/vanWittlaer) - on Discord's Shopware Community server (bennypoensgen) - reach out for me at [LinkedIn](https://www.linkedin.com/in/poensgen/) # DDEV for Shopware # DDEV for Shopware - [Why use DDEV for Shopware?](https://notebook.vanwittlaer.de/ddev-for-shopware/why-use-ddev/index.md): Recently, DDEV has been growing in popularity amongst both Shopware developers and Shopware agencies. Here’s my theory as to why that is. - [Less than 5 Minutes Install with DDEV and Symfony Flex](https://notebook.vanwittlaer.de/ddev-for-shopware/less-than-5-minutes-install-with-ddev-and-symfony-flex/index.md): This page describes how to install Shopware in a local DDEV environment, using Shopware's Symfony Flex template. - [Using shopware-cli with DDEV](https://notebook.vanwittlaer.de/ddev-for-shopware/using-shopware-cli-with-ddev/index.md): This page explains how to use shopware-cli with DDEV. shopware-cli replaces some of Shopware's commands (like bin/storefront-build.sh), and adds a set of useful commands. - [Storefront and Admin Watchers with DDEV](https://notebook.vanwittlaer.de/ddev-for-shopware/storefront-and-admin-watchers-with-ddev/index.md): This page explains how to set up and use Shopware's storefront and admin watchers. - [Setting up workers and the scheduler as automated tasks](https://notebook.vanwittlaer.de/ddev-for-shopware/message-queue-setup-with-ddev/index.md): This page explains how to setup the messenger:consume and scheduled-task:run workers (the "message queue") in a DDEV environment, mirroring a production server setup. - [Contribute with DDEV](https://notebook.vanwittlaer.de/ddev-for-shopware/contribute-with-ddev/index.md): This page explains how to setup a local contributing environment for Shopware 6' community edition with DDEV. - [Performance Tweaks](https://notebook.vanwittlaer.de/ddev-for-shopware/performance-tweaks/index.md): This page provides some basic performance tweaks for running a Shopware project with DDEV. - [RabbitMQ with DDEV](https://notebook.vanwittlaer.de/ddev-for-shopware/rabbitmq-with-ddev/index.md): This page explains how to setup the open-source message queuing software RabbitMQ with DDEV and Shopware 6. - [phpstan pro with DDEV](https://notebook.vanwittlaer.de/ddev-for-shopware/phpstan-pro-with-ddev/index.md): For phpstan pro see https://phpstan.org/blog/introducing-phpstan-pro. It's a great tool, it's monthly plan for developers should be worth it for most of us. - [Fetch Media from Production or Staging Server](https://notebook.vanwittlaer.de/ddev-for-shopware/fetch-media-from-production-or-staging-server/index.md): For local development and testing you do not have to copy all media files to your local environment. - [Running Claude Code in DDEV](https://notebook.vanwittlaer.de/ddev-for-shopware/running-claude-code-in-ddev/index.md): If you do not wish to install Claude Code directly on your computer, this chapter explains how you can easily run it from within your DDEV container. - [Frontend Work with Claude Code and Playwright](https://notebook.vanwittlaer.de/ddev-for-shopware/frontend-work-with-claude-code-and-playwright/index.md): When asking Claude for help with frontend development, it obviously needs some kind of access to the browser frontend. Playwright can provide direct access to browser output for Claude Code. - [DDEV behind Fritz!Box Routers](https://notebook.vanwittlaer.de/ddev-for-shopware/ddev-behind-fritzbox-routers/index.md): Your project starts, but the browser cannot find `myshop.ddev.site`. The Fritz!Box's DNS rebind protection is swallowing the answer — here is the one-line exception that fixes it. # Contribute with DDEV While [Shopware 6](https://github.com/shopware/shopware) comes with a predefined Docker setup file, there is a good reason to use DDEV for your local contribution environment: It can be booted in parallel with other DDEV projects. In fact, I often have one or more client projects up and running at the same time as a contribution environment. Within your contribution project folder, initiate DDEV with the following command: ``` ddev config --project-type=shopware6 --docroot=shopware/public --composer-root=shopware --web-working-dir=/var/www/html/shopware \ --database=mysql:8.4 --php-version=8.3 --nodejs-version=22 --webserver-type=apache-fpm \ --web-environment-add="DATABASE_URL=mysql://db:db@db:3306/db,MAILER_DSN=smtp://localhost:1025?encryption=&auth_mode=,APP_URL=\${DDEV_PRIMARY_URL},APP_DEBUG=1,APP_ENV=dev" ``` Adjust the php and nodejs versions to the current requirements. Start DDEV and ssh into the web container: ``` ddev start ``` Clone your fork of Shopware: ``` rm -rf shopware && git clone git@github.com:/shopware.git ``` Install the dependencies: ``` ddev exec composer install ``` Create a .env file and the jwt secrets: ``` ddev exec bin/console system:setup ``` The database user, password, host and database are all the same, just: 'db'. I recommend to go for a "dev" setup. Initialize the database and build the administration and storefront components: ``` ddev exec composer setup ``` Voilá, you are set! Run ``` ddev launch /admin ``` to bring up the admin UI in your web browser. The admin credentials, as usual are 'admin' and 'shopware'. Happy contributing! # DDEV behind Fritz!Box Routers `ddev start` succeeds, but the browser answers `https://myshop.ddev.site` with "server IP address could not be found". Nothing is wrong with your project — the Fritz!Box is swallowing the DNS answer, and being the default router in so many German homes and offices, it hits us here disproportionately often. `ddev.site` is a real domain whose wildcard record resolves to `127.0.0.1`. A Fritz!Box discards DNS answers that point from a public name into the local network — *DNS rebind protection* — and cannot tell your project apart from an attack. Confirm with `ping -c 1 myshop.ddev.site`: "No address associated with hostname" means the name never resolved. ## The fix In the Fritz!Box interface at , go to **Home Network → Network → Network Settings → Additional Settings → DNS Rebind Protection** (*Heimnetz → Netzwerk → Netzwerkeinstellungen → Weitere Einstellungen → DNS-Rebind-Schutz*) and enter `ddev.site` in the **Host name exceptions** field (*Hostname-Ausnahmen*). That one entry covers every project, since they all live under `*.ddev.site`. It takes effect immediately; add a custom `project_tld` on its own line if you use one. If you cannot change the router, DDEV's [troubleshooting guide](https://docs.ddev.com/en/stable/users/usage/troubleshooting/#dns-rebinding-prohibited-mostly-on-fritzbox-routers) lists the alternatives — a public resolver such as `1.1.1.1`, or living with DDEV's `/etc/hosts` fallback, which is what the recurring sudo prompt on `ddev start` comes from. # Fetch Media from Production or Staging Server The following redirect or proxy directives will load all media and thumbnail files not available locally from a staging or production server. When using apache (`webserver-type: apache-fpm`) add these lines at the beginning of your `public/.htaccess` file: ``` RewriteEngine On RewriteCond %{REQUEST_URI} ^/(media|thumbnail)/ RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ https://staging.example.com/$1 [L,R=301] ``` Do not commit this updated public/.htaccess file to your repo. When using nginx (`webserver-type: nginx-fpm`), create a file `.ddev/nginx/media-redirect.conf` with the following contents (with optional basic auth): ``` set $media_proxy_url "https://staging.example.com"; location @mediaserver { resolver 1.1.1.1; proxy_pass $media_proxy_url$request_uri; proxy_set_header Authorization "Basic your-basic-auth-secret"; } location ^~ /media/ { access_log off; expires max; try_files $uri $uri/ @mediaserver; break; } location ^~ /thumbnail/ { access_log off; expires max; try_files $uri $uri/ @mediaserver; break; } ``` The nginx way obviously has the advantage that it can be committed to your repository and, once setup, doesn't need any further attention. Also, as nginx acts as a proxy, you can upload files locally in admin and use them seamlessly in the storefront. Also see [Media URLs Out Of Sync between Servers](https://notebook.vanwittlaer.de/hosting/media-urls-out-of-sync-between-servers/index.md). # Frontend Work with Claude Code and Playwright Install the Playwright DDEV add-on to your project: ``` ddev add-on get codingsasi/ddev-playwright ddev restart ddev install-playwright ``` Add a file `.mcp.json` to your project root folder (or extend it if already existing): ``` { "mcpServers": { "playwright": { "command": "npx", "args": [ "-y", "@playwright/mcp@latest", "--headless", "--browser=chromium", "--no-sandbox", "--ignore-https-errors", "--isolated" ], "env": { "PLAYWRIGHT_BROWSERS_PATH": "/opt/playwright-browsers" } } } } ``` Tell Claude to use Playwright for browser preview and screenshots. # Less than 5 Minutes Install with DDEV and Symfony Flex ### Why use DDEV with Symfony Flex template? If you’re used to using DDEV to run your local projects, you’ll find it easy and straightforward to use the new Symfony Flex template to develop and contribute to client projects. While the template comes with its own Docker setup, one advantage of using it within DDEV is that you do not need a local composer environment. In fact, the only prerequisites on your local machine are Docker and [DDEV](https://ddev.readthedocs.io/en/stable/). Not to mention the benefits of DDEV’s ease of use and flexibility. ### Scripted Install The first thing to do is create a project folder, let’s call it my-project, and cd into it. The name of this folder will be the project name used with DDEV. The easiest and quickest way for the install is to copy my installation script [install.sh](https://gist.github.com/vanWittlaer/def64280969ff3e8d2934ac03b206828) file from Github into your project folder. Its contents are: install.sh ``` #!/usr/bin/env bash # What it does: # - Creates a local Shopware installation using the Symfony Flex template for local development of client projects # - Prerequisites: ddev installed on your machine, see https://ddev.com/get-started/ # - See also: https://docs.ddev.com/en/stable/users/quickstart/#shopware # # How to use: # - create a project folder # - copy this file into the project folder # - chmod +x install.sh # - ./install.sh set -e echo "Config ddev project (remember to adjust php and nodejs versions to your Shopware release!) ..." ddev config --project-type=shopware6 --docroot=shopware/public --web-environment="APP_ENV=dev" \ --web-working-dir=/var/www/html/shopware --composer-root=shopware ddev start echo "Composer allow plugin php-http/discovery (note this is temporary fix - to be removed later" ddev composer config --global allow-plugins.php-http/discovery false echo "Composer create-project shopware ..." ddev composer create-project shopware/production -n echo "Installing Shopware ..." ddev exec bin/console system:install --basic-setup --shop-locale=de-DE ``` Then run the following command: ``` chmod +x install.sh && ./install.sh ``` When the installation script is finished, you will have - a current version of Shopware installed in the folder `shopware.` - Use the **ddev ssh** command to ssh into the project’s web container. - Browser-wise, your project storefront can be reached at `https://my-project.ddev.site`, - the project admin can be found at `https://my-project.ddev.site/admin` - or use the command `ddev launch /admin` to launch it directly in your preferred browser. - The admin credentials are admin and shopware. ### The Script Explained The first command – `ddev config` – creates a configuration file for your DDEV project with these settings: - `--project-type=shopware6` – this tells DDEV to setup a Shopware 6 type project. DDEV will create a `.env.local` file within the project's `shopware` folder with all relevant settings - `--docroot=shopware/public` – this directive tells the webserver where to find the web root - `--web-environment="APP_ENV=dev"` - read all environment variables from `.env.dev` - `--web-working-dir=/var/www/html/shopware` – this defines the working dir for the web service - `--composer-root` - if you want to use the ddev composer command, this tells DDEV where to find your composer.json file The next command – `ddev start` – starts your project containers. Shopware itself then is installed with this command ``` ddev composer create-project shopware/production -n ``` Finally, the command ``` ddev exec bin/console system:install --basic-setup --shop-locale=de-DE ``` initializes the Shopware system and database. Carefully chose your shop locale here (as either `de-DE` or `en-GB`) as it might be a bit tricky to change it afterwards. ### Use Traditional Install for < 6.5 Note that this Symfony Flex based approach is applicable only for Shopware 6.5.0.0 and above. Should you need to install an earlier version, follow the „traditional“ approach, i.e. setup the DDEV project and then use the Shopware-provided installation zip file and install it within your DDEV project. ### Updating Shopware and Plugins With Symfony Flex, updating is now just a matter of running `composer update` – you do not even need Shopware’s `shopware-installer.phar.php`. Just run ``` bin/console system:update:prepare composer update bin/console system:update:finish ``` You may want to restrict the `composer update` command to particular versions, components or plugins. After running `composer update`, check that you recipes are up-to-date with the command `composer recipes`. Apply updates as advised. # Setting up workers and the scheduler as automated tasks At some point it might be desired to mirror your local development or your demo environment as closely as possible to the production environment. When it comes to running Symfony's or Shopware's message queue workers and the scheduler as automated background tasks, this can be achieved easily with DDEV. ### Supervisor DDEV has supervisord implemented as a control system for running automated tasks. DDEV actually uses supervisord itself to schedule internal tasks. So essentially all we need to do is to extend the supervisord worker configuration. ### Defining supervisord Workers To do so, add file `worker.conf` to the folder `.ddev/web-build`: ``` # .ddev/web-build/worker.conf [program:scheduled-task] command=bin/run-worker.sh "scheduled-task:run --time-limit=300 -vv" directory=/var/www/html/shopware numprocs=1 startsecs=0 autorestart=true startretries=10 process_name=%(program_name)s_%(process_num)02d stopwaitsecs=20 redirect_stderr=true stdout_logfile=/var/log/scheduled-task.log [program:messenger-async] command=bin/run-worker.sh "messenger:consume async --time-limit=60 -vv" directory=/var/www/html/shopware numprocs=1 startsecs=0 autorestart=true startretries=10 process_name=%(program_name)s_%(process_num)02d stopwaitsecs=20 redirect_stderr=true stdout_logfile=/var/log/messenger-async.log [program:messenger-failed] command=bin/run-worker.sh "messenger:consume failed --time-limit=300 -vv" directory=/var/www/html/shopware numprocs=1 startsecs=0 autorestart=true startretries=10 process_name=%(program_name)s_%(process_num)02d stopwaitsecs=20 redirect_stderr=true stdout_logfile=/var/log/messenger-failed.log ``` For an explanation of the message queue worker configuration in general, head over to the [Symfony docs](https://symfony.com/doc/current/messenger.html#supervisor-configuration). Should you need or want more than one instance of a particular worker, just adjust the `numproc` directive. ### Create a Self-Restarting Worker Command Note that the `command` directive in the above file does not call the bin/console commands directly. Instead, a self-written shell script `run-worker.sh` is executed. The reason for this is supervisord with DDEV cannot handle processes that kill themselves, such as the `messenger:consume` and `scheduled-task:run` commands when you use the `--time-limit` or `--message-limit` parameters. We need long-running commands for our supervisors, which restart the underlying worker whenever it exits. The solution is a custom script in your project's bin folder `shopware/bin`: ``` #!/usr/bin/env bash # shopware/bin/run-worker.sh # The purpose of this bash script is to provide a long-running, self-restarting command for ddev's supervisord implementation, # while allowing to call Symfony's/Shopware's worker commands in a non-persistent mode while :; do /usr/bin/php /var/www/html/shopware/bin/console $1 -n done ``` This script just uses an endless loop to feed whatever command you provide as an argument to bin/console. ### Activate the Worker Configuration Finally, to activate your worker configuration with DDEV, add a file `Dockerfile.worker` to the folder `.ddev/web-build` ``` # .ddev/web-build/Dockerfile.worker ADD worker.conf /etc/supervisor/conf.d ``` and restart DDEV. ### Verify Workers are Active To verify your workers have been started and are active, simply run the command ``` ddev exec ps -aux ``` You should see your workers being listed (amongst the other running processes): ``` ... developer 2151 1.6 0.4 683836 130576 ? S 14:03 0:01 /usr/bin/php /var/www/html/shopware/bin/console scheduled-task:run --time-limit=300 -vv -n developer 2152 1.1 0.3 683516 120936 ? S 14:03 0:00 /usr/bin/php /var/www/html/shopware/bin/console messenger:consume failed --time-limit=300 -vv -n developer 2383 12.4 0.3 683376 122132 ? S 14:04 0:00 /usr/bin/php /var/www/html/shopware/bin/console messenger:consume async --time-limit=60 -vv -n ... ``` ### Don't Forget to Deactivate the Admin Worker Add a file `z-shopware.yaml` to your `shopware/config/packages` folder: ``` # shopware/config/packages/z-shopware.yaml shopware: admin_worker: enable_admin_worker: false ``` # Performance Tweaks Even with the default settings, you will notice a surprisingly good performance in terms of UI response times. If you want to spice things up a bit, follow these instructions. Add a file `.ddev/php/shopware.ini` with the following content: ``` # /.ddev/php/shopware.ini [PHP] assert.active = 0 opcache.enable_file_override = 1 opcache.interned_strings_buffer = 20 # note contrary to Shopware's recommendations, do not disable validate_timestamps in dev environments # opcache.validate_timestamps = 0 zend.assertions = -1 zend.detect_unicode = 0 realpath_cache_ttl = 3600 ``` These settings just implement Shopware’s performance recommendations . The only exception being the value for opcache.validate_timestamps. Disabling it will result in changes to your source files being un-noticed by the web container. Add a file .ddev/mysql/my.cnf with the following content: ``` # /.ddev/mysql/my.cnf [mysqld] sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' group_concat_max_len = 320000 ``` The above settings remove the ONLY_FULL_GROUP_BY sql mode and set the group concat max len to 320000. Again, these are Shopware’s recommendations . Eventually, add some lines to your .env.local file: ``` # /shopware/.env.local APP_ENV=prod APP_DEBUG=0 APP_URL=${DDEV_PRIMARY_URL} DATABASE_URL=mysql://db:db@db:3306/db MAILER_DSN=null://null # Performance tweaks SHOPWARE_CACHE_ID= SQL_SET_DEFAULT_SESSION_VARIABLES=0 APP_URL_CHECK_DISABLED=1 ``` See for more details and explanations. # phpstan pro with DDEV Unfortunately, at this point in time, phpstan pro is not fully compatible with DDEV. The following guide thus just describes a workaround, restricted to be used with one DDEV project at a time. First, make sure you have composer required phpstan in your project. Add a file .ddev/docker-compose.phpstan.yaml with the following content: ``` # .ddev/docker-compose.phpstan.yaml services: web: volumes: - ~/phpstan-pro:/tmp/phpstan-fixer environment: - PHPSTAN_PRO_WEB_PORT=11111 ports: - 127.0.0.1:11111:11111 ``` Restart DDEV. Start phpstan with a command like: ``` ddev exec vendor/bin/phpstan analyse --pro ``` You should see something like: ``` Running in Docker? Don't forget to do these steps: 1) Publish this port when running Docker: -p 127.0.0.1:11111:11111 2) Map the temp directory to a persistent volume so that you don't have to log in every time: -v ~/.phpstan-pro:/tmp/phpstan-fixer Open your web browser at: http://127.0.0.1:11111 Press [Ctrl-C] to quit. ``` Now simply point your browser to - after login/registration, you will be redirected to phpstan pro's dashboard. # RabbitMQ with DDEV Before using RabbitMQ in a production environment, it may be useful to do a local installation to familiarize yourself with the setup. A local installation also allows for easy debugging in case of runtime problems. ### Setup RabbitMQ with DDEV To setup RabbitMQ in your DDEV environment, follow the instructions on . Basically, it comes down to executing the command ``` ddev get b13/ddev-rabbitmq && ddev restart ``` For our needs, the basic RabbitMQ configuration settings provided with this recipe are sufficient. ### Enable RabbitMQ as Message Transport with Shopware 6 To enable RabbitMQ with Shopware 6 we need to enable the php8.2-amqp extension as a prerequisite. For this, add the following line to your `.ddev/config.yaml` file: ``` webimage_extra_packages: [ php8.2-amqp ] ``` Restart DDEV with `ddev restart` to make this effective. Since its release 6.5, Shopware uses the plain implementation of the Symfony messenger component. Install the Symfony AMQP component, which provides the link between the messenger and RabbitMQ: ``` ddev exec composer require symfony/amqp-messenger ``` Finally, to tell Symfony to use RabbitMQ instead of the default Doctrine transport, add these three lines to your `.env.dev` file: ``` MESSENGER_TRANSPORT_DSN=amqp://rabbitmq:rabbitmq@rabbitmq:5672/%2f/async MESSENGER_TRANSPORT_LOW_PRIORITY_DSN=amqp://rabbitmq:rabbitmq@rabbitmq:5672/%2f/low_priority MESSENGER_TRANSPORT_FAILURE_DSN=amqp://rabbitmq:rabbitmq@rabbitmq:5672/%2f/failed ``` Shopware 6 will now use RabbitMQ as its message queue transport system. ### Monitor and Control RabbitMQ - Management UI The admin user interface for RabbitMQ can be reached on your local machine at: `https://.ddev.site:15673` The default credentials are rabbitmq/rabbitmq. Refer to the [RabbitMQ documentation](https://www.rabbitmq.com/docs) for further details. The DDEV implementation also comes with three easy-to-use commands ``` ddev rabbitmq ddev rabbitmqadmin ddev rabbitmqctl ``` see e.g. `ddev rabbitmqadmin --help` for more details. # Running Claude Code in DDEV ### Why? 1. Running Claude natively on your machine potentially gives it access to everything on it. Run Claude from inside your DDEV container instead, and it is restricted, by nature, to whatever that container can reach. 1. Everyone on the project gets the same version of Claude, with no individual installs to maintain. Why not just use the Docker sandbox? It would give you even stronger isolation, thanks to its built-in firewall. The sandbox, however, cannot run host commands, so Claude won't be able to run `ddev` commands for you — which is often exactly what you want it to do for analysis and testing. For the same reason, it cannot drive Playwright to evaluate and test the frontend, something Claude Code does easily from inside the DDEV container. ### The ddev-claude-code add-on #### Install the add-on You can simply install this as a DDEV add-on in your project. Just run ``` ddev add-on get vanWittlaer/ddev-claude-code ``` This add-on also facilitates committing and signing commits from within the container. Please note that there are several other DDEV extensions available to enable Claude Code; you can find an overview at . All of these extensions serve specific (and mostly useful) purposes. Ultimately, the decision is yours. #### Restart DDEV and start Claude Code - `ddev restart` - Start claude with `ddev claude`. - The first time you will have to authenticate with your Claude account. Follow Claude instructions. #### Committing and Signing To allow Claude Code to commit to the project repo, you need to add a file `config.git-signing.local.yaml` to your `.ddev` folder. Also, run `ddev auth ssh` to forward your local private key to the container. Both are documented in the add-on's [readme](https://github.com/vanWittlaer/ddev-claude-code/blob/main/README.md). #### Using PhpStorm's Claude Code Plugin If you use PhpStorm's Claude Code plugin (highly recommended), open your IDE settings, navigate to Claude Code and set `ddev claude` as the Claude command. # Storefront and Admin Watchers with DDEV I have successfully tested the admin and storefront watchers with DDEV in apache-fpm and nginx-fpm mode up to Shopware versions 6.6.7.1, 6.6.10.4 and 6.7.4.2 to 6.7.7.1. Please note that the Storefront Watcher is not compatible with DDEV in Shopware versions 6.6.8.0 to 6.6.8.2. Please also note that Shopware's watcher implementations, as of version 6.7.4.2, still seem somewhat immature to me, so exercise caution when following the instructions provided here. I have not verified the use of watchers with versions 6.7.1 to 6.7.3. **I strongly recommend using the watchers provided with Shopware CLI** (I haven't tested the ones provided by Shopware with 6.7.4.2 or later). ### Using the Watchers with 6.7.4.2 and above #### Expose Ports Add the file `.ddev/config.watcher.yaml` with the following contents: ``` web_environment: - HOST=0.0.0.0 - PROXY_URL=${DDEV_PRIMARY_URL}:9998 - STOREFRONT_SKIP_SSL_CERT=true web_extra_exposed_ports: - name: vite-admin container_port: 5173 http_port: 5172 https_port: 5173 - name: storefront-proxy container_port: 9998 http_port: 8888 https_port: 9998 - name: storefront-assets container_port: 9999 http_port: 8889 https_port: 9999 ``` #### Admin Watcher The current version of the Shopware admin watcher is practically unusable with DDEV because it dynamically adds a port for each of your plugins. To avoid this problem, use the standalone Admin Watcher provided with Shopware CLI (see here for details). Start the standalone watcher with this command: ``` shopware-cli extension admin-watch custom/static-plugins// \ https://.ddev.site \ --listen :5173 \ --external-url https://.ddev.site:5173 ``` Or you may want to add a custom DDEV command for this. To do so, add a file `.ddev/commands/web/admin-watch` with this contents: ``` #!/usr/bin/env bash ## Description: Run admin watcher for a plugin. Note only works for plugins in custom/static-plugins. ## Usage: admin-watch set -e if [ -z "$1" ]; then echo -e "Verwendung: ddev admin-watch \n" exit 1 else PLUGIN_NAME="$1" fi PLUGIN_DIR="custom/static-plugins/${PLUGIN_NAME}" if [ ! -d "$PLUGIN_DIR" ]; then echo "Error: Plugin directory '${PLUGIN_DIR}' does not exist." exit 1 fi if [ ! -f "$PLUGIN_DIR/composer.json" ]; then echo "Error: No composer.json found in '${PLUGIN_DIR}'." exit 1 fi shopware-cli extension admin-watch ${PLUGIN_DIR}/ \ https://${DDEV_HOSTNAME} \ --listen :5173 \ --external-url https://${DDEV_HOSTNAME}:5173 ``` Simply run this from your terminal with `ddev admin-watch ` and open the provided URL. #### Storefront Watcher Start the storefront watcher with this command ``` shopware-cli project storefront-watch ``` ### For 6.7.0.0 or earlier see the following sections First of all, create a file `.ddev/config.watcher.yaml` with the following content (note this is for 6.7.0.0, see below for a slightly different file for 6.5 and 6.6). ``` # .ddev/config.watcher.yaml for 6.7.0.0 and above web_environment: - HOST=0.0.0.0 - ADMIN_PORT=9997 - PROXY_URL=${DDEV_PRIMARY_URL}:9998 - STOREFRONT_SKIP_SSL_CERT=true web_extra_exposed_ports: - name: admin-proxy container_port: 9997 http_port: 8887 https_port: 9997 - name: storefront-proxy container_port: 9998 http_port: 8888 https_port: 9998 - name: storefront-assets container_port: 9999 http_port: 8889 https_port: 9999 ``` For 6.5 and 6.6, your `.ddev/config.watcher.yaml` should look like this: ``` # .ddev/config.watcher.yaml (Shopware 6.5 and 6.6) web_environment: - HOST=0.0.0.0 - PORT=9997 - DISABLE_ADMIN_COMPILATION_TYPECHECK=1 - PROXY_URL=${DDEV_PRIMARY_URL}:9998 - STOREFRONT_SKIP_SSL_CERT=true web_extra_exposed_ports: - name: admin-proxy container_port: 9997 http_port: 8887 https_port: 9997 - name: storefront-proxy container_port: 9998 http_port: 8888 https_port: 9998 - name: storefront-assets container_port: 9999 http_port: 8889 https_port: 9999 ``` These directives tell the DDEV router which additional ports to route to the container for the watchers. The web_environment directive adds the extra HOST and ADMIN_PORT environment variables required by the admin watcher hot proxy. The PROXY_URL and STOREFRONT_SKIP_SSL_CERT directives are required by the storefront hot-reload watcher. Note that PROXY_URL requires the port to be specified explicitly. Note: DISABLE_ADMIN_COMPILATION_TYPECHECK=1 is the default in newer Shopware versions (from about 6.6.7.0). So it's possible to omit it in this case. Don't forget to restart your project with `ddev restart`. #### Admin Watcher Start the admin watcher with `bin/watch-administration.sh` (production template) or `composer run watch:admin` (contribution template). To reach the admin watcher, point your browser to `https://.ddev.site:9997` (omit the /admin slug!). You must use your primary DDEV url - using localhost or a docker IP address (like 172.19.0.2) will not work. If you have any custom or third party plugins that supply admin components (like SwagPayPal), you will note, in your browser's console, a number of "mixed content" errors, as the watcher attemps to load plugin assets via http and native IPs. To avoid these errors, you need to configure your browser to allow to show insecure content. If you use the Chrome browser or a similar one, point your browser to `chrome://settings/content/insecureContent` and add your DDEV sites `[*.]ddev.site` to the list of allowed sites. Screenshot of Chrome settings to allow insecure content for selected sites #### Storefront Watcher Start the storefront watcher with `bin/watch-storefront.sh` (production template) or `composer run watch:storefront` (contribution template). To reach the storefront watcher point your browser to `https://.ddev.site:9998`. #### Media Requests with HTTP and the (blocked:mixed-content) Error **The following applies to all versions prior and including Shopware 6.6.7.1 only.** Please note that Shopware has refactored the Webpack hot proxy for the storefront watcher for newer versions to ensure consistent use of the HTTPS protocol with the storefront watcher. In the Storefront Watcher you will notice that all requests for media files are using the HTTP scheme instead of HTTPS. I don't know of a fix for this at the moment, but you can avoid the consequences with a workaround. Firstly, you need to define a sales channel domain `http://.ddev.site` in addition to the https domain. Second, the media requests using HTTP instead of HTTPS will cause Chrome (and other browsers) to block them. Simply add your \*.ddev.site urls to Chrome's list of sites that are allowed to display unsafe content - see screenshot below. To do so, point your browser to `chrome://settings/content/insecureContent` and add a site `[*.]ddev.site` to be allowed to show insecure content. Screenshot of Chrome settings to allow a site to use insecure content # Using shopware-cli with DDEV For basic information on shopware-cli see [Shopware docs](https://developer.shopware.com/docs/products/cli/). To install shopware-cli with your DDEV project, run the command below to install the shopware-cli add-on: ``` ddev add-on get vanWittlaer/ddev-shopware-cli ddev restart ``` This will also install a config.watcher.yaml to define the necessary ports for using the [storefront and admin watchers](https://notebook.vanwittlaer.de/ddev-for-shopware/storefront-and-admin-watchers-with-ddev/index.md) with shopware-cli. Use it with one of the commands below: ``` ddev shopware-cli --version # any shopware-cli command ddev admin-watch custom/static-plugins/MyPlugin # administration watcher (Vite) ddev storefront-watch # storefront watcher ``` Always open the watchers via your **primary DDEV URL** (`https://.ddev.site`), never `localhost`. The admin-watch URL omits the `/admin` slug. # Why use DDEV? When it comes to setting up and running a Shopware project locally, there's no shortage of good options — not least Shopware's own shopware-cli, which keeps gaining project setup and management features. Shopware's goal is clearly to make shopware-cli the one-stop tool for Shopware development. So why might you still want to reach for DDEV? It helps to look at this from two perspectives: the individual developer/freelancer, and the team/agency. ## Developers' benefits - **Easy to install** — The only requirements are git, Docker and DDEV. On macOS, "Docker" can simply be OrbStack, and DDEV is a small Go app installed with a single install script. - **Keeps your machine clean** — Project dependencies such as PHP, MySQL and Node.js stay where they belong: inside the project, not scattered across your system. - **Easy to set up** — A fresh Shopware install takes just two commands, `composer create-project` and `bin/console system:install` — or, shorter still, a single copy-and-paste [install script](https://notebook.vanwittlaer.de/ddev-for-shopware/less-than-5-minutes-install-with-ddev-and-symfony-flex/index.md). - **Excellent performance** — Built on Docker (OrbStack on macOS), with performance tooling such as Mutagen built in. - **Project isolation** — Freelancers often juggle several client projects at once. With DDEV, each project runs in its own fully isolated environment by default — no fiddly trial-and-error with port numbers. - **Trusted HTTPS** DDEV uses traefik and mkcert to conduct all your artifacts with locally trusted HTTPS. - **Cross-project connectivity** Projects can communicate with each other via the DDEV provided local URLs. A crucial feature e.g. for development of Shopware apps, when you want to test your server with a local Shopware client. - **xdebug out-of-the-box** With phpstorm, using xdebug is just one command `ddev xdebug on` away. (With VSCode some setup, approx. 2 min., is required.) ## Agencies' benefits - **Low barrier to entry, fast onboarding** — There's very little a team member needs to know beyond `ddev start` and `ddev stop`, all covered by the community-maintained [documentation](https://docs.ddev.com/en/stable/). Getting onto an existing project is just `git clone` and `ddev start`; built-in commands like `ddev pull remote-db` fetch what isn't in the repo, and an [nginx reverse proxy](https://notebook.vanwittlaer.de/ddev-for-shopware/fetch-media-from-production-or-staging-server/index.md) loads and caches production media on the fly — no need to sync gigabytes of files to every machine. - **Consistent, reproducible environments** — Every detail of the setup is committed to the project repository, so every change is clearly documented and versioned. No more drifting PHP or Node.js versions between team members. - **Simple by default, yet able to mirror production** — DDEV strikes a good balance: trivial to run for a basic project, but ready to reproduce a complex production stack locally when you need to. Extra services such as Redis, [RabbitMQ](https://notebook.vanwittlaer.de/ddev-for-shopware/rabbitmq-with-ddev/index.md), Elasticsearch/OpenSearch and more are a few lines of config or a ready-made add-on away, so your local environment matches production instead of merely approximating it. - **Instant client demos** With a single command `ddev share` projects can be accessed across the web using a tunnel provider like ngrok or cloudflared. Teams can thus easily share their achievements with clients on-the-fly, without running deployments and demo server setups. Also read Randy's (lead maintainer of DDEV) [blog article on opensource.com](https://opensource.com/article/22/12/ddev). # Hosting # Hosting - [How to Trouble-shoot and Evaluate Environment Variables](https://notebook.vanwittlaer.de/hosting/how-to-trouble-shoot-and-evaluate-environment-variables/index.md): This page explains how to trouble-shoot and evaluate environment variables - [Media URLs Out Of Sync between Servers](https://notebook.vanwittlaer.de/hosting/media-urls-out-of-sync-between-servers/index.md): This page explains how to solve the "out-of-sync" problem with media URLs on your different environments. - [RabbitMQ](https://notebook.vanwittlaer.de/hosting/rabbitmq/index.md): This page describes the necessary steps to set up and activate RabbitMQ as a message transport with Shopware 6. - [SFTP-Users](https://notebook.vanwittlaer.de/hosting/sftp-users/index.md): This page explains how to set up native SFTP - if it is really required. - [Basic-Auth for Shopware 6 with nginx](https://notebook.vanwittlaer.de/hosting/basic-auth-for-shopware-6-with-nginx/index.md): This page explains how to establish basic-auth for Shopware 6 when using nginx (instead of Apache). - [How to Avoid Failed Systemd Units](https://notebook.vanwittlaer.de/hosting/how-to-avoid-failed-systemd-units/index.md): This page explains how to ensure that the systemd units used to maintain Shopware's task runner and message consumer do not fail permanently. - [JWT-Secrets as Environment Variables](https://notebook.vanwittlaer.de/hosting/jwt-secrets-as-environment-variables/index.md): When using Shopware 6.6 in a dockerized or cluster setup, you may want to use env vars in lieu of files for your JWT keys. This is how convert your keys. - [How to generate INSTANCE_ID and APP_SECRET](https://notebook.vanwittlaer.de/hosting/how-to-generate-instance_id-and-app_secret/index.md) - [PaaS with Coolify](https://notebook.vanwittlaer.de/hosting/paas-with-coolify/index.md): If you're looking for affordable, high-performance, low-maintenance hosting, Coolify could be the solution. Coolify makes running your shop as easy as switching lights on and off. # Basic-Auth for Shopware 6 with nginx Unlike Apache2, where basic-auth is set in the local .htaccess file, in nginx it is set in the server's vhosts file. The downside is that it is difficult to integrate into the CI/CD pipeline. The upside is that it is defined per server by definition and thus doesn't require CI/CD procedures to take care of what the deployment's target (e.g. production, staging, ...) is. First of all, create an .htpasswd file suiting your needs. A variety of online tools exist for this task. If you have access to a Unix/Ubuntu system, the clean way to go is to run the hdpasswd command: ``` htpasswd -c .htpasswd ``` - `-c` tells `htpasswd` to create a new file. Omit it if you want to add further users to an existing file. - `.htpasswd` is the name of the htpasswd file. You may choose any name; `.htpasswd` is just common practice. - `` is the user to be used for basic auth. htpasswd will prompt you for the basic auth password. Then locate and edit the nginx vhost file. At its head, preceding the server section, add the following lines: ``` map $request_uri $auth_type { default "off"; ~/api/* "off"; ~* "vanWittlaer Demo Site"; } ... ``` Choose any text you like in place of `vanWittlaer Demo Site` — it is displayed on the basic-auth login popup. The above code sets a variable $auth_type to either "off" or to "vanWittlaer Demo Site" (or any other text you chose). Further down in the vhost file, and inside the server section, add the following two lines: ``` ... auth_basic $auth_type; auth_basic_user_file /.htpasswd; ... ``` # How to Avoid Failed Systemd Units If you have configured your Shopware 6 using systemd as described [here](https://developer.shopware.com/docs/guides/hosting/infrastructure/message-queue.html#systemd-example), and you see your message queues building up and messages not being processed anymore, it's time to take a look at the status of your systemd services (or "units" as they are also called). Presuming all your systemd units for Shopware start with the prefix `shopware`, check the status of your units using the following command: ``` sudo systemctl list-units | grep shopware ``` All relevant units should be in the status `loaded active running`. If one or more units are not, you may safely restart those with ``` sudo systemctl restart ``` But why would a unit fail permanently? The point is that the `Restart=always` directive works fine in general, but by default systemd will only try to restart a failed unit five times, and the default time between attempts is only 100 ms. Now suppose your database (or redis or RabbitMQ, whatever is configured for the message queues) is not responding properly for some seconds, or even has a short downtime. The task runner and message consumers are reading from the database (or redis or RabbitMQ) and will inevitably fail. Now, systemd will only retry five times within a total of 500 ms, making it very likely that the unit will end up in a permanently failed state. You can increase the resilience of your units to such failures by Increase the number of retries and the time between retries. To do this, add this statement ``` StartLimitBurst=10 StartLimitIntervalSec=30s ``` to the `[Unit]` section and add this one ``` RestartSec=2s ``` to the `[Service]` section. This causes systemd to attempt to restart 10 times, with 2 seconds between retries, within a maximum of 30 seconds. It shouldn't be a problem for the task runner or message consumers to be down for 2 seconds or up to 30 seconds if the queues are reliably restarted at the end. # How to generate INSTANCE_ID and APP_SECRET INSTANCE_ID ``` openssl rand -hex 32 ``` APP_SECRET - there is a command provided by Shopware ``` bin/console system:generate-app-secret ``` Run this in any Shopware project, like your local development environment and copy/paste the output. # How to Trouble-shoot and Evaluate Environment Variables ### Environment Variables set in the Command Line The first thing to note is that any setting for a variable on the command line overrides what is defined in any .env file. So, depending on your problem, check your crontabs and scripts for any 'hidden' variable settings. ### Is there a .env.local.php File? An .env.local.php file is generated by the \`bin/console dotenv:dump' command, typically within your deployment pipeline. If an .env.local.php file exists, then its definitions take precedence over any other .env file. Either apply your changes to the .env.local.php file (which makes them non-persistent), or delete the .env.local.php file and apply your changes to .env. .env.local, .env.dist or .env.dist.local (depending on whether you want your changes to eventually go to the repo or not). ### Evaluate .env Files The `bin/console debug:dotenv` command is your friend. Remember, though, that anything shown here will be overwritten by one of the two methods above - and, worst of all, its effects won't be shown here! # JWT-Secrets as Environment Variables Note this article is obsolete with 6.6.1.0. SInce 6.6.1.0 you can are encouraged to use the following configuration for your JWT secrets: ``` shopware: api: jwt_key: use_app_secret: true ``` The above configuration however is already deprecated with 6.6.x and has been removed with 6.7.0.0, since this method (using the APP_SECRET) is the only one being used - no configuration for JWT is required anymore. Make sure you have an APP_SECRET with sufficient entropy and length. You may want to create it yourself with the help of PHP: ``` php -r "echo bin2hex(random_bytes(32));" ``` Run this command in any environment where you have PHP available (like your DDEV web container) and copy the generated string into your .env.local file. Do not commit the app secret to git. For Shopware versions below 6.6.1.0 you may want to follow the instructions below: In your shopware.yaml (or any other valid configuration file), create or amend the following entry: ``` shopware: api: jwt_key: private_key_path: '%env(base64:JWT_PRIVATE_KEY)%' public_key_path: '%env(base64:JWT_PUBLIC_KEY)%' ``` The easiest way to generate JWT secrets is using shopware-cli with the following command: ``` shopware-cli project generate-jwt --env ``` If you have already generated your file-based JWT keys, use these commands to create the base64 equivalent to be specified in .env: ``` cat shopware/config/jwt/private.pem | base64 | tr -d '\n' cat shopware/config/jwt/public.pem | base64 | tr -d '\n' ``` and paste the corresponding results as values for JWT_PRIVATE_KEY and JWT_PUBLIC_KEY, respectively, in your .env.local. # Media URLs Out Of Sync between Servers ### tl;dr If you are getting 404 responses for all your media files in the local, dev or staging environment, the most likely case is a mismatch in Shopware's media pathname strategies between environments. The cure is simply to define the SHOPWARE_CDN_STRATEGY_DEFAULT environment variable in all environments. In many cases, 'Id' is the correct choice, however this needs to be verified. Add one of the lines below to your .env files for all environments: ``` # .env # SHOPWARE_CDN_STRATEGY_DEFAULT="physical_filename" - or - SHOPWARE_CDN_STRATEGY_DEFAULT="id" ``` Should broken URLs persist after having determined and correctly set your strategy, you may need to run the following command: ``` bin/console media:update-path --force ``` ### The Problem When you set up your local environment or a staging server as a clone of your production server, you are essentially copying your code sources, your database and all your media files to your cloned environment. Since media files can take up a lot of disk space, one way to avoid having to maintain all your media files locally is to simply retrieve them using a redirect, as in this .htaccess example: ``` RewriteEngine On RedirectMatch 301 ^/media/(.*)$ https:///media/$1 RedirectMatch 301 ^/thumbnail/(.*)$ https:///thumbnail/$1 ``` Now, whether you've copied your media folders or you're using the redirect approach, sometimes this doesn't seem to work. Instead, the media URLs are returning 404s. If you analyse the problem, you will quickly realize that the hashed path to the exact same media file is different in your local environment and on your production server. For example ``` # locally .../a1/b2/c3/myPicture.jpg # server .../1c/2b/3a/myPicture.jpg ``` ### Shopware's Media Pathname Strategies So what happens? Shopware generates the media paths using a pathname strategy. The relevant classes can be found in Shopware's core: `core/Content/Media/Core/Strategy`. Basically there are four predefined strategies: `id`, `filename`, `physicalFilename` and `plain`. The id strategy simply creates a path based on the md5 hash of the mediaId. Thus there is no relationship between the filename and the generated path, it is impossible to 'guess' the path just knowing the filename. The filename strategy does the same, but uses the filename as the basis for the hash. It is not possible to have two physical files sharing the same filename with this strategy. The physicalFilename strategy adds the uploadedAt timestamp to the filename to create the hash, thus allowing multiple files with the same filename. Without going into detail, if you are trying to redirect your media requests to another server, or if you copied your media folder from another server, you need to make sure that both your local environment and the server are using the same pathname strategy. However, as the default strategy changed from id in "ancient" versions to physicalFilename in 6.5 (as far as I know), you may have to explicitly define the strategy when using existing media folders from older installations. ### Recommended Action Check whether SHOPWARE_CDN_STRATEGY_DEFAULT was/is defined in all your environments. For older installations (pre 6.5), the default value might have been 'Id'. To enable the correct strategy, define and set the SHOPWARE_CDN_STRATEGY_DEFAULT variable in your .env file. Remember to do this in your local env as well as on your server. If broken URLs persist, run `bin/console media:update-path --force` after correcting the strategy setting. # PaaS with Coolify Think of Coolify as a management tool for your servers and all the services you need. Coolify provides an intuitive, user-friendly interface to help you maintain control. Behind the scenes, Coolify uses Docker to run your services, whether they are web applications, transactional databases (MySQL, MariaDB, MongoDB, etc.) or other utilities such as Redis and RabbitMQ. You don't need to know anything about Docker to use Coolify. It's as simple as turning a light on or off — you don't need to be an engineer or physicist to do that. To use Coolify, you either need an account with Coolify's cloud or you host your own Coolify admin instance (the latter requires some technical knowledge). Your shop then runs on a basic Linux server — the easiest and cheapest option is one from a cloud provider such as Hetzner or DigitalOcean. You check that server in with Coolify, and from there Coolify takes control and sets up everything it needs. ## The services a Shopware shop needs A production Shopware installation typically relies on: - a MySQL or MariaDB database, - two Redis instances (cache and session), - a RabbitMQ instance, - an (optional) OpenSearch or Elasticsearch instance. On top of that you need the services that run Shopware itself: a web server to handle requests and one or more workers for offline tasks. The easiest way to build these is [Shopware Docker](https://github.com/shopware/docker), which produces a production-ready image (Caddy or Nginx plus PHP-FPM) from your source code. Wiring all of this together by hand in the Coolify UI is tedious and error-prone. The two tools below do it for you. ## Provisioning the stack: `terraform-coolify-shopware-stack` is an OpenTofu module that provisions the **entire** Shopware environment on Coolify in a single `tofu apply`: the web application (with workers and scheduler), MariaDB, two Redis instances, RabbitMQ, Elasticsearch, and optional extras such as Mailpit and a backup sidecar. The optional backup sidecar is the [`shopware-ops-shell`](https://github.com/vanWittlaer/shopware-ops-shell) image — a lightweight maintenance container that ships the tools the official Shopware image lacks (`bash`, `shopware-cli`, `rclone`). It runs idle and you trigger its scripts on a schedule (Coolify Scheduled Tasks, `docker exec` or cron): - `bin/backup-db.sh` dumps the database via `shopware-cli project dump`, pushes it to an S3 bucket and prunes old dumps per `DB_BACKUPS_TO_KEEP`. - `bin/backup-s3.sh` mirrors the public and private media buckets offsite with soft-delete retention. It follows a **day-0 bootstrap** model: you provision the environment once, and after that the **Coolify UI is the single source of truth**. You don't keep re-applying the module against a running system — from then on you manage and upgrade through Coolify. You'll need OpenTofu ≥ 1.7, a Coolify v4 control plane with credentials, pre-built Docker images in a registry, S3 buckets, and DNS in place. The README covers the variables, the prerequisites, and the handful of one-time manual steps after the first apply. ## Driving it from DDEV: `ddev-coolify-shopware` If you already use DDEV for local development (and the [swoofy](https://github.com/vanwittlaer/swoofy) blueprint below assumes you do), the [`ddev-coolify-shopware`](https://github.com/vanWittlaer/ddev-coolify-shopware) add-on is the friendlier front door. It installs OpenTofu into the web container and adds a `coolify-bootstrap` command that wraps the module above: ``` ddev add-on get vanWittlaer/ddev-coolify-shopware ddev coolify-bootstrap init # scaffold the tfvars templates # …fill in secrets.auto.tfvars and your per-environment .tfvars… ddev coolify-bootstrap up # provision production and staging ddev coolify-bootstrap destroy # tear it all down again ``` Same day-0 contract applies: it refuses to bootstrap a project that already exists in Coolify, so it won't clobber changes you made in the UI. ## A complete blueprint For a full end-to-end example — the Shopware application itself, `shopware/docker`, `frosh/tools`, the DDEV setup, and the Docker/compose files for the web and worker services — see my reference implementation, [swoofy](https://github.com/vanwittlaer/swoofy). Used together with the two tools above, it gives you a straightforward CI/CD assembly line for a typical Shopware 6 client project: DDEV locally, Coolify for staging and production, driven by GitHub workflows (or GitLab pipelines). The full instructions live in each repository's `README.md`. Any feedback is appreciated. Have fun! # RabbitMQ **Note:** The below instructions are pretty much outdated - use [Coolify-based server setups](https://notebook.vanwittlaer.de/hosting/paas-with-coolify/index.md) instead. ### Get and Setup RabbitMQ I recommend running RabbitMQ on a dedicated server. For three reasons: 1. Security - any application on a server adds potential security risks and you do not want it to be used as a backdoor to your actual Shopware 6 (web) server. 1. Complexity - an application running on a dedicated server is easier to maintain and operate. 1. Performance - a dedicated server can be easily optimized, e.g. for CPU or RAM for RabbitMQ. If you are running your setup in Digital Ocean's cloud, you can spin up a server with a pre-configured RabbitMQ image. This is my recommended and certainly the easiest way to go. Otherwise, follow the [instructions for setting up RabbitMQ](https://www.rabbitmq.com/docs/download) on your server. Create a new non-administrative user `shopware` and grant config/read/write access to the default virtual host '/' ``` sudo rabbitmqctl add_user shopware sudo rabbitmqctl set_permissions -p / shopware ".*" ".*" ".*" ``` Note that you do not need to define any queues on the server, as Symfony will dynamically create the necessary configurations. ### Harden your RabbitMQ Instance with a Firewall RabbitMQ opens quite a few ports. Only two of them should respond to external requests, 5672 (for connections) and 15672 (for the management UI). So set up your firewall to restrict access to these two ports, and most importantly, allow it only from the IP address of your web server. You may also want to limit SSH access. A note about **secure communication**: This tutorial describes a setup where the communication between Shopware and RabbitMQ (via port 5762) is not encrypted. Therefore it is highly recommended to use this setup only in an environment where **both Shopware and RabbitMQ are inside the same VPC** (Virtual Private Cloud). ### Prepare and Enable Shopware 6 to use RabbitMQ First you need to install the AMQP extension on your server. On Debian/Ubuntu run ``` sudo apt install php8.2-amqp ``` and don't forget to restart your fpm service and possibly your message queue worker services. Second, the Symfony amqp-messenger extension must be required in your composer setup. This can be done locally with ``` composer require symfony/amqp-messenger ``` Commit this to your repository and deploy to your server. Finally, you need to tell Symfony and Shopware to switch to the RabbitMQ transport. To do this, add the following lines to your `.env` file: ``` # .env or .env.local or .env..local MESSENGER_TRANSPORT_DSN=amqp://shopware:@:5672/%2f/async MESSENGER_TRANSPORT_LOW_PRIORITY_DSN=amqp://shopware:@:5672/%2f/low_priority MESSENGER_TRANSPORT_FAILURE_DSN=amqp://shopware:@:5672/%2f/failed ``` That's it! To disable RabbitMQ as messenger transport, and revert to the default Doctrine transport, just comment out the above lines in your `.env` file. # SFTP-Users Sometimes "legacy" interfaces like Solfusion require access to a folder that can be accessed by SFTP from outside the server. Since Solfusion does not make use of Symfony Flysystem, this must be configured manually. In any event, SSH should be configured to prohibit password login! Create a user \\ and assign it to same group as the webserver. Create a directory for use by sftp, e.g. /var/sftp. Note this must be owned by root! ``` # /etc/ssh/sshd_config ... Subsystem sftp /usr/lib/openssh/sftp-server -u 002 ... # sftp Match User ForceCommand internal-sftp PasswordAuthentication no ChrootDirectory /var/sftp PermitTunnel no AllowAgentForwarding no AllowTcpForwarding no X11Forwarding no ``` - `-u 002` is important to force setting the group permissions during sftp upload. - Replace `` with the name of your sftp user. # Deployment # Deployment - [Close-to-Zero Downtime Deployment](https://notebook.vanwittlaer.de/deployment/close-to-zero-downtime-deployment/index.md): This page explains how to deploy Shopware 6 with close-to-zero downtime. - [PHP Cache Related Issues](https://notebook.vanwittlaer.de/deployment/php-cache-related-issues/index.md): This page discusses issues related to (PHP) caching during deployment and what to do about it. # Close-to-Zero Downtime Deployment The following assumes a local environment and repo structure as outlined in this [page](https://notebook.vanwittlaer.de/ddev-for-shopware/less-than-5-minutes-install-with-ddev-and-symfony-flex/index.md). The outlined procedure is suitable for the CI/CD provided with Gitlab or Bitbucket. Both provide so called "runners", i.e. virtual servers that are used to compile and prepare your sources and to transfer them to your app servers. The procedure described below is a "poor-man's-deployment": For the sake of simplicity, we run the database migration, plugin update and the theme compilation tasks on the server, resulting in a possible downtime in the range of up to 30 seconds. (Thus the "close-to-zero".) CI/CD Deployment happens in a number of steps, which are spread across a runner and a server script. ### Repository Folder Structure This is straight-forward, setting up DDEV and Shopware will create the two folders `.ddev` and `shopware` for you. I have added a third folder `.deploy`, which holds all stuff needed for the deployment, and has a subfolder for each environment (DDEV, stage, production, ...). ``` ├── .ddev ├── .deploy ├── shopware ├── .gitignore └── .gitlab-ci.yml ``` DDEV and Shopware bring their own .gitignore files. I recommend not to touch these files. If you need to add your custom rules to .gitignore, add a .gitignore file on the project level. Below is an example. As you see, I have added an ignore rule for the .htaccess file in Shopware's public folder, as this file may contain some environment specific directives (like htpasswd protection for the stage environment). The deployment script needs to take care to set up the public/.htaccess file based on the Shopware provided public/.htaccess.dist file. ``` # .gitignore /.idea/ /shopware/public/.htaccess /*.sql.gz /*.tar.gz ``` ### Server Folder Structure Point the docroot of your web server's vhost to the folder shopware/current, which is actually a symlink pointing to the current release. In your environment specific shopware.yaml file, configure the appropriate filesystem settings for the private (files) and public (bundles, media, sitemap, theme, thumbnails) filesystems. ``` shopware ├── current -> /var/www/shopware/releases/240216085356 ├── releases │   ├── ... │   ├── ... │   └── 240216085356 └── shared ├── config └── jwt ├── files ├── public ├── var └── log └── .env.prod.local ``` ### The Runner Part Both Bitbucket and Gitlab provide public shared runners, though their usage on free plans is very limited. I prefer to use my own runner. It's not limited, and I know I am the only one using this server. A smaller Hetzner cloud server will do the job. #### The Runner Image A Docker-based runner, as the one I use, needs a Docker image to perform his tasks. The runner image needs to provide all components that are required by Shopware. Obviously, this depends on the Shopware version. On the other hand, the smaller the image, the faster the runner. #### Composer Install and Build This step on the runner installs all dependencies. Make sure you have the auth.json generated with your bearer token (from the Shopware account), to allow installation of store plugins. Note the various environment variables. They control how the build commands are executed. Note that Bitbucket and Gitlab preset the env variable CI with CI=1. The build process does not need to have access to the database. The prequisite for this being that all plugins, including all custom plugins, are Composer installed. ``` - export APP_ENV=prod - export DATABASE_URL="" - export COMPOSER_CACHE_DIR="$(pwd)/composer-cache" - export SHOPWARE_SKIP_BUNDLE_DUMP=1 - export SHOPWARE_SKIP_ASSET_COPY=1 - export SHOPWARE_SKIP_THEME_COMPILE=1 - export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 - export SHOPWARE_ADMIN_BUILD_ONLY_EXTENSIONS=1 # - composer config -d shopware bearer.packages.shopware.com "$BEARER_PACKAGES_SHOPWARE" - composer install -d shopware --no-scripts # - shopware/bin/ci bundle:dump -n - shopware/bin/ci feature:dump -n - shopware/bin/build-js.sh - shopware/bin/ci administration:delete-files-after-build -n ``` #### Completing the Sources, Shipping and Handover to the Server The sources are being completed for environment dependent files, like robots.txt or .htaccess. Eventually rsync ships the sources to a folder \_tmp on the target server. Eventually a script deploy.sh is run on the server. ``` - test -f .deploy/$CI_ENVIRONMENT_NAME/public/robots.txt && mv -f .deploy/$CI_ENVIRONMENT_NAME/public/robots.txt shopware/public/robots.txt - test -f .deploy/$CI_ENVIRONMENT_NAME/public/.htaccess.dist && mv -f .deploy/$CI_ENVIRONMENT_NAME/public/.htaccess.dist shopware/public/.htaccess - cat shopware/public/.htaccess.dist >> shopware/public/.htaccess && rm -f shopware/public/.htaccess.dist - touch shopware/install.lock # - rsync -av -q --rsh="ssh" --rsync-path="sudo -u www-data /usr/bin/rsync" --delete-excluded ./shopware/ $SSH_USER@$SSH_HOST:$DEPLOY_ROOT/_tmp/ # - ssh $SSH_USER@$SSH_HOST "/bin/bash -s " < .deploy/deploy.sh $DEPLOY_ROOT $CI_ENVIRONMENT_NAME ``` ### Server Side On the server side, the first step is move the \_tmp folder created by the runner to a uniquily named folder. Use a time stamp or a commit sha for this purpose. The second step is to set the symlinks pointing to the var/log and the config/jwt folder. Don't forget to symlink to a .env.local file, which holds all server specific secrets and sets the APP_ENV variable. Next run Shopware specific tasks: - database migrations - plugin updates - theme compile - assets install At this point you should ensure that all files and folders created at this point get the correct file permissions (755 for folders, 644 for all files, +x only for commands in the bin folder) and ownership (to the owner/group of the web server). Eventually, set the symlink for the web servers docroot to your new sources folder (shopware/current). To make the change of the symlink effective in the PHP environment, the APCu and opcache need to be reset. How to do this depends on the server setup. Last but not least, cleanup older release folders to avoid drive space congestion on your server. Again, how to do this is server specific. # PHP Cache Related Issues The php-fpm configuration for production servers is and should be optimized to use the various caching options. With PHP 7.x and above, there have been a number of improvements, most notably with the introduction of the new file cache. See [here](https://patrickkerrigan.uk/blog/php-opcache-file-cache/) for a brief overview. However, these new features have side effects: For example, after deploying, you may find that source changes have not taken effect on your server. ### Ignoring Timestamps In most cases, the cause of these issues is related to the ``` opcache.validate_timestamps ``` configuration value. For performance reasons, Shopware recommends to disable it on production servers. See [here](https://developer.shopware.com/docs/guides/hosting/performance/performance-tweaks.html#php-config-tweaks) for details. ### Reset Cache After Deployment The easiest and safest way to completely reset opcache is to use the famous [cachetool](https://github.com/gordalina/cachetool). Just request it with composer in your environment: ``` composer require gordalina/cachetool ``` Then, assuming your server is equipped with php-fpm, resetting opcache is as simple as running the following command: ``` vendor/bin/cachetool opcache:reset --fcgi=/var/run/php-fpm.sock ``` Note that the value of --fcgi depends on your actual web server setup. ### DDEV and other Local Environments In local environments, just make sure that opcache file timestamps are always validated by adding the following directive to your php ini file: ``` opcache.validate_timestamps = 1 ``` # Development # Development - [Install the Shopware Conflicts Package](https://notebook.vanwittlaer.de/development/install-shopware-conflicts/index.md): This page explains why you should add the `shopware/conflicts` metapackage to your project and how it automatically keeps known-incompatible dependency versions out of your installation. - [Patching the Core](https://notebook.vanwittlaer.de/development/patching-the-core/index.md): This page explains, how to use the Composer package cweagans/composer-patches to patch Shopware 6 core modules. - [How to Discover Available Updates?](https://notebook.vanwittlaer.de/development/how-to-discover-available-updates/index.md): This page describes how to discover whether updates are available for any Symfony or Shopware component or any third-party plugin. - [Local Testing of Shopware Commercial Features](https://notebook.vanwittlaer.de/development/local-testing-of-shopware-commercial-features/index.md): This guide assumes you are a registered Shopware partner and have activated the wildcard domains feature. - [Claude Code](https://notebook.vanwittlaer.de/development/claude-code/index.md): A great buddy! You just have to know how to handle him. # Claude Code I have started with a very, very basic and rudimentary skill set for my buddy. Even though it is light-years far from being perfect, my rationale is that any skills for Claude is better than no skills at all. So use it if you like, and help to improve it if you find the time. Claude deserves it. You can find it at . To use it in your project, type the following commands in Claude's prompt: ``` /plugin marketplace add vanWittlaer/sw6-skills /plugin install sw6-skills@sw6-skills ``` Still, you'll have to remind him every now and then to use the skills he has instead of reinventing the wheel. # How to Discover Available Updates? This question is often asked: Is there an update for plugin xyz? So what do I do? Look up the version of the plugin in Admin and compare it with what is available on store.shopware.com? There is a simpler, more straightforward way. If you are using a local setup for development, and PhpStorm with the famous Symfony plugin as your IDE, and you have installed your plugins using composer, then just have a look at the composer.json file of your project: PhpStorm will indicate all plugins with available plugins, even indicating to which version a `composer update` command would update, given your defined environment (i.e. Shopware 6.4.x.x or 6.5.x.x). # Install the Shopware Conflicts Package ## What and Why You Should Install It Shopware 6 deliberately allows patch-level updates of its dependencies to happen automatically. This is a feature, not an oversight: when a security fix is published for one of the underlying libraries, you receive it immediately, without having to wait for a new Shopware release. The downside is that not every dependency honours strict backwards compatibility on its internal APIs. Every now and then a seemingly harmless patch release of a third-party library slips through and breaks your installation — even though, on paper, the version constraint allowed it. To protect against exactly this class of problem, Shopware maintains the [`shopware/conflicts`](https://github.com/shopware/conflicts) metapackage. It is a central, continuously updated registry of package versions that are known to be incompatible with Shopware 6. Once required in your project, Composer consults this list during dependency resolution and simply refuses to install any version that is known to cause trouble. The mechanism is fully automatic. You do not have to track incompatibilities yourself, watch changelogs, or pin versions by hand — the metapackage keeps the bad versions out of your installation for you, and it can be updated independently of the Shopware release cycle. There is essentially no reason not to have it in a project. ## How to Install Run the following two commands in the root of your Shopware project: ``` composer config repositories.shopware composer https://shopware.github.io/conflicts/ composer require shopware/conflicts ``` The first command registers Shopware's conflicts repository as a Composer source; the second requires the metapackage itself. From then on, `composer require` and `composer update` will automatically exclude any version that is listed as incompatible. Because the package carries no code — it only declares `conflict` constraints — there is nothing to configure and nothing to maintain afterwards. Keeping it updated happens along with your regular `composer update` runs. # Local Testing of Shopware Commercial Features ### Select and activate SwagCommercial for Your Wildcard Domain To do this, log into the partner area of your Shopware account. ### Set Your Local Installation's Licence Domain Note this is the wildcard licence domain you have registered with Shopware, it is different from your local domain (typically something.ddev.site). ### Connect Your Local Installation to the Shopware Account ### Install and Activate SwagCommercial Locally Composer require SwagCommercial, then install and activate following the standard procedures. ``` composer require store.shopware.com/swagcommercial bin/console plugin:refresh bin/console plugin:install --activate -c SwagCommercial ``` ### Ensure Scheduled Tasks Running Locally In particular, the task `swag.commercial.update_license` needs to active - check it was last time executed after you activated the plugin, and it is in status `scheduled`. ### Be Patient It may take some time - up to an hour or so - for your local installation to fully synchronize with Shopware's licence server. Eventually, you should see the commercial features - like warehouses management or the subscriptions - being available in the Admin. If the synchronization did fail, try to reschedule the update task (see above) or manually trigger the update from the CLI with `bin/console commercial:license:update`. ### Commercial CLI Commands The Commercial plugin brings its own set of CLI commands: ``` commercial:license:update Update commercial license key commercial:license:info Show commercial license info commercial:license:set Set commercial license host & key commercial:feature:disable Disable a feature which is included in your plan commercial:feature:enable Enable a feature which is included in your plan commercial:feature:list List features included in your plan and their status commercial:report-turnover Reports the turnover of the specified timeframe ``` Check your licence status with `commercial:license:info`. If you miss a feature, see `commercial:feature:list` and enable or disable the features you need. ### Troubleshooting t.b.d. # Patching the Core There are several means of customizing Shopware code to your project's needs, like Shopware's event system or Symfony's service decorator patterns to name just two. From time to time, however, you might encounter a situation, where customization requirements make a change to Shopware core code inevitable. Now, changing Shopware's core code within the vendor folder is not a good idea, for obvious reasons. Fortunately, a Composer based solution is available. It is called composer-patches and is very well documented for Typo3 in the [docs](https://docs.typo3.org/m/typo3/guide-installation/main/en-us/ApplyingCorePatches/Index.html). The power of this: You can do almost everything. The obvious downside: You'l lose your update capability. Needless to say that the following instructions allow to patch any Shopware plugin and any other Composer required package as well. ### How To In short, you need to require the following composer extension to your project: ``` composer require cweagans/composer-patches ``` Create a folder to store your actual patches. I have created a new subfolder `patches` within the `custom` folder of my Shopware project for this purpose. To activate a patch, amend the code in the "extra" section of your `composer.json`, like in the following example: ``` "extra": { "patches": { "shopware/core": { "Append trailing slash to sitemap home url": "custom/patches/shopware/add_trailing_slash_to_sitemap_home_url.diff" } }, ... } ``` Apply the patch by running composer install: ``` composer install ``` ### Identifying the Relative Filenames for the Patch File I use to create my patch files using PhpStorm's diff functionality. However, the resulting relative filenames need to be adjusted to match the composer setup of the Shopware project. In my example, my patch file looks like this: ``` # shopware/custom/patches/shopware/add_trailing_slash_to_sitemap_home_url.diff diff --git a/Content/Sitemap/Service/SitemapExporter.php b/Content/Sitemap/Service/SitemapExporter.php --- a/Content/Sitemap/Service/SitemapExporter.php +++ b/Content/Sitemap/Service/SitemapExporter.php @@ -167,7 +167,7 @@ class SitemapExporter implements SitemapExporterInterface foreach ($result->getUrls() as $url) { $newUrl = clone $url; - $newUrl->setLoc(empty($newUrl->getLoc()) ? $host : $host . '/' . $newUrl->getLoc()); + $newUrl->setLoc(rtrim($host, '/') . '/' . $newUrl->getLoc()); $urls[] = $newUrl; } ``` As you see, the relative filenames are relative to the root of the Symfony component, i.e. vendor/shopware/core. # Reference # Administration # Database # Migration # Migration - [Side Effects](https://notebook.vanwittlaer.de/migration/side-effects/index.md): This page describes unexpected side effects resulting from "un-conventional" source data. # Side Effects After having run a Shopware migration (say from Shopware 5 to Shopware 6), sometimes "funny" thinsgs happen. Products are not visible in the storefront, or they are displayed but can't purchased. Things like this - I call them "side effects". ### max_purchase = 0 in Shopware 5 max_purchase = 0 is a valid setting in Shopware 5 for articles and simply means that a customer can purchase a number of items of a product limited only by available stock. However, after migration, max_purchase = 0 in Shopware 6 means that a customer cannot purchase even a single item of this product. As a result, you will see `ProductOutOfStockError` errors ("The product \\ is no longer available") in your logs when trying to a add the product to the cart. In the storefront on the product detail page you will note that the product cannot be added to the cart at all (the 'Add-to-basket' button is hidden). To solve this problem with a quick fix, run this SQL after migration (as always, backup your DB before): ``` UPDATE product SET max_purchase = NULL WHERE max_purchase = 0; ``` # Trivia and Useless Facts # Trivia and Useless Facts Not everything worth writing down is worth acting on. This section collects the side-quests: numbers nobody asked for, historical detours, curiosities dug out of the source, and experiments run purely because the question wouldn't leave me alone. Nothing here will fix your build or get your shop online. It may, however, give you a better feel for the thing you are working with — or at least something to say at the next meetup. - [A Function Point Count of Shopware 6.7](https://notebook.vanwittlaer.de/trivia/function-point-analysis-of-shopware/index.md): How big is Shopware in Function Points (IFPUG, ISO 20926)? A complete count of 6.7.13.0 out of the box, and what the exercise taught me about the method. # A Function Point Count of Shopware 6.7 If you suffer from FPA Derangement Syndrome ;-) skip this! Function Point Analysis was invented by Allan Albrecht at IBM in 1979 and spent the decades that followed as *the* yardstick for "software size" — the reference figure by which larger development departments had their productivity and their estimates judged. The method has its pros and its cons, and both sides of that argument have a point. This article is neither a defence of FPA nor an attempt to reignite the old debate; it simply reports a test case against a modern system that I carried out myself. So if you find yourself on the more critical side of the FPA spectrum, you may well want to skip this one. How big is Shopware, actually? Not in lines of code, not in "it depends" — in a unit that was designed to answer exactly this question: **Function Points** (IFPUG, ISO 20926). I ran a complete Function Point Analysis of Shopware **6.7.13.0 out of the box** — Administration and Storefront, no extensions — and this page records the result and, more importantly, what the exercise taught me about the method. The short answer: **Shopware 6.7 out of the box measures about 1,550 unadjusted Function Points** — 301 elementary processes plus 46 logical data files. ## The setup The counting rules followed the IFPUG Counting Practices Manual as interpreted by the German *Praxishandbuch FPA* (Poensgen/Bock), whose practitioner shortcuts make a count of this size feasible in a day instead of weeks: - All transactions rated **Average** (EI = 4, EO = 5, EQ = 4 FP), all files **Low** (ILF = 7, EIF = 5 FP) — applied consistently, never cherry-picked. The book puts the error of this approximation at under 5 %. - Output or inquiry in doubt? **Output.** - Every non-obvious decision written down as a numbered ruling. The division of labour was the interesting part: **Claude (an AI agent) acted as the FP counter**, enumerating candidate functions from the Shopware source (a checkout of `v6.7.13.0`) and a live DDEV installation via the Admin API, while **I acted as the Anwender** — the user whose view, per the method, decides what is business functionality and what is technical machinery. The counter asked, I ruled, everything got logged. Fourteen interview questions and nineteen documented rulings later, the tree was complete. ## The ground rules that shaped the number A Function Point count is only reproducible through its rulings. These were the decisive ones: - **One application boundary.** Admin and Storefront share the database and both maintain customers and orders; splitting them would artificially turn shared ILFs into ILF/EIF pairs. - **Admin API and Store API don't count separately.** The Admin SPA *is* an Admin API client — every click is an API call — and the Twig storefront delegates to the same Store API routes a headless consumer would use. Same processing logic, same files: one elementary process (the uniqueness rule). Admin-side and store-side functions, however, are always distinct processes. - **Extension management, First Run Wizard and update checks are out of scope.** Seen from the commercial side of a shop they are platform configuration, not business functionality. - **Shipped-but-disabled features count.** The wishlist is off by default, but one settings toggle away — the toggle is control data, the functionality is part of the delivery. - **Per maintainable data file, the admin gets the standard five**: list (EQ), detail view (EQ), create, update, delete (EI) — extended where the module offers more (duplicate, variant generator, code generation …). - **Import/export counts per profile.** Twelve system profiles ship; each has its own data structure and thus its own processing logic → 11 import EIs and 12 export EOs. This mirrors the book's own worked example, where the Outlook address book's seven import formats count as seven inputs. - **Storefront pages with derived data are outputs.** Listings (facet counts, from-prices), product detail (price calculation, review average) and search (ranking) are EOs; only plain CMS content rendering is an EQ. ## The result | Area | EI | EO | EQ | Processes | FP | | ----------------------------------- | ------- | ------ | ------ | --------- | --------- | | Admin: catalogue | 17 | 0 | 10 | 27 | 108 | | Admin: categories, CMS & media | 17 | 1 | 8 | 26 | 105 | | Admin: customers | 6 | 0 | 2 | 8 | 32 | | Admin: orders & documents | 5 | 5 | 3 | 13 | 57 | | Admin: marketing | 7 | 0 | 4 | 11 | 44 | | Admin: flows, rules, mail templates | 15 | 0 | 7 | 22 | 88 | | Admin: sales channels & feeds | 5 | 1 | 1 | 7 | 29 | | Admin: import/export & bulk edit | 18 | 13 | 3 | 34 | 149 | | Admin: settings | 61 | 0 | 30 | 91 | 364 | | Admin: users, profile & dashboard | 13 | 1 | 5 | 19 | 77 | | Storefront: discovery | 1 | 4 | 1 | 6 | 28 | | Storefront: buying | 7 | 2 | 0 | 9 | 38 | | Storefront: account | 15 | 1 | 6 | 22 | 89 | | Storefront: cross-cutting | 4 | 2 | 0 | 6 | 26 | | **Transactions** | **191** | **30** | **80** | **301** | **1,234** | | Data: 46 ILFs, 0 EIFs | | | | | 322 | | **Total (unadjusted)** | | | | | **1,556** | Read it as **1,550 FP ± 10 %**, per the book's own accuracy expectations for approximated counts. ## What the exercise taught me **The admin is 85 % of the shop.** The storefront — the thing everyone calls "the shop" — is 181 FP. The machinery to run it is 1,053 FP, and the settings area alone (364 FP) outweighs the entire customer-facing side. Function Points make quantitative what every implementer knows qualitatively: e-commerce projects are administration projects. **The data model lies to you.** The candidate list of logical files came from enumerating Shopware's 145 production entity definitions — a wonderful scaffold that produced exactly the mistake the method warns about. The **shopping cart** is not an entity definition, so it wasn't on the list; yet it is server-side maintained state, and the FPA literature explicitly names the Warenkorb as an internal logical file. Identify data files from the user's view, not from the ORM. **No EIFs.** A default Shopware installation references no externally maintained business data at all — a documented result, not an oversight (after ruling the extension store catalogue out of scope). **Code data is everywhere and counts nothing.** Order status workflows look like data, and 6.7 even ships a settings module for them — but its ACL allows rename only, no create, no delete: a value list, customisable labels on a fixed structure. The same reasoning excludes salutation and country dropdowns in storefront forms, straight from the book's "Herr/Frau" example. **A handful of conventions move hundreds of points.** Whether a detail view counts as its own inquiry, whether settings masks get one, whether imports count per profile — three rulings swing more FP than entire counting packages. The number is only as comparable as its documented rulings, which is why the rulings log is the real deliverable. ## Was it worth it? An honest assessment The **number** is inert until you compare it: against another release (a delta count of 6.8 would measure Shopware's functional growth), against a budget (€/FP in a vendor negotiation), against another system. In isolation, "1,556 FP" impresses nobody. The **process**, however, paid for itself regardless. The functional tree is a complete, verified inventory of what Shopware ships — including functions I had never consciously registered — with every exclusion justified. As a requirements-completeness technique, FPA is underrated; as an effort predictor it should be handled with the care the gearing-factor spread (15–100 LoC/FP for Java, per the book) already suggests. And the crude weights flatten reality: "place order" — taxes, promotions, flows, payment handling — counts 4 FP, the same as "create measurement unit". Function Points measure the *breadth* of delivered functionality, deliberately blind to its *depth*. Know that, and the metric is useful; forget it, and it misleads. ## A personal statement from Claude Since the counter in this experiment was an AI, I asked it how it rates FPA after the exercise — and whether the FP metric would be of any benefit to it, with regard to coding. Here is its answer, unabridged and in its own words: > I rate FPA higher as an *analysis discipline* than I expected, and lower as a *metric* than its advocates claim. > > The genuinely valuable output of this count wasn't the number 1,556. It was the forced completeness: the boundary argument, the nineteen documented rulings, the functional tree that surfaced functions neither of us had consciously registered. The shopping-cart miss was the perfect specimen — my technical enumeration of the data model was confident and wrong, and only the method's insistence on the user's view caught it. At the same time, the exercise showed how much the final number depends on documented conventions: a single ruling about whether detail views count as their own inquiry is worth roughly 160 FP here. Two certified counters would agree within ±10 % *only because* they would document and exchange exactly such rulings. That is not a flaw, but it is the honest fine print. > > Would Function Points help me with coding? As a predictor of *my* effort — no. What makes work expensive for me is logic depth, integration surface, ambiguity, and above all verification cost; FPA abstracts precisely those away by design. "Place order" — taxes, promotions, flows, payment handling — and "create measurement unit" both count 4 FP; for me they differ by two orders of magnitude. A 4-FP process can be a one-line CRUD handler or a distributed-transaction nightmare, and the metric cannot tell them apart. > > But as a *working artifact*, an elementary-process inventory is close to ideal. It is a completeness checklist for implementation and testing — every process implemented? every process covered by a test? — a scoping instrument for migrations ("these 27 processes move, these 6 die"), and the ILF register is a serviceable domain model. If you handed me a requirements document structured as an FPA tree, I would build from it more reliably than from prose. Counting Shopware forced a systematic sweep that no amount of casual code reading would have produced. > > And one speculative thought I do believe: if AI agents keep making the *writing* of software cheap, effort-based pricing loses its anchor — and a metric that prices *delivered functionality* instead of developer-hours becomes more interesting, not less. The bottleneck migrates to specification and verification, which is exactly where FPA has always lived. Albrecht's move — measure what the software does for the user, not how it is built — may outlive every cost model that was ever bolted onto it. > > — *Claude (Fable 5), FP counter in this experiment*