http://localhost:8000 - shopware
http://localhost:8000/admin - admin (admin/shopware)
http://localhost:8001 - adminer (mysql/app/app)



Go to folder when you installed Shopware, enter development folder. Build and start the containers and access the application containers.

./psh.phar docker:start
./psh.phar docker:ssh



Auto generating plugin

You can skip first three steps and auto generate plugin with

bin/console plugin:create MichalMachovicTesting
bin/console plugin:refresh
bin/console plugin:install --activate MichalMachovicTesting
bin/console plugin:refresh

Creating migration

bin/console database:create-migration -p MichalMachovicTesting

Database

 ./psh.phar mysql
 




Creating plugin manually

  1. Create plugin directory under <shopware root>/custom/plugins, for example <shopware root>/custom/plugins/SwagBundleExample. Swag is company/vendor, BundleExample is name of plugin.



  1. Create composer.json under your plugin directory

/custom/plugins/SwagBundleExample/composer.json

{
    "name": "swag/bundle-example",
    "description": "Bundle example",
    "version": "v1.0.0",
    "license": "MIT",
    "authors": [
        {
            "name": "shopware AG"
        }
    ],
    "type": "shopware-platform-plugin",
    "autoload": {
        "psr-4": {
            "Swag\\BundleExample\\": "src/"
        }
    },
    "extra": {
        "shopware-plugin-class": "Swag\\BundleExample\\BundleExample",
        "copyright": "(c) by shopware AG",
        "label": {
            "de-DE": "Beispiel für Shopware",
            "en-GB": "Example for Shopware"
        }
    }
}



  1. Create plugin base class

/custom/plugins/SwagBundleExample/src/BundleExample.php

<?php declare(strict_types=1);

namespace Swag\BundleExample;

use Shopware\Core\Framework\Plugin;

class BundleExample extends Plugin
{
}



  1. Install the plugin

./bin/console plugin:refresh
./bin/console plugin:install --activate --clearCache BundleExample



Clear cache

This should work in 99% cases
bin/console cache:clear



Hard clean cache
./psh.phar cache



Rebuild storefront - after css changes

./psh.phar storefront:dev && ./psh.phar storefront:build && ./psh.phar cache



Rebuild admin

./psh.phar administration:build



Plugin folder structure

Plugins live under custom\plugins folder. Migration folder is generated with bin/console database:create-migration -p MichalMachovicTesting.

Core\Content\Testing - we want to use our own DB table, so we need to specify Collection, Definition and Entity.
Migration - here is SQL query, which will create our own table
Resources - config files and views

MichalMachovicTesting
|
|--src
|    |
|    |-Core
|    |    |
|    |     Content
|    |           |
|    |           Testing
|    |                 |
|    |                 |--TestingCollection.php
|    |                 |--TestingDefinition.php
|    |                 |--TestingEntity.php
|    |
|    |--Migration
|    |          |
|    |          --Migration<TIMESTAP>Testing.php
|    |
|    |--Resources
|    |         |
|    |         |--config
|    |         |       |
|    |         |       |--config.xml
|    |         |       |--routes.xml
|    |         |       |--services.xml
|    |         |
|    |         |--views
|    |                |
|    |                |--page
|    |                      |
|    |                      product-detail
|    |                                   |
|    |                                   |--index.html.twig
|    |
|    |--Storefront
|    |           |
|    |           |--Controller
|    |                       |
|    |                       |--A2cController.php
|    |
|    |--Testing.php
|
|--composer.json



MichalMachovicTesting/composer.json

{
    "name": "michalmachovic/testing",
    "description": "Testing",
    "version": "v1.0.0",
    "license": "MIT",
    "authors": [
        {
            "name": "Michal Machovic"
        }
    ],
    "type": "shopware-platform-plugin",
    "autoload": {
        "psr-4": {
            "MichalMachovic\\Testing\\": "src/"
        }
    },
    "extra": {
        "shopware-plugin-class": "MichalMachovic\\Testing\\Testing",
        "copyright": "(c) by Michal Machovic",
        "label": {
            "de-DE": "Testing",
            "en-GB": "Testing"
        }
    }
}



MichalMachovicTesting/src/Testing.php

<?php declare(strict_types=1);

namespace MichalMachovic\Testing;

use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
use Shopware\Core\Framework\CustomField\CustomFieldTypes;
use Shopware\Core\Framework\Uuid\Uuid;

