integration_documentation:plugin:en:integration:shopware_6:extension
Shopware 6 plugin extension
The Findologic extension plugin for Shopware 6 allows manual adaptions to the behavior of the main Findologic plugin.
It's possible override, extend and replace components, to fit the needs of your store. The most common use case is to add additional data to the Findologic product export.
Installation
Download the latest zip file from our GitHub release page.
Follow the installation instructions in the Shopware documentation.
3.x
is compatible with 3.x
and 4.x
is compatible with 4.x
, etc.
Basics
Decorators
Adaptions need to be done using Symfony decorators.
By default the extension plugin decorates the AttributeAdapter
and DefaultPropertiesAdapter
, which are responsible to generate the attributes and properties of a product.
Any adapter in FINDOLOGIC\Shopware6Common\Export\Adapters
can be decorated. The original files are located within vendor/findologic/shopware6-common/src/Export/Adapters
.
src/Resources/config/services.xml
<?xml version="1.0" ?> <container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="FINDOLOGIC\ExtendFinSearch\Export\Adapters\AttributeAdapter" decorates="FINDOLOGIC\Shopware6Common\Export\Adapters\AttributeAdapter" public="true" decoration-on-invalid="ignore" autowire="true" > <argument key="$dynamicProductGroupService" type="service" id="FINDOLOGIC\FinSearch\Export\Services\DynamicProductGroupService" /> <argument key="$catUrlBuilderService" type="service" id="FINDOLOGIC\FinSearch\Export\Services\CatUrlBuilderService" /> <argument key="$translator" type="service" id="Shopware\Core\Framework\Adapter\Translation\Translator" /> </service> <service id="FINDOLOGIC\ExtendFinSearch\Export\Adapters\DefaultPropertiesAdapter" decorates="FINDOLOGIC\Shopware6Common\Export\Adapters\DefaultPropertiesAdapter" public="true" decoration-on-invalid="ignore" autowire="true" > <argument key="$translator" type="service" id="Shopware\Core\Framework\Adapter\Translation\Translator" /> </service> </services> </container>
AttributeAdapter
This class can be used to customize the export of all attributes.
src/Export/Adapters/AttributeAdapter.php
<?php declare(strict_types=1); namespace FINDOLOGIC\ExtendFinSearch\Export\Adapters; use FINDOLOGIC\Export\Data\Attribute; use FINDOLOGIC\Shopware6Common\Export\Adapters\AttributeAdapter as OriginalAttributeAdapter; use Vin\ShopwareSdk\Data\Entity\Product\ProductEntity; class AttributeAdapter extends OriginalAttributeAdapter { public function adapt(ProductEntity $product): array { $attributes = parent::adapt($product); // $attributes[] = new Attribute( // 'Some attribute name', // ['I am an attribute value!'] // ); return $attributes; } }
DefaultPropertiesAdapter
This class can be used to customize the export of the default properties.
src/Export/Adapters/AttributeAdapter.php
<?php declare(strict_types=1); namespace FINDOLOGIC\ExtendFinSearch\Export\Adapters; use FINDOLOGIC\Export\Data\Property; use FINDOLOGIC\Shopware6Common\Export\Adapters\DefaultPropertiesAdapter as OriginalDefaultPropertiesAdapter; use Vin\ShopwareSdk\Data\Entity\Product\ProductEntity; class DefaultPropertiesAdapter extends OriginalDefaultPropertiesAdapter { public function adapt(ProductEntity $product): array { $properties = parent::adapt($product); // $properties[] = new Property( // 'Some property name', // ['' => 'I am a property value!'] // ); return $properties; } }
Autoloading
While composer autoloading is disabled by default, you can always enable it by uncommenting the marked line in \FINDOLOGIC\ExtendFinSearch\ExtendFinSearch
.
Examples
Add custom properties to the export
The base DefaultPropertiesAdapter
allows you to add properties by extending the adapt
function.
//... public function adapt(ProductEntity $product): array { $properties = parent::adapt($product); $properties[] = new Property( 'Some property name', ['' => 'I am a property value!'] ); return $properties; }
The part ['' => 'I am a property value!']
has an empty string as array index. An empty string as an array key, simply means that there is no usergroup, as property data can be usergroup-specific.
You can read more about usergroups in the libflexport documentation, which is the library used to build the export xml.
Add variant data to the export
By default Findologic maps all variants into a single product during product export. However there is still the possibility to show variant information on listing pages, but this information needs to be exported to Findologic.
Please follow the next sections to understand how the core plugin maps variants into a single product.
Basic product mapping
Shopware works with display groups, which are used to take one product and all its variants, and map them to one product. See the following table, which represents a simplified product table:
id | parentId | name | displayGroup |
---|---|---|---|
1 | null | Findologic T-Shirt | null |
2 | 1 | Findologic T-Shirt (Black - White Logo) | 1 |
3 | 1 | Findologic T-Shirt (Gray - Orange Logo) | 1 |
These are three separate products, two of those products are variants of Findologic T-Shirt
. The ProductSearcher
is responsible for fetching all variants of one display group, and mapping them together. This results in main product no longer being Findologic T-Shirt
, but instead Findologic T-Shirt (Black - White Logo)
. To still have the data of the main product available in the export, the main product, and all other variants, are assigned as children of this variant.
Structure:
Findologic T-Shirt (Black - White Logo) ├── children │ ├── Findologic T-Shirt (Gray - Orange Logo) │ └── Findologic T-Shirt
This data structure is exactly like Shopware determines its results on listing pages. The used main variant in this case is typically the cheapest available variant. In case a product has no variants, or fan out properties in the product list is configured, the product has its own display group, so none of this reordering is happening.
Basic code structure
This extension will use the defined events from the main plugin.
src/Subscriber/ProductSubscriber.php
<?php declare(strict_types=1); namespace FINDOLOGIC\ExtendFinSearch\Subscriber; use FINDOLOGIC\Export\Data\Property; use FINDOLOGIC\Shopware6Common\Export\Adapters\AdapterFactory; use FINDOLOGIC\Shopware6Common\Export\Events\AfterItemBuildEvent; use FINDOLOGIC\Shopware6Common\Export\Events\AfterVariantAdaptEvent; use FINDOLOGIC\Shopware6Common\Export\Events\BeforeItemAdaptEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Vin\ShopwareSdk\Data\Entity\Product\ProductEntity; class ProductSubscriber implements EventSubscriberInterface { /** @var AdapterFactory */ private $adapterFactory; private $variantData = []; public function __construct(AdapterFactory $adapterFactory) { $this->adapterFactory = $adapterFactory; } public static function getSubscribedEvents() { return [ AfterVariantAdaptEvent::NAME => 'afterVariantAdapted', AfterItemBuildEvent::NAME => 'afterItemCompleted' ]; } public function afterVariantAdapted(AfterVariantAdaptEvent $event) { $product = $event->getProduct(); $this->variantData[] = [ // Your variant data ]; } public function afterItemCompleted(AfterItemBuildEvent $event) { $item = $event->getItem(); if (count($this->variantData)) { $item->addProperty( new Property('variants', [ '' => json_encode($this->variantData) ]) ); } $this->variantData = []; } }
src/Resources/config/services.xml
<?xml version="1.0" ?> <container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="FINDOLOGIC\ExtendFinSearch\Subscriber\ProductSubscriber" class="FINDOLOGIC\ExtendFinSearch\Subscriber\ProductSubscriber" public="true" > <tag name="kernel.event_subscriber" /> <argument type="service" id="FINDOLOGIC\Shopware6Common\Export\Adapters\AdapterFactory" /> </service> </services> </container>
Add basic variant data
src/Subscriber/ProductSubscriber.php
//... public function afterVariantAdapted(AfterVariantAdaptEvent $event) { $product = $event->getProduct(); $this->variantData[] = [ 'name' => $this->getName($product), 'url' => $this->getUrl($product), 'price' => $this->getPrice($product) ]; } //... private function getName(ProductEntity $productEntity): string { $name = $this->adapterFactory->getNameAdapter()->adapt($productEntity); return $name ? $name->getValues()[''] : ''; } private function getUrl(ProductEntity $productEntity): string { $url = $this->adapterFactory->getUrlAdapter()->adapt($productEntity); return $url ? $url->getValues()[''] : ''; } private function getPrice(ProductEntity $productEntity): string { $prices = $this->adapterFactory->getPriceAdapter()->adapt($productEntity); $prices = array_filter($prices, function (Price $price) { return array_key_exists('', $price->getValues()); }); return count($prices) ? current($prices)->getValues()[''] : ''; } //...
This will export the name
, url
and price
of each variant, the JSON would be like:
[ { "name": "Findologic T-Shirt (Gray - Orange Logo)", "url": "https://store.com/Findologic-T-Shirt-Gray-Orange-Logo/411dc735cade4c8789421f9c2aaec51f", "price": 59.99 }, { "name": "Findologic T-Shirt", "url": "https://store.com/Findologic-T-Shirt/ad99a6257e1546f08dbe9886a48e4230", "price": 55.99 }, { "name": "Findologic T-Shirt (Black - White Logo)", "url": "https://store.com/Findologic-T-Shirt/7c18c5ff8aa548e1bdba6b738ac42f71", "price": 49.99 } ]
Add images
For variant images, you will need to add the relevant variant associations as shown here. (cover
and media
)
Additionally:
src/Subscriber/ProductSubscriber.php
// ... public function afterVariantAdapted(AfterVariantAdaptEvent $event) { $product = $event->getProduct(); $this->variantData[] = [ // ... 'image' => $this->getImageUrl($product) ]; } // ... private function getImageUrl(ProductEntity $productEntity): string { $images = $this->adapterFactory->getImagesAdapter()->adapt($productEntity); return count($images) ? current($images)->getUrl() : ''; }
Deal with variant-specific data
src/Subscriber/ProductSubscriber.php
// ... private const VARIANT_PROPERTY = 'color'; //... public function afterVariantAdapted(AfterVariantAdaptEvent $event) { $product = $event->getProduct(); foreach ($product->properties as $variantProperty) { // Ignore all properties except the property we want to export our variants off. if ( !$variantProperty->group || $variantProperty->group->getTranslation('name') !== self::VARIANT_PROPERTY) { continue; } $propertyName = $variantProperty->getTranslation('name'); $this->variantData[$propertyName] = [ 'name' => $this->getName($product), 'url' => $this->getUrl($product), 'price' => $this->getPrice($product), 'image' => $this->getImageUrl($product), self::VARIANT_PROPERTY => $propertyName ]; } } // ...
After this implementation, the JSON would be like:
{ "Gray": { "name": "Findologic T-Shirt (Gray - Orange Logo)", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt-Gray-Orange-Logo/411dc735cade4c8789421f9c2aaec51f", "color": "Gray" }, "Black": { "name": "Findologic T-Shirt (Black - White Logo)", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt/7c18c5ff8aa548e1bdba6b738ac42f71", "color": "Black" } }
Usergroup-specific variants
src/Subscriber/ProductSubscriber.php
//... public function afterVariantAdapted(AfterVariantAdaptEvent $event) { $product = $event->getProduct(); $basicVariantData = [ 'name' => $this->getName($product), 'url' => $this->getUrl($product), 'image' => $this->getImageUrl($product) ]; foreach ($this->adapterFactory->getPriceAdapter()->adapt($product) as $price) { $values = $price->getValues(); $variantData = array_merge( $basicVariantData, [ 'price' => current($values) ] ); $this->variantData[array_key_first($values)][] = json_encode($variantData); } } public function afterItemCompleted(AfterItemBuildEvent $event) { $variantData = []; foreach ($this->variantData as $usergroup => $userGroupVariantData) { $variantData[$usergroup] = json_encode($userGroupVariantData); } $item = $event->getItem(); if (count($this->variantData)) { $item->addProperty( new Property('variants', $variantData) ); } } //...
variants
must be exported for all usergroups. If a usergroup is not exported, this usergroup will not have any variants
data.
An XML export with usergroup in properties may look like:
<!-- ... --> <allProperties> <property usergroup="JyInVwMCBQtTdQ1RAwkmCSRXUVkGBCZ6B1YgDyFXVnc="> <key> <![CDATA[ variants ]]> </key> <value> <![CDATA[ [ { "name": "Findologic T-Shirt (Gray - Orange Logo)", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt-Gray-Orange-Logo/411dc735cade4c8789421f9c2aaec51f", "price": 59.99 }, { "name": "Findologic T-Shirt", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt/ad99a6257e1546f08dbe9886a48e4230", "price": 55.99 }, { "name": "Findologic T-Shirt (Black - White Logo)", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt/7c18c5ff8aa548e1bdba6b738ac42f71", "price": 49.99 } ] ]]> </value> </property> <property usergroup="dX1WUgsgCyVUIHcPcXYFUw1xUlBZUwcAB1FUcgNQCwA="> <key> <![CDATA[ variants ]]> </key> <value> <![CDATA[ [ { "name": "Findologic T-Shirt (Gray - Orange Logo)", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt-Gray-Orange-Logo/411dc735cade4c8789421f9c2aaec51f", "price": 599.99 }, { "name": "Findologic T-Shirt", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt/ad99a6257e1546f08dbe9886a48e4230", "price": 559.99 }, { "name": "Findologic T-Shirt (Black - White Logo)", "image": "https://store.com/some/path/to/image_800x800.jpg", "url": "https://store.com/Findologic-T-Shirt/7c18c5ff8aa548e1bdba6b738ac42f71", "price": 499.99 } ] ]]> </value> </property> </allProperties> <!-- ... -->
Add product or variant associations
Sometimes the plugin doesn't add the associations needed in the extension. This may cause the associated field to always return `null. It's also the case if third party plugins extend the product entity with an additional table. Other examples include image URLs or color codes from variant properties.
Decorate FINDOLOGIC\FinSearch\Export\Search\ProductCriteriaBuilder
:
src/Export/Search/ProductCriteriaBuilder.php
<?php declare(strict_types=1); namespace FINDOLOGIC\ExtendFinSearch\Export\Search; use FINDOLOGIC\FinSearch\Export\Search\ProductCriteriaBuilder as OriginalProductCriteriaBuilder; class ProductCriteriaBuilder extends OriginalProductCriteriaBuilder { public function withProductAssociations(): OriginalProductCriteriaBuilder { parent::withProductAssociations(); $this->criteria->addAssociations([ // Additional associations ]); return $this; } public function withVariantAssociations(): OriginalProductCriteriaBuilder { parent::withVariantAssociations(); $this->criteria->addAssociations([ 'cover', 'media' ]); return $this; } }
src/Resources/config/services.xml
<?xml version="1.0" ?> <container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <!-- ... --> <service id="FINDOLOGIC\ExtendFinSearch\Export\Search\ProductCriteriaBuilder" decorates="FINDOLOGIC\FinSearch\Export\Search\ProductCriteriaBuilder" public="true" decoration-on-invalid="ignore" autowire="true" /> <!-- ... --> </services> </container>
Cookie Consent for Direct Integration
In case it's required by the integration, you are required to add a cookie consent. Please see the Shopware 6 documentation about Adding a cookie to the cookie manager.
There is also an example available in our GitHub repository.
Add custom sorting options for API Integration
The Findologic base plugin already provides sorting options for the most common use-cases. The plugin uses SortingHandler
to send the currently selected sorting option via API parameters to the Findologic Search-API (see all available SortingHandlers).
Therefore to handle custom sorting options, create a custom SortingHandler
in the extension plugin, and override the responsible SortingHandlerService
to include the created SortingHandler
.
Prerequisites
Before a custom sorting can be used, make sure to export the value for the custom sort in the <sort>
field in the export. See the XML Format documentation for further details.
Implementation
Step 1: Create a SortingHandler
Create the folder structure Core/Content/Product/SalesChannel/Listing/SortingHandler
in the extension plugin, and add a custom sorting handler class. In this example it will be ThirdPartySortingHandler
:
use FINDOLOGIC\Api\Requests\SearchNavigation\SearchNavigationRequest; use FINDOLOGIC\FinSearch\Core\Content\Product\SalesChannel\Listing\SortingHandler\SortingHandlerInterface; use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting; class ThirdPartySortingHandler implements SortingHandlerInterface { public function supportsSorting(FieldSorting $fieldSorting): bool { // Enter your custom sort here. // To get the name of the sort, select the sort in the storefront and // add a dd($fieldSorting) here and refresh the page. return $fieldSorting->getField() === 'product.third_party_extension.field'; } public function generateSorting(FieldSorting $fieldSorting, SearchNavigationRequest $searchNavigationRequest): void { $searchNavigationRequest->setOrder('shopsort ' . $fieldSorting->getDirection()); } }
Step 2: Decorate the SortingHandlerService
The SortingHandlerService
holds all available sorting options. Therefore, you want to add your custom sort to this service. The service can be found in Findologic/Request/Handler
. Simply override the getSortingHandlers
method to include your own custom sorting handler.
namespace FINDOLOGIC\ExtendFinSearch\Findologic\Request\Handler; use FINDOLOGIC\ExtendFinSearch\Core\Content\Product\SalesChannel\Listing\SortingHandler\ThirdPartySortingHandler; use FINDOLOGIC\FinSearch\Core\Content\Product\SalesChannel\Listing\SortingHandler\SortingHandlerInterface; use FINDOLOGIC\FinSearch\Findologic\Request\Handler\SortingHandlerService as OriginalSortingHandlerService; class SortingHandlerService extends OriginalSortingHandlerService { /** * @return SortingHandlerInterface[] */ protected function getSortingHandlers(): array { return array_merge( parent::getSortingHandlers(), [ new ThirdPartySortingHandler() ] ); } }
Step 3: Add the decorated service to the services.xml
As a last step, simply decorate in your src/Resources/config/services.xml
the service of the main plugin:
<?xml version="1.0" ?> <container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="FINDOLOGIC\ExtendFinSearch\Findologic\Request\Handler\SortingHandlerService" decorates="FINDOLOGIC\FinSearch\Findologic\Request\Handler\SortingHandlerService" decoration-on-invalid="ignore" /> </services> </container>
Once this step is done, selecting your relevant sorting option will send the order parameter to the Findologic API.