Using the data handling
The Shopware 6 Administration allows you to fetch and write nearly everything in the database. This guide will teach you the basics of the data handling.
Prerequisites
All you need for this guide is a running Shopware 6 instance and full access to both the files, as well as the command line and preferably registered module. Of course you'll have to understand JavaScript, but that's a prerequisite for Shopware as a whole and will not be taught as part of this documentation.
Considering that the data handling in the Administration is remotely operating the Data Abstraction Layer its highly encouraged to read the articles Reading data with the DAL and Writing data with the DAL.
Relevant classes
Repository
: Allows to send requests to the server - used for all CRUD operations Entity
: Object for a single storage record EntityCollection
: Enable object-oriented access to a collection of entities SearchResult
: Contains all information available through a search request RepositoryFactory
: Allows to create a repository for an entity Context
: Contains the global state of the Administration (language, version, auth, ...) Criteria
: Contains all information for a search request (filter, sorting, pagination, ...)
The repository service
Accessing the Shopware API in the Administration is done by using the repository service, which can be injected with a bottleJs dependency injection container. In the Shopware Administration, there's a wrapper that makes bottleJs
work with the inject / provide from Vue
. In short: You can use the inject
key in your component configuration to fetch services from the bottleJs
DI container, such as the repositoryFactory
, that you will need in order to get a repository for a single entity.
Add those lines to your component configuration:
inject: [
'repositoryFactory'
],
This way the repositoryFactory
object is accessible in your component. The create
function can be used to create a repository for a single entity, like in this example:
const productRepository = this.repositoryFactory.create('product')
Note: You can also change some options in the repository, with the third parameter:
Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
repository: undefined
}
},
created() {
const options = {
version: 1 // default is the latest api version
};
this.repository = this.repositoryFactory.create('product', null, options);
}
});
Note: The version 1 used in the options is just an example, how to select a version. Then again the default would be the newest version. There are no other options.
Working with the criteria class
To fetch data from the server, the repository has a search
function. Each repository function requires the API context
and criteria
class, which contains all functionality of the core criteria class. If you want to see all the options take a look at the file src/Administration/Resources/app/administration/src/core/data/criteria.data.ts.
const { Criteria } = Shopware.Data;
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
result: undefined
}
},
computed: {
productRepository() {
// create a repository for the `product` entity
return this.repositoryFactory.create('product');
},
},
created() {
const criteria = new Criteria();
criteria.setPage(1);
criteria.setLimit(10);
criteria.setTerm('foo');
criteria.setIds(['some-id', 'some-id']); // Allows to provide a list of ids which are used as a filter
/**
* Configures the total value of a search result.
* 0 - no total count will be selected. Should be used if no pagination required (fastest)
* 1 - exact total count will be selected. Should be used if an exact pagination is required (slow)
* 2 - fetches limit * 5 + 1. Should be used if pagination can work with "next page exists" (fast)
*/
criteria.setTotalCountMode(2);
criteria.addFilter(
Criteria.equals('product.active', true)
);
criteria.addSorting(
Criteria.sort('product.name', 'DESC')
);
criteria.addAggregation(
Criteria.avg('average_price', 'product.price')
);
criteria.getAssociation('categories')
.addSorting(Criteria.sort('category.name', 'ASC'));
this.productRepository.create('product');
this.productRepository
.search(criteria, Shopware.Context.api)
.then(result => {
this.result = result;
});
}
});
How to fetch a single entity
Since the context of an edit or update form is usually a single root entity, the data handling diverges here from the Data Abstraction Layer and provides loading of a single resource from the Admin API.
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
entity: undefined
}
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
}
},
created() {
const entityId = 'some-id';
this.productRepository
.get(entityId, Shopware.Context.api)
.then(entity => {
this.entity = entity;
});
}
});
Update an entity
The data handling contains change tracking and sends only changed properties to the Admin API endpoint. Please be aware that in order to be as transparent as possible, updating data will not be handled automatically. A manual update is mandatory.
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
entityId: '1de38487abf04705810b719d4c3e8faa',
entity: undefined
}
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
}
},
created() {
this.productRepository
.get(this.entityId, Shopware.Context.api)
.then(entity => {
this.entity = entity;
});
},
methods: {
// a function which is called over the ui
updateTrigger() {
this.entity.name = 'updated';
// sends the request immediately
this.productRepository
.save(this.entity, Shopware.Context.api)
.then(() => {
// the entity is stateless, the data has be fetched from the server, if required
this.productRepository
.get(this.entityId, Shopware.Context.api)
.then(entity => {
this.entity = entity;
});
});
}
}
});
Delete an entity
The delete
method sends a delete
request for a provided id. To delete multiple entities at once use the syncDeleted
method by passing an array of ids
.
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
computed: {
productRepository() {
return this.repositoryFactory.create('product');
}
},
created() {
this.productRepository.delete('1de38487abf04705810b719d4c3e8faa', Shopware.Context.api);
}
});
Create an entity
Although entities are detached from the data handling once retrieved or created they still must be set up through a repository. You can create an entity by using the this.repositoryFactory.create()
method, fill it with data and save it as seen below:
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
entity: undefined
}
},
computed: {
manufacturerRepository() {
return this.repositoryFactory.create('product_manufacturer');
}
},
created() {
this.entity = this.manufacturerRepository.create(Shopware.Context.api);
this.entity.name = 'test';
this.manufacturerRepository.save(this.entity, Shopware.Context.api);
}
});
Working with associations
Each association can be accessed via normal property access:
const { Criteria } = Shopware.Data;
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined
}
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
productCriteria() {
return new Criteria()
.addAssociation('manufacturer')
.addAssociation('categories')
.addAssociation('prices');
}
},
created() {
this.repository = this.repositoryFactory.create('product');
const entityId = '66338d4e19f749fd90b59032134ecb74';
this.repository
.get(entityId, Shopware.Context.api, this.productCriteria)
.then(product => {
this.product = product;
// ManyToOne: contains an entity class with the manufacturer data
console.log(this.product.manufacturer);
// ManyToMany: contains an entity collection with all categories.
// contains a source property with an api route to reload this data (/product/{id}/categories)
console.log(this.product.categories);
// OneToMany: contains an entity collection with all prices
// contains a source property with an api route to reload this data (/product/{id}/prices)
console.log(this.product.prices);
});
}
});
Set a ManyToOne
If you have a ManyToOne association, you can write changes as seen below:
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined,
};
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
manufacturerRepository() {
return this.repositoryFactory.create('product_manufacturer');
}
},
created() {
this.productRepository
.get('some-product-id', Shopware.Context.api)
.then((product) => {
this.product = product;
this.product.manufacturerId = 'some-manufacturer-id'; // manually set the foreign key y
this.productRepository.save(this.product, Shopware.Context.api);
});
},
});
Working with lazy loaded associations
In most cases, ToMany associations can be loaded by adding a the association with the .addAssociation()
method of the Criteria object.
const { Criteria } = Shopware.Data;
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined
};
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
productCriteria() {
const criteria = new Criteria();
criteria.addAssociation('prices');
return criteria;
}
},
created() {
this.productRepository
.get('some-id', Shopware.Context.api, this.productCriteria)
.then((product) => {
this.product = product;
});
}
});
Working with OneToMany associations
The following example shows how to create a repository based on associated data. In this case the priceRepository
contains associated prices
to the product with the id
'some-id'.
const { Criteria } = Shopware.Data;
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined,
prices: undefined
};
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
priceRepository() {
if (!this.product) {
return undefined;
};
return this.repositoryFactory.create(
// `product_price`
this.product.prices.entity,
// `product/some-id/priceRules`
this.product.prices.source
);
}
},
created() {
this.productRepository
.get('some-product-id', Shopware.Context.api)
.then((product) => {
this.product = product;
});
},
methods: {
loadPrices() {
this.priceRepository
.search(new Criteria(), Shopware.Context.api)
.then((prices) => {
this.prices = prices;
});
},
addPrice() {
const newPrice = this.priceRepository.create(Shopware.Context.api);
newPrice.quantityStart = 1;
// Note: there are more things required than just the quantityStart
this.priceRepository
.save(newPrice, Shopware.Context.api)
.then(this.loadPrices);
},
deletePrice(priceId) {
this.priceRepository
.delete(priceId, Shopware.Context.api)
.then(this.loadPrices);
},
updatePrice(price) {
this.priceRepository
.save(price, Shopware.Context.api)
.then(this.loadPrices);
}
}
});
Working with ManyToMany associations
The following example shows how to create a repository based on associated data. In this case the categoryRepository
contains associated categories to the product with the id
'some-id'.
const { Criteria } = Shopware.Data;
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined,
categories: undefined
};
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
categoryRepository() {
if (!this.product) {
return undefined;
};
return this.repositoryFactory.create(
// `product_categories`
this.product.categories.entity,
// `product/some-id/categories`
this.product.categories.source
);
}
},
created() {
this.productRepository
.get('some-product-id', Shopware.Context.api)
.then((product) => {
this.product = product;
});
},
methods: {
loadCategories() {
this.categoryRepository
.search(new Criteria(), Shopware.Context.api)
.then((categories) => {
this.categories = categories;
});
},
addCategoryToProduct(category) {
this.categoryRepository
.assign(category.id, Shopware.Context.api)
.then(this.loadCategories);
},
removeCategoryFromProduct(categoryId) {
this.categoryRepository
.delete(categoryId, Shopware.Context.api)
.then(this.loadCategories);
}
}
});
Working with local associations
In case of a new entity, the associations can not be sent directly to the server using the repository, because the parent entity isn't saved yet. For example: You can not add prices to a product which is not even saved in the database yet.
For this case the association can be used as storage as well and will be updated with the parent entity. In the following examples, this.productRepository.save(this.product, Shopware.Context.api)
will send the prices and category changes.
Notice: It is mandatory to add
entities to collections in order to get reactive data for the UI.
Working with local OneToMany associations
The following example shows how to create a repository based on associated data. In this case the priceRepository
contains associated prices
to the product with the id
'some-id'.
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined
};
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
priceRepository() {
if (!this.product) {
return undefined;
};
this.priceRepository = this.repositoryFactory.create(
// `product_price`
this.product.prices.entity,
// `product/some-id/priceRules`
this.product.prices.source
);
}
},
created() {
this.productRepository
.get('some-id', Shopware.Context.api)
.then(product => {
this.product = product;
});
},
methods: {
loadPrices() {
this.prices = this.product.prices;
},
addPrice() {
const newPrice = this.priceRepository
.create(Shopware.Context.api);
newPrice.quantityStart = 1;
// update some other fields
this.product.prices.add(newPrice);
},
savePrice() {
this.productRepository.save(this.product)
},
deletePrice(priceId) {
this.product.prices.remove(priceId);
},
updatePrice(price) {
// price entity is already updated and already assigned to product, no sources needed
}
}
});
Working with local ManyToMany associations
The following example shows how to create a repository based on associated data. In this case the categoryRepository
contains associated categories to the product with the id
'some-id'.
Shopware.Component.register('swag-basic-example', {
inject: ['repositoryFactory'],
template,
data: function () {
return {
product: undefined,
prices: undefined
};
},
computed: {
productRepository() {
return this.repositoryFactory.create('product');
},
priceRepository() {
if (!this.product) {
return undefined;
};
return this.repositoryFactory.create(
// `product_price`
this.product.prices.entity,
// `product/some-id/priceRules`
this.product.prices.source
);
}
},
created() {
this.productRepository
.get('some-id', Shopware.Context.api)
.then(product => {
this.product = product;
});
},
methods: {
loadPrices() {
this.prices = this.product.prices;
},
addPrice() {
const newPrice = this.priceRepository
.create(Shopware.Context.api);
newPrice.quantityStart = 1;
// update some other fields
this.product.prices.add(newPrice);
},
savePrice() {
this.productRepository.save(this.product)
},
deletePrice(priceId) {
this.product.prices.remove(priceId);
},
updatePrice(price) {
// price entity is already updated and already assigned to product, no sources needed
}
}
});
Working with entity extensions
The following example shows how to pass on and save data of entity extensions.
import template from './swag-paypal-pos-wizard.html.twig';
import './swag-paypal-pos-wizard.scss';
import {
PAYPAL_POS_SALES_CHANNEL_EXTENSION,
PAYPAL_POS_SALES_CHANNEL_TYPE_ID,
} from '../../../../../constant/swag-paypal.constant';
const { Component, Context } = Shopware;
const { Criteria } = Shopware.Data;
Component.extend('swag-paypal-pos-wizard', 'sw-first-run-wizard-modal', {
template,
inject: [
'SwagPayPalPosApiService',
'SwagPayPalPosSettingApiService',
'SwagPayPalPosWebhookRegisterService',
'salesChannelService',
'repositoryFactory',
],
mixins: [
'swag-paypal-pos-catch-error',
'notification',
],
data() {
return {
showModal: true,
isLoading: false,
salesChannel: {},
cloneSalesChannelId: null,
stepperPages: [
'connection',
'connectionSuccess',
'connectionDisconnect',
'customization',
'productSelection',
'syncLibrary',
'syncPrices',
'finish',
],
stepper: {},
currentStep: {},
};
},
metaInfo() {
return {
title: this.wizardTitle,
};
},
computed: {
paypalPosSalesChannelRepository() {
return this.repositoryFactory.create('swag_paypal_pos_sales_channel');
},
salesChannelRepository() {
return this.repositoryFactory.create('sales_channel');
},
salesChannelCriteria() {
return (new Criteria(1, 500))
.addAssociation(PAYPAL_POS_SALES_CHANNEL_EXTENSION)
.addAssociation('countries')
.addAssociation('currencies')
.addAssociation('domains')
.addAssociation('languages');
},
},
watch: {
'$route'(to) {
this.handleRouteUpdate(to);
},
},
mounted() {
this.mountedComponent();
},
methods: {
//...
createdComponent() {
//...
this.createNewSalesChannel();
},
save(activateSalesChannel = false, silentWebhook = false) {
if (activateSalesChannel) {
this.salesChannel.active = true;
}
return this.salesChannelRepository.save(this.salesChannel, Context.api).then(async () => {
this.isLoading = false;
this.isSaveSuccessful = true;
this.isNewEntity = false;
this.$root.$emit('sales-channel-change');
await this.loadSalesChannel();
this.cloneProductVisibility();
this.registerWebhook(silentWebhook);
}).catch(() => {
this.isLoading = false;
this.createNotificationError({
message: this.$tc('sw-sales-channel.detail.messageSaveError', 0, {
name: this.salesChannel.name || this.placeholder(this.salesChannel, 'name'),
}),
});
});
},
createNewSalesChannel() {
if (Context.api.languageId !== Context.api.systemLanguageId) {
Context.api.languageId = Context.api.systemLanguageId;
}
this.previousApiKey = null;
this.salesChannel = this.salesChannelRepository.create(Context.api);
this.salesChannel.typeId = PAYPAL_POS_SALES_CHANNEL_TYPE_ID;
this.salesChannel.name = this.$tc('swag-paypal-pos.wizard.salesChannelPrototypeName');
this.salesChannel.active = false;
this.salesChannel.extensions.paypalPosSalesChannel
= this.paypalPosSalesChannelRepository.create(Context.api);
Object.assign(
this.salesChannel.extensions.paypalPosSalesChannel,
{
mediaDomain: '',
apiKey: '',
imageDomain: '',
productStreamId: null,
syncPrices: true,
replace: 0,
},
);
this.salesChannelService.generateKey().then((response) => {
this.salesChannel.accessKey = response.accessKey;
}).catch(() => {
this.createNotificationError({
message: this.$tc('sw-sales-channel.detail.messageAPIError'),
});
});
},
loadSalesChannel() {
const salesChannelId = this.$route.params.id || this.salesChannel.id;
if (!salesChannelId) {
return new Promise((resolve) => { resolve(); });
}
this.isLoading = true;
return this.salesChannelRepository.get(salesChannelId, Shopware.Context.api, this.salesChannelCriteria)
.then((entity) => {
this.salesChannel = entity;
this.previousApiKey = entity.extensions.paypalPosSalesChannel.apiKey;
this.isLoading = false;
});
},
//...
},
});
Next steps
As this is very similar to the DAL it might be interesting to learn more about that. For this, head over to the section about the data handling in PHP.