class Testing extends Plugin
{
    //what will happen if plugin is installed    
    public function install(InstallContext $context): void
    {
        //this example will set two new custom fields on product
        $customFieldSetRepository = $this->container->get('custom_field_set.repository');
        $id = Uuid::randomHex();
        $attributeSet = [
            'id' => $id,
            'name' => 'MichalMachovic Testing',
            'config' => ['description' => 'MichalMachovic Testing'],
            'customFields' => [
                [
                    'id' => Uuid::randomHex(),
                    'name' => 'testing_defaultAppUrl',
                    'type' => CustomFieldTypes::TEXT,
                    'config' => [
                        'label' => [
                            'de-DE' => 'Default App Url',
                            'en-GB' => 'Default App Url'
                        ],
                        'componentName' => "sw-field",
                        'customFieldType' => "text",
                        'customFieldPosition' => 1
                    ]
                ],
                [
                    'id' => Uuid::randomHex(),
                    'name' => 'testing_mobileAppUrl',
                    'type' => CustomFieldTypes::TEXT,
                    'config' => [
                        'label' => [
                            'de-DE' => 'Mobile App Url',
                            'en-GB' => 'Mobile App Url'
                        ],
                        'componentName' => "sw-field",
                        'customFieldType' => "text",
                        'customFieldPosition' => 2
                    ]
                ],
            ],
            'relations' => [
                [
                    'entityName' => 'product',
                ]
            ],
        ];
        $result = $customFieldSetRepository->create([$attributeSet], $context->getContext());
    }

    public function uninstall(UninstallContext $context): void
    {
    }



MichalMachovicTesting/src/Migration/Migration(TIMESTAMP).php

<?php declare(strict_types=1);

namespace MichalMachovic\Testing\Migration;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\InheritanceUpdaterTrait;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1554708925Testing extends MigrationStep
{
    use InheritanceUpdaterTrait;

    public function getCreationTimestamp(): int
    {
        return 1554708925;
    }

