integration_documentation:plugin:en:integration:shopware_6:extension_old

integration_documentation:plugin:en:integration:shopware_6:extension_old

Shopware 6 plugin extension Old

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.

Download the latest zip file from our GitHub release page.

Follow the installation instructions in the Shopware documentation.

Please make sure to use the same major version as the base Findologic plugin. This means that 1.x is compatible with 1.x and 2.x is compatible with 2.x, etc.

Decorators

Adaptions need to be done using Symfony decorators.

By default the extension plugin decorates the FindologicProductFactory which is responsible for creating new FindologicProduct instances. Those products are used by the export to build an XML.

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\FindologicProductFactory"
            decorates="FINDOLOGIC\FinSearch\Export\FindologicProductFactory"
            public="true"
            decoration-on-invalid="ignore"
        >
        </service>
 
    </services>
</container>

FindologicProductFactory

With the decoration in place the plugin returns FindologicProduct instances (imported as FindologicProductOverride) via the extension plugin.

src/Export/FindologicProductFactory.php

<?php
 
declare(strict_types=1);
 
namespace FINDOLOGIC\ExtendFinSearch\Export;
 
use FINDOLOGIC\ExtendFinSearch\Struct\FindologicProduct as FindologicProductOverride;
use FINDOLOGIC\Export\Data\Item;
use FINDOLOGIC\FinSearch\Exceptions\ProductHasNoCategoriesException;
use FINDOLOGIC\FinSearch\Exceptions\ProductHasNoNameException;
use FINDOLOGIC\FinSearch\Exceptions\ProductHasNoPricesException;
use Psr\Container\ContainerInterface;
use Shopware\Core\Checkout\Customer\Aggregate\CustomerGroup\CustomerGroupEntity;
use Shopware\Core\Content\Product\ProductEntity;
use Symfony\Component\Routing\RouterInterface;
 
class FindologicProductFactory
{
    /**
     * @param CustomerGroupEntity[] $customerGroups
     *
     * @throws ProductHasNoCategoriesException
     * @throws ProductHasNoNameException
     * @throws ProductHasNoPricesException
     */
    public function buildInstance(
        ProductEntity $product,
        RouterInterface $router,
        ContainerInterface $container,
        string $shopkey,
        array $customerGroups,
        Item $item
    ): FindologicProductOverride {
        return new FindologicProductOverride($product, $router, $container, $shopkey, $customerGroups, $item);
    }
}

FindologicProduct

This class can be used to customize functionality of the main plugin.

src/Struct/FindologicProduct.php

<?php declare(strict_types=1);
 
namespace FINDOLOGIC\ExtendFinSearch\Struct;
 
use FINDOLOGIC\Export\Data\Attribute;
use FINDOLOGIC\Export\Data\Item;
use FINDOLOGIC\Export\Data\Property;
use FINDOLOGIC\FinSearch\Struct\FindologicProduct as OriginalFindologicProduct;
use Psr\Container\ContainerInterface;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Framework\Context;
use Symfony\Component\Routing\RouterInterface;
 
class FindologicProduct extends OriginalFindologicProduct
{
    public function __construct(
        ProductEntity $product,
        RouterInterface $router,
        ContainerInterface $container,
        string $shopkey,
        array $customerGroups,
        Item $item
    ) {
        parent::__construct($product, $router, $container, $shopkey, $customerGroups, $item);
    }
 
    protected function setProperties(): void
    {
        // Example of adding a new property:
//        $this->properties[] = new Property(
//            'Some property name',
//            ['' => 'I am a property value!']
//        );
 
        parent::setProperties();
    }
 
    protected function setAttributes(): void
    {
        // Example of adding a new attribute:
//        $this->attributes[] = new Attribute(
//            'Some attribute name',
//            ['I am an attribute value!']
//        );
 
        parent::setAttributes();
    }
}

Autoloading

You only need composer autoloading in case you require additional composer dependencies.

https://docs.findologic.com/lib/exe/fetch.php?t=1603273975&tok=ddd8c2&media=integration_documentation:extension.png

While composer autoloading is disabled by default, you can always enable it by uncommenting the marked line in \FINDOLOGIC\ExtendFinSearch\ExtendFinSearch.

Examples

The base FindologicProduct exposes its class properties and methods as protected, which allows you to manually add attributes and properties, by assigning it directly to the property.

src/Struct/FindologicProduct.php

use FINDOLOGIC\Export\Data\Attribute;
use FINDOLOGIC\Export\Data\Property;
 
