Add store API route

Overview

In this guide you'll learn how to add a custom store API route. In this example, we will create a new route called ExampleRoute that searches entities of type swag_example. The route will be accessible under /store-api/example.

Prerequisites

In order to add your own Store API route for your plugin, you first need a plugin as base. Therefore, you can refer to the Plugin Base Guide.
You also should have a look at our Adding custom complex data guide, since this guide is built upon it.

Add Store API route

As you may already know from the adjusting service guide, we use abstract classes to make our routes more decoratable.
All fields that should be available through the API require the flag ApiAware in the definition.

Create abstract route class

First of all, we create an abstract class called AbstractExampleRoute. This class has to contain a method getDecorated and a method load with a Criteria and SalesChannelContext as parameter. The load method has to return an instance of ExampleRouteResponse, which we will create later on.
<plugin root>/src/Core/Content/Example/SalesChannel/AbstractExampleRoute.php
1
<?php declare(strict_types=1);
2
3
namespace Swag\BasicExample\Core\Content\Example\SalesChannel;
4
5
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
6
use Shopware\Core\System\SalesChannel\SalesChannelContext;
7
8
abstract class AbstractExampleRoute
9
{
10
abstract public function getDecorated(): AbstractExampleRoute;
11
12
abstract public function load(Criteria $criteria, SalesChannelContext $context): ExampleRouteResponse;
13
}
Copied!

Create route class

Now we can create a new class ExampleRoute which uses our previously created AbstractExampleRoute.
<plugin root>/src/Core/Content/Example/SalesChannel/ExampleRoute.php
1
<?php declare(strict_types=1);
2
3
namespace Swag\BasicExample\Core\Content\Example\SalesChannel;
4
5
use OpenApi\Annotations as OA;
6
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
7
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
8
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
9
use Shopware\Core\Framework\Routing\Annotation\Entity;
10
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
11
use Shopware\Core\System\SalesChannel\SalesChannelContext;
12
use Symfony\Component\Routing\Annotation\Route;
13
14
/**
15
* @Route(defaults={"_routeScope"={"store-api"}})
16
*/
17
class ExampleRoute extends AbstractExampleRoute
18
{
19
protected EntityRepositoryInterface $exampleRepository;
20
21
public function __construct(EntityRepositoryInterface $exampleRepository)
22
{
23
$this->exampleRepository = $exampleRepository;
24
}
25
26
public function getDecorated(): AbstractExampleRoute
27
{
28
throw new DecorationPatternException(self::class);
29
}
30
31
/**
32
* @Entity("swag_example")
33
* @OA\Post(
34
* path="/example",
35
* summary="This route can be used to load the swag_example by specific filters",
36
* operationId="readExample",
37
* tags={"Store API", "Example"},
38
* @OA\Parameter(name="Api-Basic-Parameters"),
39
* @OA\Response(
40
* response="200",
41
* description="",
42
* @OA\JsonContent(type="object",
43
* @OA\Property(
44
* property="total",
45
* type="integer",
46
* description="Total amount"
47
* ),
48
* @OA\Property(
49
* property="aggregations",
50
* type="object",
51
* description="aggregation result"
52
* ),
53
* @OA\Property(
54
* property="elements",
55
* type="array",
56
* @OA\Items(ref="#/components/schemas/swag_example_flat")
57
* )
58
* )
59
* )
60
* )
61
* @Route("/store-api/example", name="store-api.example.search", methods={"GET", "POST"})
62
*/
63
public function load(Criteria $criteria, SalesChannelContext $context): ExampleRouteResponse
64
{
65
return new ExampleRouteResponse($this->exampleRepository->search($criteria, $context->getContext()));
66
}
67
}
Copied!
As you can see, our class is annotated with @RouteScope and the defined scope store-api.
In our class constructor we've injected our swag_example.repository. The method getDecorated() must throw a DecorationPatternException because it has no decoration yet and the method load, which fetches the data, returns a new ExampleRouteResponse with the respective repository search result as argument.
Now let's take a look at the annotation of our load method which is required for the automatic OpenAPI generation using Swagger UI.
  • @Entity: The entity we are representing.
  • @OA\Post:
    • path: The route, where we can access this action.
    • summary: A description for this route.
    • operationId: An unique name for this action.
    • tags: First argument is the API we're using e.g. 'Store API' or 'API', the second is the name of the group where this action is grouped.
  • @OA\Parameter: Parameter for our route. 'Api-Basic-Parameters' is used for the abstraction of a criteria which adds parameters like sort, post-filters, ...
  • @OA\Response:
    • response: HTTP status code of the response.
    • description: A description for the response, e.g. Successfully saved.
