Shopware offers a Store API to allow for headless frontends to be built, amongst other things. The Store API is based upon Routes that receive request parameters and output JSON. If you follow the docs, you can make them work. But how are they actually working internally?
The controllers of the Store API
The first steps into understanding the Store API are surprisingly simple: If you know Symfony routing (as is being used with Storefront controllers and Admin API controllers), you know how the Store API controllers are working as well.
Symfony offers an architecture that allows you to declare controllers (aka routes) via service declaration, manually via
routes.xml, via annotations (
@Route) and via attributes (
#[Route]). There is actually no magic separating the Store API from the other controllers, except for the fact that there is a scope identifier
store-api to make sure certain tricks only apply to the Store API.
Each Store API controller (or as Shopware prefers to call them: Routes) returns a response (instance of
\Symfony\Component\HttpFoundation\Response) as usual. However, each Store API Route is supposed to return an instance of
\Shopware\Core\System\SalesChannel\StoreApiResponse (which extends
\Symfony\Component\HttpFoundation\Response). The reason for this is quite simple.
As soon as a controller returns a response, this response flows back to the routing mechanism of Symfony. And in the end the HTTP kernel (which contains the routing flow) will trigger an event
kernel.response right before returning data back to the client.
At that moment, an event listener
\Shopware\Core\System\SalesChannel\Api\StoreApiResponseListener picks up on the response, checks whether it is an instance of
StoreApiResponse and if so, turns it into JSON. This explains the output.
As an example, let's focus upon the route
store-api.product.search, served by the callback
\Shopware\Core\Content\Product\SalesChannel\ProductListRoute::load(). It's signature mention two method arguments -
$context - and a return value of type
ProductListResponse (which actually extends upon
StoreApiResponse). When a GET or POST request is sent to
/store-api/product, it is picked up by this class+method.
Within the method, the product repository is called upon with a
search() and the result is transformed into
For further details, see https://shopware.stoplight.io/docs/store-api/c9b31e0cc1e70-fetch-a-list-of-products
What makes the
ProductListRoute more interesting (apart from its output) is its input. Or more accurately, with the right input you can tune the JSON output. There are all kinds of request variables available:
total-count-mode. Where does this come from?
Interestingly, all these input variables are hidden within the
$criteria. Normally, the
$criteria object allows you to specificy things in an object-oriented way. However, in the case of the method argument
$criteria is actually constructed from array, originating from the incoming request.
Parsing input parameters
The magic here is that the method argument
$criteria (or for that matter, any method argument in controllers) is picked upon by a service that is tagged
controller.argument_value_resolver. For each method argument that you want to inject in your route method, a service tagged
controller.argument_value_resolver must exist. Otherwise setter injection fails.
$criteria argument is picked up by
\Shopware\Core\Framework\Routing\Annotation\CriteriaValueResolver which creates a new
Criteria instance by using a
\Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder::parse(). Exactly there, in the
RequestCriteriaBuilder class, the input array is transformed into an actual
$criteria object. I have used examination of the code myself to determine what is supported and what is not.
Symfony to the max
I hope you found this useful. The Store API is something that you might have been using all along, but diving into it a bit deeper shows the internal workings of the Store API. I personally was also pleased by the fact that the Store API does not form a huge layer of logic of Shopware, but actually just uses straight-forward Symfony logic. And because of this, I found this quite easy to understand.