Reading data

Overview

In this guide you'll learn how to properly fetch data from the database in your plugin or for core contributions. This will also cover how to add filters to only find specific data, and how to aggregate your desired data. Unlike most other Symfony applications, Shopware 6 uses no ORM but rather a thin Data Abstraction Layer. It's worth getting used to the "DAL", as you might stumble upon this term every now and then in the Shopware universe.

Prerequisites

Since this guide is built upon the plugin base guide plugin base guide, you might want to have a look at it. Furthermore, the guide about Dependency injection will come in handy, since you need to know how to inject a service using the DI container. You also might want to have a look at the concept behind the Data abstraction layer concept first to get a better grasp of how it works.

Reading data

Let's get started with examples on how to read data now. This example will be about reading products, but adjusting them for other data is easy, you'll see what we mean.

Injecting the repository

Dealing with the Data Abstraction Layer is done by using the automatically generated repositories for each entity, such as a product. This means, that you have to inject the repository into your service first.

The repository's service name follows this pattern: entity_name.repository For products this then would be product.repository, so let's do this.

// SwagBasicExample/src/Resources/config/services.xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Swag\BasicExample\Service\ReadingData" >
<argument type="service" id="product.repository"/>
</service>
</services>
</container>

And here's the respective class including its constructor:

// SwagBasicExample/src/Service/ReadingData.php
<?php declare(strict_types=1);
namespace Swag\BasicExample\Service;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
class ReadingData
{
/**
* @var EntityRepositoryInterface
*/
private $productRepository;
public function __construct(EntityRepositoryInterface $productRepository)
{
$this->productRepository = $productRepository;
}
}

So we registered a custom service called ReadingData and applied the repository as a constructor parameter. If you want to fetch data for another entity, just switch the id in the services.xml to whatever repository you need, e.g. order.repository for orders.

Using the repository

Now that you've injected the repository into your service, you can start using it. First of all, for the following examples you'll need two more imports:

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;

Let's start with the most basic read action now:

public function readData(Context $context): void
{
$products = $this->productRepository->search(new Criteria(), $context);
}

This example assumes that you're using / calling a method called readData on your previously created service. That's it already. It will read some products without any special filtering. The result of the search method will be an instance of an EntitySearchResult, which then contains the collection of products.

The $context is usually passed through to your method, starting from a controller or an event.

Filtering

Now let's get into actually filtering your search result to get more precise results.

Searching for IDs

Often you have an ID from an entity and you just want to find the whole dataset related to that ID, so here you go:

public function readData(Context $context): void
{
$product = $this->productRepository->search(new Criteria([$myId]), $context)->first();
}

This will just find the product with the ID $myId or it will return null, if no product was found with that ID. But how does that work now?

The Criteria object accepts an array of IDs to search for as a constructor parameter. This means, that you could apply more than just one ID here.

The search method will then return a EntitySearchResult, which contains the according entity collection of all products. Even though just one product can be matched here, the method will always return a collection, which then contains your single product. Therefore we're calling first() to get the actual entity, and not the collection as a return.

Searching for any other field

While searching for an ID will do the trick quite often, you might want to search a product by e.g. its name instead.

In order to do this, you can apply filters to the Criteria object, such as an EqualsFilter, which accepts a field name and the value to search for. You can find the EqualsFilter here: Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$products = $this->productRepository->search($criteria, $context);
}

This example will search for all products with the name Example name and return an EntitySearchResult containing all matched products. Since the EntitySearchResult is extending the EntityCollection, which is iterable, you could just iterate over the results using a foreach.

All available fields can be found in the entities' respective definition, ProductDefinition for this example.

Combining filters

What would you do now if you are fine with a product which has either the ID "X" OR the mentioned name "Example name"?

For this case, you can combine filters using the OrFilter or the AndFilter, or the NandFilter, etc.

Let's just build the example mentioned above:

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new OrFilter([
new EqualsFilter('id', 'Your example ID'),
new EqualsFilter('name', 'Example name')
]));
$products = $this->productRepository->search($criteria, $context);
}

So now you'll find all products, that either have the mentioned id OR the mentioned name. The OrFilter can be found here: Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter.

You can find an example for each of the available filters in our DAL reference about filters.

Post filters

Later in this guide you will learn about aggregated data. Sometimes you want to filter the result returned by the DAL, but you don't want those filters to apply to the aggregation result.

E.g.: Fetch all products, whose name is Example product, but also return the total amount of products available.

In that case, you can just use the addPostFilter instead of addFilter:

public function readData(): void
{
$criteria = new Criteria();
$criteria->addPostFilter(new EqualsFilter('name', 'Example name'));
$products = $this->productRepository->search($criteria, $context);
}

This example does not contain any aggregation, since they're only explained later.

Other filters

There's more than just an EqualsFilter, which is the SQL equivalent of WHERE fieldX = valueX. You can find all other filters either on GitHub or with an explanation in our filters reference.

Associations

Of course associations to other entities are also possible in Shopware 6. If you, for example, want to load all product-reviews, which is an entity itself, related to the product you've found, you can do so by adding associations to the criteria object.

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->addAssociation('productReviews');
$products = $this->productRepository->search($criteria, $context);
}