The last part of our response is the content, using an @OA\JsonContent annotation with type Object, since we are returning a JSON object. Within the JSON content, we have three properties annotated with @OA\Property. The first property is the amount of entities we retrieved. The next property contains the aggregations of our criteria.
Finally, we have our retrieved entities, using an @OA\Items annotation which references to #/components/schemas/ and the technical name of our entity, so in our case #/components/schemas/swag_example. This is used to generate the schema according to our definition.

Route response

After we have created our route, we need to create the mentioned ExampleRouteResponse. This class should extend from Shopware\Core\System\SalesChannel\StoreApiResponse. In this class we have a property $object of type Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult. The class constructor has one argument EntitySearchResult, which was passed to it by our ExampleRoute. The constructor calls the parent constructor with the parameter $object which sets the value for our object property. Finally, we add a method getExamples in which we return our entity collection that we got from the object.
<plugin root>/src/Core/Content/Example/SalesChannel/ExampleRouteResponse.php
1
<?php declare(strict_types=1);
2
3
namespace Swag\BasicExample\Core\Content\Example\SalesChannel;
4
5
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
6
use Shopware\Core\System\SalesChannel\StoreApiResponse;
7
use Swag\BasicExample\Core\Content\Example\ExampleCollection;
8
9
class ExampleRouteResponse extends StoreApiResponse
10
{
11
protected EntitySearchResult $object;
12
13
public function __construct(EntitySearchResult $object)
14
{
15
parent::__construct($object);
16
}
17
18
public function getExamples(): ExampleCollection
19
{
20
/** @var ExampleCollection $collection */
21
$collection = $this->object->getEntities();
22
23
return $collection;
24
}
25
}
Copied!

Register route

The last thing we need to do now is to tell Shopware how to look for new routes in our plugin. This is done with a routes.xml file at <plugin root>/src/Resources/config/ location. Have a look at the official Symfony documentation about routes and how they are registered.
<plugin root>/src/Resources/config/routes.xml
1
<?xml version="1.0" encoding="UTF-8" ?>
2
<routes xmlns="http://symfony.com/schema/routing"
3
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
xsi:schemaLocation="http://symfony.com/schema/routing
5
https://symfony.com/schema/routing/routing-1.0.xsd">
6
7
<import resource="../../Core/**/*Route.php" type="annotation" />
8
</routes>
Copied!

Check route via Symfony debugger

To check, if your route was registered correctly, you can use the Symfony route debugger.
1
$ ./bin/console debug:router store-api.example.search
Copied!

Check route in Swagger

To check, if your OpenApi Annotations are correct, you'll have to check Swagger. To do this, go to the following route: /store-api/_info/swagger.html.
Your generated request and response could look like this:

Request

1
{
2
"page": 0,
3
"limit": 0,
4
"term": "string",
5
"filter": [
6
{
7
"type": "string",
8
"field": "string",
9
"value": "string"
10
}
11
],
12
"sort": [
13
{
14
"field": "string",
15
"order": "string",
16
"naturalSorting": true
17
}
18
],
19
"post-filter": [
20
{
21
"type": "string",
22
"field": "string",
23
"value": "string"
24
}
25
],
26
"associations": {},
27
"aggregations": [
28
{
29
"name": "string",
30
"type": "string",
31
"field": "string"
32
}
33
],
34
"query": [
35
{
36
"score": 0,
37
"query": {
38
"type": "string",
39
"field": "string",
40
"value": "string"
41
}
42
}
43
],
44
"grouping": [
45
"string"
46
]
47
}
Copied!

Response

1
{
2
"total": 0,
3
"aggregations": {},
4
"elements": [
5
{
6
"id": "string",
7
"name": "string",
8
"description": "string",
9
"active": true,
10
"createdAt": "2021-03-24T13:18:46.503Z",
11
"updatedAt": "2021-03-24T13:18:46.503Z"
12
}
13
]
14
}
Copied!