// ...
protected function setAttributes(): void
{
    $this->attributes[] = new Attribute(
        'Some attribute name',
        ['I am an attribute value!']
    );
 
    parent::setAttributes();
}
 
protected function setProperties(): void
{
    $this->properties[] = new Property(
        'Some property name',
        ['' => 'I am a property value!']
    );
 
    parent::setProperties();
}
// ...

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.

In case you have usergroup-specific data, properties which should be available for all usergroups, must be added individually for each usergroup.

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 ProductService 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.

Get variant data

To get all variants including the actual main product, the children need to be available in the same collection.

src/Struct/FindologicProduct.php

use Shopware\Core\Content\Product\ProductCollection;
 
private function getAllVariants(): ProductCollection
{
    $variants = $this->product->getChildren();
    $variants->add($this->product);
 
    return $variants;
}

This will automatically add the main variant as a child of itself. Please note that this will not remove the children which were previously assigned to the main variant.

Build JSON

A simple JSON may only contain the name and the image of the variant.

src/Struct/FindologicProduct.php

use FINDOLOGIC\Export\Data\Property;
use Shopware\Core\Content\Product\ProductEntity;
 
protected function setProperties(): void
{
    parent::setProperties();
    $this->addVariants();
}
 
private function addVariants(): void
{
    $variantData = [];
 
    $variants = $this->getAllVariants();
    foreach ($variants as $variant) {
        $variantData[] = [
            'name' => $variant->getTranslation('name'),
            'image' => $this->getImageUrl($variant),
        ];
    }
 
    $this->properties[] = new Property('variants', ['' => json_encode($variantData)]);
}
 
private function getImageUrl(ProductEntity $variant): ?string
{
    $images = $this->productImageService->getProductImages($variant);
    if (count($images) === 0) {
        return null;
    }
 
    return array_values($images)[0]->getUrl();
}

This will export the name and the image of each variant, the JSON would be like:

[
  {
    "name": "Findologic T-Shirt (Gray - Orange Logo)",
    "image": "https://store.com/some/path/to/image_800x800.jpg"
  },
  {
    "name": "Findologic T-Shirt",
    "image": "https://store.com/some/path/to/image_800x800.jpg"
  },
  {
    "name": "Findologic T-Shirt (Black - White Logo)",
    "image": "https://store.com/some/path/to/image_800x800.jpg"
  }
]

Variant URLs

In most cases the user should have the possibility to directly link to a specific variant.

src/Struct/FindologicProduct.php

use Shopware\Core\Content\Product\ProductEntity;
 
private function addVariants(): void
{
    $variantData = [];
 
    $variants = $this->getAllVariants();
    foreach ($variants as $variant) {
        $variantData[] = [
            // ...
            'url' => $this->getVariantUrl($variant),
        ];
    }
 
    $this->properties[] = new Property('variants', ['' => json_encode($variantData)]);
}
 
private function getVariantUrl(ProductEntity $variant): string
{
    $baseUrl = $this->getTranslatedDomainBaseUrl();
    $seoPath = $this->getTranslatedSeoPath($variant);
 
    if ($baseUrl && $seoPath) {
        return sprintf('%s/%s', $baseUrl, $seoPath);
    }
 
    // Fallback to manual URL generation, if no SEO URL has been generated yet.
    return $this->router->generate(
        'frontend.detail.page',
        ['productId' => $variant->getId()],
        RouterInterface::ABSOLUTE_URL
    );
}
 
/**
 * Override existing getTranslatedSeoPath from the core plugin, to allow passing a
 * variant instead of $this->product.
 */
protected function getTranslatedSeoPath(?ProductEntity $productEntity = null): ?string
{
    $product = $productEntity ?? $this->product;
 
    $salesChannel = $this->salesChannelContext->getSalesChannel();
    if (!$seoUrls = $product->getSeoUrls()) {
        return null;
    }
 
    $seoUrlCollection = $seoUrls->filterBySalesChannelId($salesChannel->getId());
    $collectionWithoutDeletedEntities = $this->filterDeletedSeoUrls($seoUrlCollection);
 
    $seoUrlEntities = $this->getTranslatedEntities($collectionWithoutDeletedEntities);
    if (!$seoUrlEntities) {
        return null;
    }
 
    $canonicalSeoUrl = $seoUrlEntities->filter(function (SeoUrlEntity $entity) {
        return $entity->getIsCanonical();
    })->first();
 
    $seoUrlEntity = $canonicalSeoUrl ?? $seoUrlEntities->first();
 
    return $seoUrlEntity ? ltrim($seoUrlEntity->getSeoPathInfo(), '/') : null;
}