Just like the available entity fields, you can find all possible associations in the entity definition.

Also worth to mention is the fact, that you can chain the association key. E.g. a product-review has another association to the customer, who created that review. If you want access to both the review itself, as well as the customer, you can just write the association like that:

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->addAssociation('productReviews.customer');
$products = $this->productRepository->search($criteria, $context);
}

Filter associations

Yes, this is doable. You can apply filters to an association. E.g. "Add all product reviews to the product, whose rating is above 4 stars.".

For this we can use getAssociation instead, which basically returns its own Criteria object, on which you can apply a filter.

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->getAssociation('productReviews')->addFilter(new RangeFilter('points', [
RangeFilter::GTE => 4
]));
$product = $this->productRepository->search($criteria, $context)->first();
}

Once again: Note, that we used getAssociation here now instead of addAssociation. Also you need the RangeFilter, which can be found here: Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter

Another example to clarify what's going on here:

public function readData(Context $context): void
{
// This will always return the product with the given name, no matter if it has a review with 4 or more stars.
// But only matching reviews are added to the dataset then.
$criteria->getAssociation('productReviews')->addFilter(new RangeFilter('points', [
RangeFilter::GTE => 4
]));
$product = $this->productRepository->search($criteria, $context)->first();
// This will only return products, whose name matches AND which have at least one rating of 4 stars or more
$criteria->addAssociation('productReviews');
$criteria->addFilter(new RangeFilter('productReviews.points', [
RangeFilter::GTE => 4
]));
$product = $this->productRepository->search($criteria, $context)->first();
}

The first will return your product, that you found anyway, and add all matching productReview associations. The latter will just return your product, if it has at least one matching review.

Reading mapping entities

Every ManyToMany association comes with a mapping entity, such as the ProductCategoryDefinition. It's important to know, that you cannot read those mapping entities using the search() method.

The following example will not work:

public function readData(Context $context): void
{
$criteria = new Criteria();
// It's the product_category.repository here
$result = $this->productCategoryRepository->search($criteria, $context);
}

Since mapping entities just consist of two primary keys, there is no need to search for the "full entity" via search. It will suffice to use searchIds instead, which will return the IDs - and that's all there is in a mapping entity.

Aggregations

Of course you can also aggregate your data. Just like filters and associations, this can be done by using an addAggregation method on the Criteria object. Let's create an example aggregation, that returns the average rating for a product:

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->addAssociation('productReviews');
$criteria->addAggregation(new AvgAggregation('avg-rating', 'productReviews.points'));
$products = $this->productRepository->search($criteria, $context);
$rating = $products->getAggregations()->get('avg-rating');
}

Important to note here is that you have to remove the first() call, because we do not need the entity itself but the EntitySearchResult here instead. The AvgAggregation class can be found here: Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\AvgAggregation

A list of all available aggregations can be found on GitHub or in the DAL aggregations reference.

Limiting, paging and sorting

There's just a few more things missing: Limiting your result intentionally to e.g. ten results, adding an offset for paging reasons and sorting the result.

Let's start with the limiting of the result:

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->setLimit(1);
$product = $this->productRepository->search($criteria, $context)->first();
}

That's quite self-explanatory, isn't it? Just use the setLimit method with your desired limit as parameter. Little spoiler: It's the same for the offset!

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->setOffset(1);
$criteria->setLimit(1);
$product = $this->productRepository->search($criteria, $context)->first();
}

This way you get the 2nd possible product. But since you didn't define a sorting yourself, the result can be quite confusing, so let's add a sorting.

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', 'Example name'));
$criteria->setOffset(1);
$criteria->setLimit(1);
$criteria->addSorting(new FieldSorting('createdAt', FieldSorting::ASCENDING));
$product = $this->productRepository->search($criteria, $context)->first();
}

Now you've added an ascending sort by the createdAt field, so the result becomes a lot more predictable. The FieldSorting can be found here: Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting.

Using the RepositoryIterator

Another special way to read data in Shopware is by using the RepositoryInterator.

But what does it do? Basically it's a little helper class that helps you deal with big data sets by being iterable and returning a batch of data with each iteration, but never all data at once.

Imagine you need to iterate over all products of your shop, which contains more than 100000 products. Reading them all out at once and saving this huge set of data into a variable will most likely crash your server with a "memory exhausted" error message.

Instead, the RepositoryIterator will return a batch of data, which size you can define, with each iteration. Just be sure to not use it unnecessarily, since it will create a new database request with each iteration, which is not needed for smaller chunks of data.

public function readData(Context $context): void
{
$criteria = new Criteria();
$criteria->setLimit(500);
$iterator = new RepositoryIterator($this->productRepository, $context, $criteria);
while (($result = $iterator->fetch()) !== null) {
$products = $result->getEntities();
// Do something with the products
}
}

In this example, you'd get a batch of 500 products with each iteration of the while loop. This way, $result will not cause a "memory exhausted" error and you can handle huge amounts of data this way.

And that's basically it for this guide!

Next steps

Now that you know how to read data from the database using the Data Abstraction Layer, you can head over to our guide on writing data.