    public function update(Connection $connection): void
    {
      $connection->executeQuery('
      CREATE TABLE IF NOT EXISTS `michalmachovic_testing` (
        `id` BINARY(16) NOT NULL,
        `buy_request` LONGTEXT NOT NULL,
        `created_at` DATETIME(3) NOT NULL,
        `updated_at` DATETIME(3) NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  ');
  //$this->updateInheritance($connection, 'product', 'personaliseit');
    }

    public function updateDestructive(Connection $connection): void
    {
    }
}



MichalMachovicTesting/src/Core/Content/Testing/TestingEntity.php

<?php declare(strict_types=1);

namespace MichalMachovic\Testing\Core\Content\Testing;

use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityIdTrait;

class TestingEntity extends Entity
{
    use EntityIdTrait;

    /**
     * @var string
     */
    protected $buyRequest;

  
    public function getBuyRequest(): string
    {
        return $this->defaultAppUrl;
    }

    public function setBuyRequest(string $buyRequest): void
    {
        $this->buyRequest = $buyRequest;
    }
}



MichalMachovicTesting/src/Core/Content/Testing/TestingDefinition.php

<?php declare(strict_types=1);

namespace MichalMachovic\Testing\Core\Content\Testing;

use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FloatField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\LongTextField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;

class TestingDefinition extends EntityDefinition
{
    public const ENTITY_NAME = 'michalmachovic_testing';

    public function getEntityName(): string
    {
        return self::ENTITY_NAME;
    }

    public function getCollectionClass(): string
    {
        return TestingCollection::class;
    }

    public function getEntityClass():string
    {
        return TestingEntity::class;
    }

    protected function defineFields(): FieldCollection
    {
        return new FieldCollection([
            (new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey()),
            (new LongTextField('buy_request', 'buyRequest'))->addFlags(new Required())
        ]);
    }
}



MichalMachovicTesting/src/Core/Content/Testing/TestingCollection.php

<?php declare(strict_types=1);

namespace MichalMachovic\Testing\Core\Content\Testing;

use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;

/**
 * @method void              add(TestingEntity $entity)
 * @method void              set(string $key, TestingEntity $entity)
 * @method TestingEntity[]    getIterator()
 * @method TestingEntity[]    getElements()
 * @method TestingEntity|null get(string $key)
 * @method TestingEntity|null first()
 * @method TestingEntity|null last()
 */
class TestingCollection extends EntityCollection
{
    protected function getExpectedClass(): string
    {
        return TestingEntity::class;
    }
}



MichalMachovicTesting/src/Storefront/Controller/A2cController.php

<?php declare(strict_types=1);

namespace MichalMachovic\Testing\Storefront\Controller;

use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Order\SalesChannel\OrderService;
use Shopware\Storefront\Page\GenericPageLoader;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Storefront\Controller\StorefrontController;
use Symfony\Component\Routing\Annotation\Route;
use Shopware\Core\Content\Product\Cart\ProductLineItemFactory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;

/**
 * @RouteScope(scopes={"storefront"})
 */
class A2cController extends StorefrontController
{   

    /**
     *  @var EntityRepositoryInterface
     */
    private $testingRepository;
  
    private $cartService;
    private $orderService;
    private $genericPageLoader;
   
    public function __construct(
        CartService $cartService, 
        OrderService $orderService, 
        GenericPageLoader $genericPageLoader, 
        EntityRepositoryInterface $personaliseItRepository)
    {
        $this->cartService = $cartService;
        $this->orderService = $orderService;
        $this->genericPageLoader = $genericPageLoader;
        $this->personaliseItRepository = $personaliseItRepository;
    }



    /**
     * @Route("/testing/a2c/{id}", name="frontend.testing.a2c", options={"seo"="false"}, methods={"POST"})
     */
    public function a2c(SalesChannelContext $salesChannelContext, Context $context, Request $request, string $id)
    {   

        //data are coming from form with post 
        $data = json_decode($request->get('data'));
        
        //update item in cart
        if ($id) {
            $cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
            $product = (new ProductLineItemFactory())->create($id, ['quantity' => 1]);
            $this->cartService->add($cart, $product, $salesChannelContext);

            $lastItem = $cart->getLineItems()->last();
            $lastItem->setPayloadValue('testing', json_encode($data));
            $this->cartService->setCart($cart, $salesChannelContext);
        }
        

        //insert data into own DB table
        $insert = [
            'id' => Uuid::randomHex(),
            'buyRequest' => json_encode($data)
        ];
        $this->testingRepository->create(array($insert), $context);
                          

        return new JsonResponse(array(
            'redirect' => 'http://localhost:8000/checkout/cart',
        ));
    }

}



MichalMachovicTesting/src/Resources/config/config.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/master/src/Core/System/SystemConfig/Schema/config.xsd">

    <card>
        <title>Minimal configuration</title>
        <title lang="de-DE">Minimale Konfiguration</title>
        <input-field>
            <name>example</name>
            <label>Example Label EN</label>
            <label lang="de-DE">Beispiel Label DE</label>
        </input-field>
    </card>
</config>



MichalMachovicTesting/src/Resources/config/routes.xml

<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
        https://symfony.com/schema/routing/routing-1.0.xsd">

    <import resource="../../**/Storefront/Controller/*Controller.php" type="annotation" />
</routes>



MichalMachovicTesting/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="MichalMachovic\Testing\Core\Content\Testing\TestingItDefinition">
            <tag name="shopware.entity.definition" entity="michalmachovic_testing" />
        </service>

        <service id="MichalMachovic\Testing\Storefront\Controller\A2cController" public="true">
            <argument type="service" id="Shopware\Core\Checkout\Cart\SalesChannel\CartService"/>
            <argument type="service" id="Shopware\Core\Checkout\Order\SalesChannel\OrderService"/>
            <argument type="service" id="Shopware\Storefront\Page\GenericPageLoader"/>
            <argument type="service" id="michalmachovic_testing.repository" /> 
              
            <call method="setContainer">
                <argument type="service" id="service_container"/>
            </call>
        </service>
    </services>
</container>



MichalMachovicTesting/src/Resources/views/page/product-detail/index.html.twig

{% sw_extends '@Storefront/base.html.twig' %}

{% block base_head %}
    {% sw_include '@Storefront/page/product-detail/meta.html.twig' %}
{% endblock %}

{% block base_content %}

.....
<script>
let a2c = 'http://localhost:8000/testing/a2c/';

var xhr = new XMLHttpRequest();
xhr.widthCredentials = true;
                                                
xhr.onreadystatechange = function() {
    if(xhr.readyState == 4) {
        if(xhr.status == 200) {
            var data = JSON.parse(xhr.responseText);
            window.top.location = data.redirect;
        } else {
            try {
                var data = JSON.parse(xhr.responseText);
                alert(data.error.message);
            } catch(e) {
                alert("An unknown error ocurred");
            }
        }
    }
};

xhr.open("POST", a2c);
                            
var fd = new FormData();
fd.append('data', JSON.stringify(body));
xhr.send(fd);
break;
</script>
.....