Shopware6: Plugin development
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
- Create plugin directory under
<shopware root>/custom/plugins
, for example<shopware root>/custom/plugins/SwagBundleExample
.Swag
is company/vendor,BundleExample
is name of plugin.
- 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"
}
}
}
- 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
{
}
- 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>
.....