---
title: Constructing a Flow Action Endpoint | Shopware Community Hub
description: >-
  Learn how to build an App Server endpoint that receives Flow Builder requests,
  processes order data, and saves it in a custom entity.
canonical_url: 'https://hub.shopware.com/learn/unit/constructing-flow-action-endpoint'
---

# Constructing a Flow Action Endpoint

<LearningObjectives>

- **Build a controller** on the App Server that can **receive Flow Builder requests**.
- **Extract** and **process** data from incoming requests.
- **Define** and **migrate** a custom `DispatchNote` entity on the App Server to **store** Shopware data.
- **Use the Symfony Maker Bundle** to speed up common development tasks.

</LearningObjectives>

# Constructing a Flow Action Endpoint

In the previous learning unit, you learned how to create a custom Flow Action and used it in a Flow Builder sequence.

In this learning unit, you will implement the **App Server endpoint**, i.e., an HTTP route handler (controller/function) in your App Server that the Flow Builder calls.

## Code-Along

To follow along, use the following command within the directory containing your `ShopwareStoreTrackerBackend` repository:

```bash
git checkout tags/creating_flow_controller--start
```

## Defining the `DispatchNote` Entity

To persist the data sent from Shopware (e.g., order number, delivery address, customer email), create a custom `DispatchNote` entity on the App Server. This entity will store the parameters you defined in the `flow.xml`.