Using the name and the image implementation from above, the JSON would be like:

[
  {
    "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"
  },
  {
    "name": "Findologic T-Shirt",
    "image": "https://store.com/some/path/to/image_800x800.jpg",
    "url": "https://store.com/Findologic-T-Shirt/ad99a6257e1546f08dbe9886a48e4230"
  },
  {
    "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"
  }
]

Deal with variant-specific data

Variant data can be stored in Shopware properties or custom fields. Mostly variant-specific information can be found in Shopware properties, as those fields define the variant properties.

src/Struct/FindologicProduct.php

private const VARIANT_PROPERTY = 'color';
 
private function addVariants(): void
{
    $variantData = [];
 
    $variants = $this->getAllVariants();
    foreach ($variants as $variant) {
        foreach ($variant->getProperties() as $variantProperty) {
            // Ignore all properties except the property we want to export our variants off.
            if (!$property->getGroup() || $property->getGroup()->getTranslation('name') !== self::VARIANT_PROPERTY) {
                continue;
            }
 
            $propertyName = $property->getTranslation('name');
 
            $variantData[$propertyName] = [
                // ...
                self::VARIANT_PROPERTY => $propertyName
            ];
        }
    }
 
    $this->properties[] = new Property('variants', ['' => json_encode($variantData)]);
}

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"
  }
}

Variant prices

In case variants have different prices, those can be exported within the variants JSON as well.

src/Struct/FindologicProduct.php

use Shopware\Core\Content\Product\ProductEntity;
 
private function addVariants(): void
{
    $variantData = [];
 
    $variants = $this->getAllVariants();
    foreach ($variants as $variant) {
        $variantData[] = [
            // ...
            'price' => $this->getVariantPrice($variant),
        ];
    }
 
    $this->properties[] = new Property('variants', ['' => json_encode($variantData)]);
}
 
private function getVariantPrice(ProductEntity $variant): ?float
{
    if (!$price = $variant->getPrice()) {
        return null;
    }
 
    if (!$currencyPrice = $price->getCurrencyPrice($this->salesChannelContext->getCurrency()->getId())) {
        return null;
    }
 
    return $currencyPrice->getGross();
}

The JSON would be like:

[
  {
    "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
  }
]

Usergroup-specific variants

In case there are different prices for customer groups, variants may be exported per usergroup.

src/Struct/FindologicProduct.php

use Shopware\Core\Content\Product\ProductEntity;
 
private function addVariants(): void
{
    $variantData = [];
 
    $variants = $this->getAllVariants();
    foreach ($variants as $variant) {
        $prices = $this->getUsergroupPrices($variant);
 
        foreach ($prices as $usergroup => $price) {
            $this->properties[] = new Property('variants', $usergroup => json_encode([
                // ...
                'price' => $price
            ]));
        }
    }
}
 
private function getUsergroupPrices(ProductEntity $variant): array
{
    $prices = [];
 
    foreach ($variant->getPrice() as $item) {
        foreach ($this->customerGroups as $customerGroup) {
            $userGroupHash = Utils::calculateUserGroupHash($this->shopkey, $customerGroup->getId());
            if (Utils::isEmpty($userGroupHash)) {
                continue;
            }
 
            $prices[$userGroupHash] = $customerGroup->getDisplayGross() ? $item->getGross() : $item->getNet();
        }
    }
}
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>
<!-- ... -->

Sometimes the plugin doesn't add the associations, but are 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. This example adds a media association for products and variants.

Decorate \FINDOLOGIC\FinSearch\Export\ProductService:

src/Export/ProductService.php

use FINDOLOGIC\FinSearch\Export\ProductService as OriginalProductService;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
 
class ProductService extends OriginalProductService
{
    protected function addProductAssociations(Criteria $criteria): void
    {
        parent::addProductAssociations($criteria);
 
        $criteria->addAssociation('children.properties.media');
        $criteria->addAssociation('properties.media');
    }
}

Add the decorated class to services.xml:

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\ProductService"
            decorates="FINDOLOGIC\FinSearch\Export\ProductService"
            public="true"
            decoration-on-invalid="ignore"
        >
            <argument type="service" id="service_container"/>
        </service>
 
        <!-- ... -->
 
    </services>
</container>

Inside of src/Struct/FindologicProduct.php it's possible to access ->getMedia(), in case the product has any media associated.

src/Struct/FindologicProduct.php

private function someCustomization(): void
{
    foreach ($product->getProperties() as $property) {
        // This now properly returns the associated media
        $media = $property->getMedia();
 
        // ...
    }
}

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.

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.