A fast and efficient method of doing this is to use the Symfony Maker bundle. Follow the [Maker bundle installation instructions](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html#installation), then run the following command with in the App-Server root directory:

```bash
bin/console make:entity
```

The `make:entity` command will then spawn an interactive wizard with the following steps:

### Defining an Entity Name

First, you will be prompted to define an entity name. Make sure to use **PascalCase** here. The name becomes the PHP [class name](https://www.php.net/manual/en/language.oop5.basic.php) in your definition.

```bash
 Class name of the entity to create or update (e.g. DeliciousPizza):
 > DispatchNote
```

### Adding the 'Shopware Order Number' Field

Next, add the first property for your entity. Follow the wizard prompts:

```bash
 New property name (press <return> to stop adding fields):
 > shopwareOrderNumber

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 100

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/DispatchNote.php
```

We set the length to 100 and made the field non-nullable to ensure the order number is always present.

<Callout title="Property Naming" type="info">

Use [camelCase](https://developer.mozilla.org/en-US/docs/Glossary/Camel_case) for property names (e.g., `shopwareOrderNumber`). The names map directly to PHP properties and generated getters/setters.

</Callout>

### Adding the 'Customer Delivery Address' Field

A delivery address consists of multiple parts (street, city, postal code, region). Store it as a **JSON Field** you can keep the parts together in one property. Make it non-nullable; otherwise the warehouse will have a hard time delivering the order!

```bash
New property name (press <return> to stop adding fields):
> customerDeliveryAddress

Field type (enter ? to see all types) [string]:
> json

Can this field be null in the database (nullable) (yes/no) [no]:
> no

updated: src/Entity/DispatchNote.php
```

### Adding the 'Customer Phone Number' Field

Add a phone number field to complete the entity. Follow the wizard prompts:

```bash
New property name (press <return> to stop adding fields):
> customerPhoneNumber

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 50 #or just press Return/Enter key to keep the default (255)

Can this field be null in the database (nullable) (yes/no) [no]:
> yes

updated: src/Entity/DispatchNote.php
```

<Callout title="Why Nullable?" type="info">

Phone numbers are often optional, so it makes sense to allow this field to be nullable.

</Callout>

At this point, the entity definition is complete. You can stop the wizard prompt by pressing the Return/Enter Key.

```bash
Add another property? Enter the property name (or press <return> to stop adding fields):
> 
         
Success! 
         
Next: When you're ready, create a migration with php bin/console make:migration
```

To find the new files we have just created run `git status` from the App-Server root. You should see the following:

```bash
On branch main
Your branch is up to date with 'origin/main'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/Entity/DispatchNote.php
        src/Repository/DispatchNoteRepository.php
```

If you want to check them out, follow the file paths in your IDE to see what the Maker Bundle has constructed.

### Migrating the Entity Schema

It is important to remember that the `make:entity` command alone **doesn't perform any operations upon the Symfony database**. To update the database schema with the new entity, the following two commands are required:

**1. Generate a migration**: It creates a PHP file with the SQL changes. Make sure that the output of the command is `Success`:

```bash
bin/console make:migration
```

**2. Run the migration**: It executes the SQL changes in the database. Respond with 'yes' when confirmation is requested for command execution:

```bash
bin/console doctrine:migrations:migrate
```

Now the `DispatchNote` entity is successfully migrated to the database.

## Notifying Shopware Admin of the Process Result

Ideally, when a `DispatchNote` is created (or fails to be created), we want to give the merchant a visual confirmation in the Shopware Admin panel that creating a `DispatchNote` has succeeded (or failed). One possible solution is to attach additional data to an entity, in this case, the `Order` entity that triggered the flow.

<Callout title="Current Limitation" type="warning">

Unfortunately, the App Server cannot send UI notifications directly to the Shopware Admin panel. If you want to provide feedback (e.g., success or error codes), you need to implement a workaround, such as updating an entity with extra information.

</Callout>

## Generating a Controller Endpoint for Our Flow-Action

The last step in our flow setup is to create a **Controller** on the App Server that can handle the HTTP request sent by our custom Flow Action. As defined earlier, the Flow Action calls the endpoint `http://localhost:8001/dispatch-note/`.

Instead of writing boilerplate code by hand, we can use the [Symfony Maker bundle](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html#installation) to generate a skeleton for us.

Run the following command from the App Server root directory:

```bash
bin/console make:controller
```

When prompted, enter a name for the new controller class:

```bash
 Choose a name for your controller class (e.g. TinyKangarooController):
 > DispatchNoteController

 created: src/Controller/DispatchNoteController.php

           
  Success! 
           

 Next: Open your new controller class and add some pages!
```

### Creating a Route

Open the new generated file `src/Controller/DispatchNoteController.php` in the **ShopwareStoreTrackerBackend** project. Symfony has already created a basic controller skeleton:

```php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class DispatchNoteController extends AbstractController
{
    #[Route('/dispatch-note', name: 'app_dispatch_note', methods:['POST'])]
    public function index(): JsonResponse
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/DispatchNoteController.php',
        ]);
    }
}
```

This automatically registers a **POST route** at the `/dispatch-note` endpoint. When called, it will return a small JSON response. From here, the work focuses on two things:

- **Process incoming data**: Take the data passed by parameter to the route, and disseminate it into a new `DispatchNote` record.
- **Provide Feedback**: Send feedback back to the Shopware merchant that a new `DispatchNote`` has been successfully created, or that the process of creating a DispatchNote has failed.

#### Writing a New `DispatchNote` Record to the Database

To persist incoming Flow Action data as a `DispatchNote`, we need three things:

1. Dependencies, so services to inject.
2. A way to verify and parse the incoming data.
3. Logic to save the entity and send feedback to Shopware.

##### Declaring Dependencies

We use the Symfony **EntityManager** to persist data and also inject the Shopware App SDK services for sending responses back to Shopware. Add this constructor to your controller:

```php
public function __construct(
    private EntityManager $entityManager,
    private ClientFactory $clientFactory,
    private ShopResolver $shopResolver,
    private LoggerInterface $logger,
) {}
```

- **EntityManager**: It saves entities to the database.
- **ClientFactory** and **ShopResolver**: They are needed to send feedback to the right Shopware shop.
- **LoggerInterface**: It is used to log information about the process, useful for debugging.

##### Verifying Received Data

Next, check that the request payload looks as expected. For now, log the request body:

```php
#[Route('/dispatch-note', name: 'app_dispatch_note', methods: ['POST'])]
public function index(RequestInterface $request): JsonResponse
{
    $this->logger->alert($request->getBody()->getContents());
    // We need to action a 'rewind' in order to reuse the contents of the request body
    $request->getBody()->rewind();
}
```

If all is well with your Controller, the logger should report a payload in the following format:

```log
[Application] Jun 30 13:25:27 |ALERT  | APP    {"shopwareOrderNumber":"10064","orderDeliveryAddress":"             Harbour Lane,             ,             ,             Hull,             HU3 GLU,             ","customerEmail":"somecustomer@mailservices.com","source":{"url":"http:\/\/localhost:8000","eventId":"0197c10394a173e1bc3199230c1c1c6c","shopId":"z6OGOVPzRIUmL7Ef","appVersion":"1.0.8","inAppPurchases":null,"action":"senddispatchnote"},"timestamp":1751289926}
```

If anything is wired correctly, the log will show a payload similar to what you defined in the **flow.xml** file.

<Callout title="Rewinding Request Body" type="warning">

Each time you read the request body, you must call the `rewind()` method (as demonstrated above). Otherwise, the stream pointer stays at the end and will return nothing.

</Callout>

##### Setting Up the HTTP Client

Before saving data, prepare an HTTP client for sending feedback to Shopware. For this, we use the [SimpleHttpClient](https://developer.shopware.com/docs/guides/plugins/apps/app-sdks/php/05-http-client.html) from the Shopware App SDK. Unlike [WebHooks](https://developer.shopware.com/docs/guides/plugins/apps/webhook.html), where Shopware itself decides which endpoint to call, the controller in your App Server may serve multiple shops at once. That means you must first determine which shop sent the request.

This is where the `ShopResolver` dependency comes in: it looks up the correct `shop` instance in the App Server database via the '`shop-id`' parameter provided in the request. After resolving the shop, you can create a client bound to that shop and use it for API calls:

```php
$shop = $this->shopResolver->resolveShop($request);
$client = $this->clientFactory->createSimpleClient($shop);
```

<Callout title="Note" type="info">

If your App Server handles multiple shops, always resolve the shop first. Otherwise, you risk sending feedback to the wrong shop.

</Callout>

##### Collecting Up Data

To breathe life into a new `DispatchNote` record, we need order data from the Shopware server. Specifically, we need: **Order Number**, **Customer Delivery Address** and **Customer Phone Number**:

We extract the payload (`$request->getBody()->getContents()`) from the request body again, this time saving the result to a variable `$responseContents`. Then we filter out the delivery address parts, and prepare the URL for updating the Shopware `Order` entity.

```php
// Let's also extract payload data and prepare for sending!
$responseContents = json_decode(
    $request->getBody()->getContents(),
    true,
    512,
    JSON_THROW_ON_ERROR
);
// array_filter allows us to extract the customer address via a pattern in the array keys
$customerAddress = array_filter(
    $responseContents, 
    fn ($key) => str_starts_with($key, 'customerDelivery'),
    ARRAY_FILTER_USE_KEY
);

// The URL we will use for updating the Shopware `Order` entity 
$orderPatchURL = $shop->getShopUrl() . '/api/order/' . $responseContents['orderId'];
```

Since the rest of the method will be wrapped in a [`try..catch`](https://www.php.net/manual/en/language.exceptions.php#language.exceptions.catch) block, we define the URL (`orderPatchURL`) in advance.

As mentioned before, the [Shopware Stoplight docs](https://shopware.stoplight.io/docs/admin-api/twpxvnspkg3yu-quick-start-guide) are an extremely useful resource in understanding how to construct Shopware API requests. Using [this page](https://shopware.stoplight.io/docs/admin-api/3cc867261ff28-partially-update-information-about-a-order-resource) as reference, we can see that a `PATCH` request requires an `{id}` parameter. Within the `<parameters>` configured in our 'dispatchNote' action, this corresponds to `orderId`.

##### Saving a `DispatchNote` Entity to the Database

Next, we create a new `DispatchNote` entity, fill it with the data from the request and save it to the database using Symfony's [EntityManager](https://symfony.com/doc/current/doctrine.html).

We wrap this code in a `try..catch` block, so that we can later also handle success and failure cases when updating Shopware.

```php
    try {
        $dispatchNote = new DispatchNote();
        
        $dispatchNote->setShopwareOrderNumber($responseContents['shopwareOrderNumber']);
        $dispatchNote->setCustomerDeliveryAddress($customerAddress);
        $dispatchNote->setCustomerPhoneNumber($responseContents['customerEmail']);

        $this->entityManager->persist($dispatchNote);

        $client->patch($orderPatchURL, [
            'internalComment' => "Order dispatch note created for customer " . $responseContents['customerEmail']
        ]);
    } catch (\Exception $e) {
        $client->patch($orderPatchURL, [
            'internalComment' => "Order dispatch note failed for customer " . $responseContents['customerEmail'] . ".\nError Message: " . $e->getMessage()
        ]);
    }

    return $this->json([
        'message' => 'All processes complete',
    ]);
```

- The `DispatchNote` entity is created and persisted to the database.
- A **PATCH** request updates the Shopware `Order` entity with the `success` message.
- If an error occurs, the `catch` block instead patches the order with a `failure` message.
- Finally, the controller returns a JSON response.

## Checking the Results

To verify that our App Server successfully updates the order with a comment, follow these steps:

### 1. Navigate to the Orders Section

Open the Shopware Admin panel (`http://localhost:8000/admin` for devenv users) and go to the 'Orders' tab.

![Hovering over the Shopware Admin menu](assets/finalCheck-admin-dashboard.png)

### 2. Changing the Payment Status of an Order

Pick any order from the list, open the detail view and change the **Payment Status** to **Paid**.

![Detail view for a selected order](assets/finalCheck-order-detail.png)

### 3. Locating a PATCH Request in the App Server Logs

After updating the order payment status, check the logs on you App Server (`symfony server:start --port 8001`). You should see a PATCH request being sent to your local Shopware Shop.

![PATCH request displayed in the logs of the Symfony App Server](assets/finalCheck-symfony-logs.png)

### 4. Check the 'Internal Comment' Field Within the Order

Return to the order detail view in the Shopware Admin panel and open the **Details** tab. Under **Internal comment**, there should be a message created by your App Server.

![Internal comment field demonstration](assets/finalCheck-order-internalComment.png)

## Code-Along (end)

To see what the final result of this learning unit should look like, run the following command in the `ShopwareStoreTrackerBackend` project and also in your `ShopwareStoreTracker` App:

```bash
git checkout tags/creating_flow_controller--end
```

<Callout title="Removing Working Changes" type="warning">

If you have local changes, you will need to run `git reset --hard HEAD`. Be mindful that this command will destroy any local changes!

</Callout>

## Summary

In this learning unit, you learned how to:

- **Define and migrate** a custom entity with the Symfony Maker Bundle
- **Create** a controller to handle incoming HTTP requests by the Flow Action
- **Persist** incoming data into the database
- **Update** the Shopware `Order` entity to signal success or failure
- **Verify** the process in the Shopware Admin panel

**Congratulations!** You have now completed this learning path and gained a solid foundation in **Shopware's App Development**.
