diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4506332be..5fb39b3cb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog This is the official changelog index of Shopware 6. Here you find a registry of all Shopware 6 releases with a reference to the detailed changelog of each version. If you want to know more about how the changelog is created have a look [here](/adr/2020-08-03-Implement-New-Changelog.md). +## 6.4.4.1 +* [NEXT-15675 - Improve file download](/changelog/release-6-4-4-1/2021-07-29-improve-file-download.md) +* [NEXT-17058 - Bugfix autowiring type+name for already defined repositories](/changelog/release-6-4-4-1/2021-09-06-bugfix-autowiring-type-name-for-already-defined-repositories.md) ([mynameisbogdan](https://github.com/mynameisbogdan)) +* [NEXT-17059 - Fixed Theme Inheritance](/changelog/release-6-4-4-1/2021-09-08-fixed-theme-inheritance.md) +* [NEXT-17072 - Fixed PriceFieldSerializer percentage calculation](/changelog/release-6-4-4-1/2021-09-09-fixed-price-field-serializer-percentage-calculation.md) +* [NEXT-17170 - Add missing context to CartVerifyPersistEvent](/changelog/release-6-4-4-1/2021-09-10-add-missing-context-to-cartverifypersistevent.md) +* [NEXT-17105 - Fixed EntitySearcher for PrimaryKeys other than `id`](/changelog/release-6-4-4-1/2021-09-10-fixed-entity-reader-for-non-id-pks.md) + ## 6.4.4.0 * [NEXT-16588 - Add support for .well-known/change-password](/changelog/release-6-4-4-0/2020-11-22-add-well-known-change-password.md) ([Joshua Behrens](https://github.com/JoshuaBehrens)) * [NEXT-16516 - Sidebar flash on cookie accept all](/changelog/release-6-4-4-0/2021-04-08-sidebar-flash-on-cookie-accept-all.md) ([Rune Laenen](https://github.com/runelaenen)) @@ -131,6 +139,10 @@ This is the official changelog index of Shopware 6. Here you find a registry of * [NEXT-16770 - Fix JSON structure of App CMS blocks](/changelog/release-6-4-4-0/2021-08-25-fix-json-structure-of-app-cms-blocks.md) * [NEXT-16968 - Pin symfony/translation to 5.3.4](./changelog/release-6-4-4-0/2021-08-30-pin-symfony-translation-to-5-3-4.md) +## 6.4.3.1 +* [NEXT-15675 - Improve file download](/changelog/release-6-4-3-1/2021-07-29-improve-file-download.md) +* [NEXT-15671 - Add `system.plugin_upload` privilege](./changelog/release-6-4-3-1/2021-07-30-add-plugin-upload-privilege.md) + ## 6.4.3.0 * [NEXT-14114 - Add the new field is tax-free from to table currency and country.](/changelog/release-6-4-3-0/2021-03-10-add-new-field-tax-free-from-to-table-currency-and-country.md) * [NEXT-14118 - Add VAT id required to each country setting](/changelog/release-6-4-3-0/2021-03-21-add-vat-id-required-to-each-country-setting.md) diff --git a/UPGRADE-6.4.md b/UPGRADE-6.4.md index 1e0013dfcef..26de81f1816 100644 --- a/UPGRADE-6.4.md +++ b/UPGRADE-6.4.md @@ -1,6 +1,33 @@ UPGRADE FROM 6.3.x.x to 6.4 ======================= +# 6.4.4.1 +## Deprecating reading entities with the storage name of the primary key fields + +When you added a custom entity definition with a combined primary key you need to pass the field names when you want to read specific entities. +The use of storage names when reading entities is deprecated by now, please use the property names instead. +The support of reading entities with the storage name of the primary keys will be removed in 6.5.0.0. + +### Before +```php +new Criteria([ + [ + 'storage_name_of_first_pk' => 1, + 'storage_name_of_second_pk' => 2, + ], +]); +``` + +### Now +```php +new Criteria([ + [ + 'propertyNameOfFirstPk' => 1, + 'propertyNameOfSecondPk' => 2, + ], +]); +``` + # 6.4.4.0 ## Added support for building administration without database diff --git a/changelog/_unreleased/2021-07-29-improve-file-download.md b/changelog/release-6-4-3-1/2021-07-29-improve-file-download.md similarity index 100% rename from changelog/_unreleased/2021-07-29-improve-file-download.md rename to changelog/release-6-4-3-1/2021-07-29-improve-file-download.md diff --git a/changelog/release-6-4-4-1/2021-09-06-bugfix-autowiring-type-name-for-already-defined-repositories.md b/changelog/release-6-4-4-1/2021-09-06-bugfix-autowiring-type-name-for-already-defined-repositories.md new file mode 100644 index 00000000000..271c72df85d --- /dev/null +++ b/changelog/release-6-4-4-1/2021-09-06-bugfix-autowiring-type-name-for-already-defined-repositories.md @@ -0,0 +1,9 @@ +--- +title: Bugfix autowiring type+name for already defined repositories +issue: NEXT-17058 +author: mynameisbogdan +author_email: mynameisbogdan@protonmail.com +author_github: mynameisbogdan +--- +# Core +* Changed `Shopware\Core\Framework\DependencyInjection\CompilerPass\EntityCompilerPass` to add `registerAliasForArgument` for already defined repositories and move duplicated calls after try-catch. diff --git a/changelog/release-6-4-4-1/2021-09-08-fixed-theme-inheritance.md b/changelog/release-6-4-4-1/2021-09-08-fixed-theme-inheritance.md new file mode 100644 index 00000000000..88ea3e19a57 --- /dev/null +++ b/changelog/release-6-4-4-1/2021-09-08-fixed-theme-inheritance.md @@ -0,0 +1,6 @@ +--- +title: Fixed Theme Inheritance +issue: NEXT-17059 +--- +# Storefront +* Changed `ThemeService::mergeStaticConfig` to read correct config even on themes without own configFields. diff --git a/changelog/release-6-4-4-1/2021-09-09-fixed-price-field-serializer-percentage-calculation.md b/changelog/release-6-4-4-1/2021-09-09-fixed-price-field-serializer-percentage-calculation.md new file mode 100644 index 00000000000..d7b8b1cfaea --- /dev/null +++ b/changelog/release-6-4-4-1/2021-09-09-fixed-price-field-serializer-percentage-calculation.md @@ -0,0 +1,6 @@ +--- +title: Fixed PriceFieldSerializer percentage calculation +issue: NEXT-17072 +--- +# Core +* Changed `\Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\PriceFieldSerializer` to not divide by zero when calculating the percentage and allowing to decode data in the old format. diff --git a/changelog/release-6-4-4-1/2021-09-10-add-missing-context-to-cartverifypersistevent.md b/changelog/release-6-4-4-1/2021-09-10-add-missing-context-to-cartverifypersistevent.md new file mode 100644 index 00000000000..563216fabaa --- /dev/null +++ b/changelog/release-6-4-4-1/2021-09-10-add-missing-context-to-cartverifypersistevent.md @@ -0,0 +1,8 @@ +--- +title: Add missing context to CartVerifyPersistEvent +issue: NEXT-17170 +author_github: @Dominik28111 +--- +# Core +* Added SalesChannelConext to `Shopware\Core\Checkout\Cart\Event\CartVerifyPersistEvent::__construct()`. +* Added method `Shopware\Core\Checkout\Cart\Event\CartVerifyPersistEvent::setShouldPersist()`. diff --git a/changelog/release-6-4-4-1/2021-09-10-fixed-entity-reader-for-non-id-pks.md b/changelog/release-6-4-4-1/2021-09-10-fixed-entity-reader-for-non-id-pks.md new file mode 100644 index 00000000000..341a971e20c --- /dev/null +++ b/changelog/release-6-4-4-1/2021-09-10-fixed-entity-reader-for-non-id-pks.md @@ -0,0 +1,35 @@ +--- +title: Fixed EntitySearcher for PrimaryKeys other than `id` +issue: NEXT-17105 +--- +# Core +* Changed `\Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper` to fix problem when entities have primary keys, where the storage name and the property name differs. +* Deprecated reading entities with the storage name of the primary key, use the property name instead. +___ +# Upgrade Information + +## Deprecating reading entities with the storage name of the primary key fields + +When you added a custom entity definition with a combined primary key you need to pass the field names when you want to read specific entities. +The use of storage names when reading entities is deprecated by now, please use the property names instead. +The support of reading entities with the storage name of the primary keys will be removed in 6.5.0.0. + +### Before +```php +new Criteria([ + [ + 'storage_name_of_first_pk' => 1, + 'storage_name_of_second_pk' => 2, + ], +]); +``` + +### Now +```php +new Criteria([ + [ + 'propertyNameOfFirstPk' => 1, + 'propertyNameOfSecondPk' => 2, + ], +]); +``` diff --git a/src/Core/Checkout/Cart/CartPersister.php b/src/Core/Checkout/Cart/CartPersister.php index 8aebe9ebe5d..f5592554371 100644 --- a/src/Core/Checkout/Cart/CartPersister.php +++ b/src/Core/Checkout/Cart/CartPersister.php @@ -69,7 +69,7 @@ public function save(Cart $cart, SalesChannelContext $context): void || $cart->getCustomerComment() !== null || $cart->getExtension(DeliveryProcessor::MANUAL_SHIPPING_COSTS) instanceof CalculatedPrice; - $event = new CartVerifyPersistEvent($cart, $shouldPersist); + $event = new CartVerifyPersistEvent($context, $cart, $shouldPersist); $this->eventDispatcher->dispatch($event); diff --git a/src/Core/Checkout/Cart/Event/CartVerifyPersistEvent.php b/src/Core/Checkout/Cart/Event/CartVerifyPersistEvent.php index a0544e97102..20efe1c14cb 100644 --- a/src/Core/Checkout/Cart/Event/CartVerifyPersistEvent.php +++ b/src/Core/Checkout/Cart/Event/CartVerifyPersistEvent.php @@ -16,8 +16,9 @@ class CartVerifyPersistEvent extends Event implements ShopwareSalesChannelEvent protected bool $shouldPersist; - public function __construct(Cart $cart, bool $shouldPersist) + public function __construct(SalesChannelContext $context, Cart $cart, bool $shouldPersist) { + $this->context = $context; $this->cart = $cart; $this->shouldPersist = $shouldPersist; } @@ -41,4 +42,9 @@ public function shouldBePersisted(): bool { return $this->shouldPersist; } + + public function setShouldPersist(bool $persist): void + { + $this->shouldPersist = $persist; + } } diff --git a/src/Core/Checkout/Test/Cart/CartPersisterTest.php b/src/Core/Checkout/Test/Cart/CartPersisterTest.php index 1894ba49e2a..34e03e5ce35 100644 --- a/src/Core/Checkout/Test/Cart/CartPersisterTest.php +++ b/src/Core/Checkout/Test/Cart/CartPersisterTest.php @@ -16,8 +16,11 @@ use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection; use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection; use Shopware\Core\Checkout\Test\Cart\Common\Generator; +use Shopware\Core\Defaults; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; use Shopware\Core\Framework\Uuid\Uuid; +use Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory; +use Shopware\Core\System\SalesChannel\SalesChannelContext; use Symfony\Component\EventDispatcher\EventDispatcher; class CartPersisterTest extends TestCase @@ -232,4 +235,46 @@ public function testCartVerifyPersistEventIsFiredAndPersisted(): void static::assertTrue($caughtEvent->shouldBePersisted()); static::assertCount(1, $caughtEvent->getCart()->getLineItems()); } + + public function testCartVerifyPersistEventIsFiredAndModified(): void + { + $connection = $this->createMock(Connection::class); + $eventDispatcher = new EventDispatcher(); + + $caughtEvent = null; + $handler = static function (CartVerifyPersistEvent $event) use (&$caughtEvent): void { + $caughtEvent = $event; + $event->setShouldPersist(false); + }; + $eventDispatcher->addListener(CartVerifyPersistEvent::class, $handler); + + $persister = new CartPersister($connection, $eventDispatcher); + + $cart = new Cart('shopware', 'existing'); + $cart->addLineItems(new LineItemCollection([ + new LineItem(Uuid::randomHex(), LineItem::PROMOTION_LINE_ITEM_TYPE, Uuid::randomHex(), 1), + ])); + + $connection->expects(static::once()) + ->method('delete') + ->with('`cart`', ['token' => $cart->getToken()]); + + $persister->save( + $cart, + $this->getSalesChannelContext($cart->getToken()) + ); + + static::assertInstanceOf(CartVerifyPersistEvent::class, $caughtEvent); + static::assertFalse($caughtEvent->shouldBePersisted()); + static::assertCount(1, $caughtEvent->getCart()->getLineItems()); + + $eventDispatcher->removeListener(CartVerifyPersistEvent::class, $handler); + } + + private function getSalesChannelContext(string $token): SalesChannelContext + { + return $this->getContainer() + ->get(SalesChannelContextFactory::class) + ->create($token, Defaults::SALES_CHANNEL); + } } diff --git a/src/Core/Framework/DataAbstractionLayer/Dbal/EntityDefinitionQueryHelper.php b/src/Core/Framework/DataAbstractionLayer/Dbal/EntityDefinitionQueryHelper.php index b46fec7dd80..cc20268d285 100644 --- a/src/Core/Framework/DataAbstractionLayer/Dbal/EntityDefinitionQueryHelper.php +++ b/src/Core/Framework/DataAbstractionLayer/Dbal/EntityDefinitionQueryHelper.php @@ -559,8 +559,23 @@ private function addIdConditionWithOr(Criteria $criteria, EntityDefinition $defi $where = []; - foreach ($primaryKey as $storageName => $value) { - $field = $definition->getFields()->getByStorageName($storageName); + foreach ($primaryKey as $propertyName => $value) { + $field = $definition->getFields()->get($propertyName); + + /* + * @deprecated tag:v6.5.0 - with 6.5.0 the only passing the propertyName will be supported + */ + if (!$field) { + $field = $definition->getFields()->getByStorageName($propertyName); + } + + if (!$field) { + throw new UnmappedFieldException($propertyName, $definition); + } + + if (!$field instanceof StorageAware) { + throw new \RuntimeException('Only storage aware fields are supported in read condition'); + } if ($field instanceof IdField || $field instanceof FkField) { $value = Uuid::fromHexToBytes($value); @@ -568,11 +583,17 @@ private function addIdConditionWithOr(Criteria $criteria, EntityDefinition $defi $key = 'pk' . Uuid::randomHex(); - $accessor = EntityDefinitionQueryHelper::escape($definition->getEntityName()) . '.' . EntityDefinitionQueryHelper::escape($storageName); + $accessor = EntityDefinitionQueryHelper::escape($definition->getEntityName()) . '.' . EntityDefinitionQueryHelper::escape($field->getStorageName()); - $where[] = $accessor . ' = :' . $key; + /* + * @deprecated tag:v6.5.0 - check for duplication in accessors will be removed, + * when we only support propertyNames to be used in search and when IdSearchResult only returns the propertyNames + */ + if (!\array_key_exists($accessor, $where)) { + $where[$accessor] = $accessor . ' = :' . $key; - $query->setParameter($key, $value); + $query->setParameter($key, $value); + } } $wheres[] = '(' . implode(' AND ', $where) . ')'; diff --git a/src/Core/Framework/DataAbstractionLayer/FieldSerializer/PriceFieldSerializer.php b/src/Core/Framework/DataAbstractionLayer/FieldSerializer/PriceFieldSerializer.php index bd62069bb3c..3129ea444e5 100644 --- a/src/Core/Framework/DataAbstractionLayer/FieldSerializer/PriceFieldSerializer.php +++ b/src/Core/Framework/DataAbstractionLayer/FieldSerializer/PriceFieldSerializer.php @@ -73,15 +73,23 @@ public function encode( $price['gross'] = (float) $price['gross']; $price['net'] = (float) $price['net']; - if (isset($price['listPrice']) && isset($price['listPrice']['gross'])) { + if (isset($price['listPrice'])) { + $price['percentage'] = null; + } + + if (($price['listPrice']['net'] ?? 0) > 0 || ($price['listPrice']['gross'] ?? 0) > 0) { $price['percentage'] = [ - 'net' => round(100 - $price['net'] / $price['listPrice']['net'] * 100, 2), - 'gross' => round(100 - $price['gross'] / $price['listPrice']['gross'] * 100, 2), + 'net' => 0.0, + 'gross' => 0.0, ]; - } - if (\array_key_exists('listPrice', $price) && $price['listPrice'] === null) { - $price['percentage'] = null; + if (($price['listPrice']['net'] ?? 0) > 0) { + $price['percentage']['net'] = round(100 - $price['net'] / $price['listPrice']['net'] * 100, 2); + } + + if (($price['listPrice']['gross'] ?? 0) > 0) { + $price['percentage']['gross'] = round(100 - $price['gross'] / $price['listPrice']['gross'] * 100, 2); + } } $converted['c' . $price['currencyId']] = $price; @@ -137,7 +145,7 @@ public function decode(Field $field, /*?string */$value)/*: ?PriceCollection*/ (float) $data['gross'], (bool) $data['linked'], ), - $row['percentage'] + $row['percentage'] ?? null ) ); } diff --git a/src/Core/Framework/DependencyInjection/CompilerPass/EntityCompilerPass.php b/src/Core/Framework/DependencyInjection/CompilerPass/EntityCompilerPass.php index fc3ec87d0ca..1f9500b48e5 100644 --- a/src/Core/Framework/DependencyInjection/CompilerPass/EntityCompilerPass.php +++ b/src/Core/Framework/DependencyInjection/CompilerPass/EntityCompilerPass.php @@ -58,7 +58,6 @@ private function collectDefinitions(ContainerBuilder $container): void $repository = $container->getDefinition($repositoryId); //@deprecated tag:v6.5.0 (flag:FEATURE_NEXT_16155) - remove add method call $repository->addMethodCall('setEntityLoadedEventFactory', [new Reference(EntityLoadedEventFactory::class)]); - $repository->setPublic(true); } catch (ServiceNotFoundException $exception) { $repository = new Definition( EntityRepository::class, @@ -72,11 +71,11 @@ private function collectDefinitions(ContainerBuilder $container): void new Reference(EntityLoadedEventFactory::class), ] ); - $repository->setPublic(true); - $container->setDefinition($repositoryId, $repository); - $container->registerAliasForArgument($repositoryId, EntityRepositoryInterface::class); } + $repository->setPublic(true); + $container->registerAliasForArgument($repositoryId, EntityRepositoryInterface::class); + $repositoryNameMap[$entity] = $repositoryId; } diff --git a/src/Core/Framework/Test/DataAbstractionLayer/Field/TestDefinition/NonIdPrimaryKeyTestDefinition.php b/src/Core/Framework/Test/DataAbstractionLayer/Field/TestDefinition/NonIdPrimaryKeyTestDefinition.php new file mode 100644 index 00000000000..7661e1c2b5e --- /dev/null +++ b/src/Core/Framework/Test/DataAbstractionLayer/Field/TestDefinition/NonIdPrimaryKeyTestDefinition.php @@ -0,0 +1,34 @@ +addFlags(new ApiAware(), new PrimaryKey()), + + new StringField('name', 'name'), + ]); + } +} diff --git a/src/Core/Framework/Test/DataAbstractionLayer/FieldSerializer/PriceFieldSerializerTest.php b/src/Core/Framework/Test/DataAbstractionLayer/FieldSerializer/PriceFieldSerializerTest.php index a6ff390cccc..a1a3fed6472 100644 --- a/src/Core/Framework/Test/DataAbstractionLayer/FieldSerializer/PriceFieldSerializerTest.php +++ b/src/Core/Framework/Test/DataAbstractionLayer/FieldSerializer/PriceFieldSerializerTest.php @@ -21,10 +21,7 @@ class PriceFieldSerializerTest extends TestCase { use KernelTestBehaviour; - /** - * @var PriceFieldSerializer - */ - protected $serializer; + protected PriceFieldSerializer $serializer; public function setUp(): void { @@ -80,7 +77,7 @@ public function testSerializeStringsFloat(): void static::assertSame('{"cb7d2554b0ce847cd82f3ac9bd1c0dfca":{"net":5.5,"gross":5.5,"currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true}}', $data); } - public function testEncoindingWithMultiplePrices(): void + public function testEncodingWithMultiplePrices(): void { $data = $this->encode([ [ @@ -177,6 +174,86 @@ public function testSerializeWithListPrice(): void static::assertSame($json, $data); } + public function testSerializeWithZeroNetListPrice(): void + { + $data = $this->encode([ + Defaults::CURRENCY => [ + 'net' => '5', + 'gross' => '5', + 'currencyId' => Defaults::CURRENCY, + 'linked' => true, + 'listPrice' => [ + 'net' => '0', + 'gross' => '10', + 'currencyId' => Defaults::CURRENCY, + 'linked' => true, + ], + ], + ]); + + $json = '{"cb7d2554b0ce847cd82f3ac9bd1c0dfca":{"net":5.0,"gross":5.0,"currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true,"listPrice":{"net":"0","gross":"10","currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true},"percentage":{"net":0.0,"gross":50.0}}}'; + static::assertSame($json, $data); + } + + public function testSerializeWithZeroGrossListPrice(): void + { + $data = $this->encode([ + Defaults::CURRENCY => [ + 'net' => '5', + 'gross' => '5', + 'currencyId' => Defaults::CURRENCY, + 'linked' => true, + 'listPrice' => [ + 'net' => '10', + 'gross' => '0', + 'currencyId' => Defaults::CURRENCY, + 'linked' => true, + ], + ], + ]); + + $json = '{"cb7d2554b0ce847cd82f3ac9bd1c0dfca":{"net":5.0,"gross":5.0,"currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true,"listPrice":{"net":"10","gross":"0","currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true},"percentage":{"net":50.0,"gross":0.0}}}'; + static::assertSame($json, $data); + } + + public function testSerializeWithZeroListPrice(): void + { + $data = $this->encode([ + Defaults::CURRENCY => [ + 'net' => '5', + 'gross' => '5', + 'currencyId' => Defaults::CURRENCY, + 'linked' => true, + 'listPrice' => [ + 'net' => '0', + 'gross' => '0', + 'currencyId' => Defaults::CURRENCY, + 'linked' => true, + ], + ], + ]); + + $json = '{"cb7d2554b0ce847cd82f3ac9bd1c0dfca":{"net":5.0,"gross":5.0,"currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true,"listPrice":{"net":"0","gross":"0","currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true},"percentage":null}}'; + static::assertSame($json, $data); + } + + public function testDecodeIsBackwardCompatible(): void + { + $json = '{"cb7d2554b0ce847cd82f3ac9bd1c0dfca":{"net":5.0,"gross":5.0,"currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true,"listPrice":{"net":"10","gross":"10","currencyId":"b7d2554b0ce847cd82f3ac9bd1c0dfca","linked":true}}}'; + + $field = new PriceField('test', 'test'); + + $decoded = $this->serializer->decode($field, $json); + + $price = $decoded->get(Defaults::CURRENCY); + + static::assertSame(5.0, $price->getNet()); + static::assertSame(5.0, $price->getGross()); + static::assertSame(10.0, $price->getListPrice()->getNet()); + static::assertSame(10.0, $price->getListPrice()->getGross()); + static::assertNull($price->getPercentage()); + } + private function encode(array $data): string { $field = new PriceField('test', 'test'); diff --git a/src/Core/Framework/Test/DataAbstractionLayer/Reader/EntityReaderTest.php b/src/Core/Framework/Test/DataAbstractionLayer/Reader/EntityReaderTest.php index a10b79aaf0e..fd3f09cad5f 100644 --- a/src/Core/Framework/Test/DataAbstractionLayer/Reader/EntityReaderTest.php +++ b/src/Core/Framework/Test/DataAbstractionLayer/Reader/EntityReaderTest.php @@ -28,6 +28,8 @@ use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting; +use Shopware\Core\Framework\Test\DataAbstractionLayer\Field\DataAbstractionLayerFieldTestBehaviour; +use Shopware\Core\Framework\Test\DataAbstractionLayer\Field\TestDefinition\NonIdPrimaryKeyTestDefinition; use Shopware\Core\Framework\Test\IdsCollection; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; use Shopware\Core\Framework\Uuid\Uuid; @@ -37,36 +39,19 @@ class EntityReaderTest extends TestCase { use IntegrationTestBehaviour; + use DataAbstractionLayerFieldTestBehaviour; - /** - * @var Connection - */ - private $connection; + private Connection $connection; - /** - * @var EntityRepositoryInterface - */ - private $productRepository; + private EntityRepositoryInterface $productRepository; - /** - * @var EntityRepositoryInterface - */ - private $categoryRepository; + private EntityRepositoryInterface $categoryRepository; - /** - * @var EntityRepository - */ - private $languageRepository; + private EntityRepository $languageRepository; - /** - * @var EntityRepositoryInterface - */ - private $taxRepository; + private EntityRepositoryInterface $taxRepository; - /** - * @var string - */ - private $deLanguageId; + private string $deLanguageId; protected function setUp(): void { @@ -75,6 +60,33 @@ protected function setUp(): void $this->categoryRepository = $this->getContainer()->get('category.repository'); $this->languageRepository = $this->getContainer()->get('language.repository'); $this->deLanguageId = $this->getDeDeLanguageId(); + + $this->registerDefinition(NonIdPrimaryKeyTestDefinition::class); + + $this->connection->rollBack(); + + $this->connection->executeUpdate(' + DROP TABLE IF EXISTS `non_id_primary_key_test`; + CREATE TABLE `non_id_primary_key_test` ( + `test_field` BINARY(16) NOT NULL, + `name` VARCHAR(255) NULL, + `created_at` DATETIME(3) NOT NULL, + `updated_at` DATETIME(3) NULL, + PRIMARY KEY (`test_field`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + '); + + $this->connection->beginTransaction(); + } + + protected function tearDown(): void + { + $this->connection->rollBack(); + + $this->connection->executeUpdate('DROP TABLE `non_id_primary_key_test`'); + $this->connection->beginTransaction(); + + parent::tearDown(); } public function testTranslated(): void @@ -1891,4 +1903,85 @@ public function testLoadToOneWithToMany(): void static::assertTrue($translations->count() >= 2); } } + + public function testSearchWithNonIdPK(): void + { + $id1 = Uuid::randomHex(); + $id2 = Uuid::randomHex(); + + $data = [ + [ + 'testField' => $id1, + 'name' => 'test1', + ], + [ + 'testField' => $id2, + 'name' => 'test2', + ], + ]; + + /** @var EntityRepositoryInterface $repository */ + $repository = $this->getContainer()->get('non_id_primary_key_test.repository'); + + $repository->create($data, Context::createDefaultContext()); + + $result = $repository->search(new Criteria(), Context::createDefaultContext()); + + static::assertEquals(2, $result->getTotal()); + } + + public function testReadWithNonIdPKOverPropertyName(): void + { + $id1 = Uuid::randomHex(); + $id2 = Uuid::randomHex(); + + $data = [ + [ + 'testField' => $id1, + 'name' => 'test1', + ], + [ + 'testField' => $id2, + 'name' => 'test2', + ], + ]; + + /** @var EntityRepositoryInterface $repository */ + $repository = $this->getContainer()->get('non_id_primary_key_test.repository'); + + $repository->create($data, Context::createDefaultContext()); + + $result = $repository->search(new Criteria([['testField' => $id1]]), Context::createDefaultContext()); + + static::assertEquals(1, $result->getTotal()); + } + + /** + * @deprecated tag: v6.5.0 - Can be safely removed when we remove support for reading of storage + */ + public function testReadWithNonIdPKOverStorageName(): void + { + $id1 = Uuid::randomHex(); + $id2 = Uuid::randomHex(); + + $data = [ + [ + 'testField' => $id1, + 'name' => 'test1', + ], + [ + 'testField' => $id2, + 'name' => 'test2', + ], + ]; + + /** @var EntityRepositoryInterface $repository */ + $repository = $this->getContainer()->get('non_id_primary_key_test.repository'); + + $repository->create($data, Context::createDefaultContext()); + + $result = $repository->search(new Criteria([['test_field' => $id1]]), Context::createDefaultContext()); + + static::assertEquals(1, $result->getTotal()); + } } diff --git a/src/Core/Framework/Test/DependencyInjection/CompilerPass/EntityCompilerPassTest.php b/src/Core/Framework/Test/DependencyInjection/CompilerPass/EntityCompilerPassTest.php index acff71edc94..9ac48df54f1 100644 --- a/src/Core/Framework/Test/DependencyInjection/CompilerPass/EntityCompilerPassTest.php +++ b/src/Core/Framework/Test/DependencyInjection/CompilerPass/EntityCompilerPassTest.php @@ -5,7 +5,9 @@ use PHPUnit\Framework\TestCase; use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressDefinition; use Shopware\Core\Checkout\Customer\CustomerDefinition; +use Shopware\Core\Content\Product\ProductDefinition; use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry; +use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DependencyInjection\CompilerPass\EntityCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -39,4 +41,35 @@ public function testEntityRepositoryAutowiring(): void static::assertNotNull($container->getAlias('Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $customerRepository')); static::assertNotNull($container->getAlias('Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $customerAddressRepository')); } + + public function testEntityRepositoryAutowiringForAlreadyDefinedRepositories(): void + { + $container = new ContainerBuilder(); + + $container + ->register(ProductDefinition::class, ProductDefinition::class) + ->addTag('shopware.entity.definition') + ; + + $container + ->register(DefinitionInstanceRegistry::class, DefinitionInstanceRegistry::class) + ->addArgument(new Reference('service_container')) + ->addArgument([ + ProductDefinition::ENTITY_NAME => ProductDefinition::class, + ]) + ->addArgument([ + ProductDefinition::ENTITY_NAME => 'product.repository', + ]) + ; + + $container + ->register('product.repository', EntityRepository::class) + ->addArgument(new Reference(ProductDefinition::class)) + ; + + $entityCompilerPass = new EntityCompilerPass(); + $entityCompilerPass->process($container); + + static::assertTrue($container->hasAlias('Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $productRepository')); + } } diff --git a/src/Core/Migration/Test/Migration1627929168UpdatePriceFieldInProductTableTest.php b/src/Core/Migration/Test/Migration1627929168UpdatePriceFieldInProductTableTest.php index 00bed2e95fb..23566d822b9 100644 --- a/src/Core/Migration/Test/Migration1627929168UpdatePriceFieldInProductTableTest.php +++ b/src/Core/Migration/Test/Migration1627929168UpdatePriceFieldInProductTableTest.php @@ -20,11 +20,43 @@ class Migration1627929168UpdatePriceFieldInProductTableTest extends TestCase use KernelTestBehaviour; use DatabaseTransactionBehaviour; + private string $previousSqlMode; + + public function setUp(): void + { + parent::setUp(); + + $con = $this->getContainer()->get(Connection::class); + + $this->previousSqlMode = $con->fetchOne('SELECT @@sql_mode'); + + $current = array_filter(explode(',', $this->previousSqlMode)); + + if (!\in_array('STRICT_ALL_TABLES', $current, true)) { + $current[] = 'STRICT_ALL_TABLES'; + } + + if (!\in_array('ERROR_FOR_DIVISION_BY_ZERO', $current, true)) { + $current[] = 'ERROR_FOR_DIVISION_BY_ZERO'; + } + + $con->executeStatement(sprintf('SET @@session.sql_mode = "%s"', implode(',', $current))); + } + + public function tearDown(): void + { + parent::tearDown(); + $con = $this->getContainer()->get(Connection::class); + + $con->executeStatement(sprintf('SET @@session.sql_mode = "%s"', $this->previousSqlMode)); + } + /** * @dataProvider dataProvider */ public function testUpdatePriceColumn(array $price, ?array $percentageResult): void { + $this->getContainer()->get(Connection::class)->executeStatement('SET @@session.sql_mode = "STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"'); $productId = $this->createProduct($price); $migration = new Migration1627929168UpdatePriceFieldInProductTable(); @@ -130,6 +162,44 @@ public function dataProvider(): array $currencyId => ['net' => 50, 'gross' => 50], ], ], + 'Product with list price with 0 in net' => [ + [ + Defaults::CURRENCY => [ + 'gross' => 5, + 'net' => 5, + 'linked' => true, + 'currencyId' => Defaults::CURRENCY, + 'listPrice' => [ + 'gross' => 5, + 'net' => 0, + 'linked' => false, + 'currencyId' => Defaults::CURRENCY, + ], + ], + ], + [ + Defaults::CURRENCY => ['net' => 0, 'gross' => 0], + ], + ], + 'Product with list price with 0 in gross' => [ + [ + Defaults::CURRENCY => [ + 'gross' => 5, + 'net' => 5, + 'linked' => true, + 'currencyId' => Defaults::CURRENCY, + 'listPrice' => [ + 'gross' => 5, + 'net' => 0, + 'linked' => false, + 'currencyId' => Defaults::CURRENCY, + ], + ], + ], + [ + Defaults::CURRENCY => ['net' => 0, 'gross' => 0], + ], + ], ]; } @@ -156,6 +226,9 @@ private function createProduct(array $price = []): string $this->getContainer()->get('product.repository')->create([$product], Context::createDefaultContext()); + // The ProductFieldSerializer writes the percentage for us. This is bad for testing this migration. Remove it here again + $this->getContainer()->get(Connection::class)->executeStatement('UPDATE product SET price = JSON_REMOVE(price, "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.percentage")'); + return $id; } diff --git a/src/Core/Migration/V6_4/Migration1627929168UpdatePriceFieldInProductTable.php b/src/Core/Migration/V6_4/Migration1627929168UpdatePriceFieldInProductTable.php index c3160d97db3..c1eb19186b5 100644 --- a/src/Core/Migration/V6_4/Migration1627929168UpdatePriceFieldInProductTable.php +++ b/src/Core/Migration/V6_4/Migration1627929168UpdatePriceFieldInProductTable.php @@ -3,7 +3,6 @@ namespace Shopware\Core\Migration\V6_4; use Doctrine\DBAL\Connection; -use Shopware\Core\Defaults; use Shopware\Core\Framework\Migration\MigrationStep; class Migration1627929168UpdatePriceFieldInProductTable extends MigrationStep @@ -15,33 +14,47 @@ public function getCreationTimestamp(): int public function update(Connection $connection): void { - $connection->executeUpdate( + $connection->executeStatement( 'UPDATE product SET price = JSON_SET( price, - CONCAT("$.c", :currencyId, ".percentage"), + "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.percentage", JSON_OBJECT( "net", - COALESCE(ROUND(( - 100 - JSON_UNQUOTE(JSON_EXTRACT( - price, CONCAT("$.c", :currencyId, ".net") - )) / JSON_UNQUOTE(JSON_EXTRACT(price, CONCAT("$.c", :currencyId, ".listPrice.net"))) * 100 - ),2), 0), + COALESCE( + ROUND( + ( + IF( + IFNULL(JSON_UNQUOTE(JSON_EXTRACT(price, "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.listPrice.net")), 0) = 0, 0, + 100 - JSON_UNQUOTE(JSON_EXTRACT(price, CONCAT("$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.net"))) / + JSON_UNQUOTE(JSON_EXTRACT(price, "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.listPrice.net")) * 100 + ) + ), + 2 + ), + 0 + ), "gross", - COALESCE(ROUND(( - 100 - JSON_UNQUOTE(JSON_EXTRACT( - price, CONCAT("$.c", :currencyId, ".gross") - )) / JSON_UNQUOTE(JSON_EXTRACT(price, CONCAT("$.c", :currencyId, ".listPrice.gross"))) * 100 - ),2), 0) - ) + COALESCE( + ROUND( + ( + IF ( + IFNULL(JSON_UNQUOTE(JSON_EXTRACT(price, "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.listPrice.gross")), 0) = 0, + 0, + 100 - JSON_UNQUOTE(JSON_EXTRACT(price, CONCAT("$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.gross"))) / + JSON_UNQUOTE(JSON_EXTRACT(price, "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.listPrice.gross")) * 100 + ) + ) + ,2 + ), + 0) + ) ) - WHERE JSON_UNQUOTE(JSON_EXTRACT(price, CONCAT("$.c", :currencyId, ".listPrice"))) IS NOT NULL', - ['currencyId' => Defaults::CURRENCY] + WHERE JSON_UNQUOTE(JSON_EXTRACT(price, "$.cb7d2554b0ce847cd82f3ac9bd1c0dfca.listPrice")) IS NOT NULL' ); } public function updateDestructive(Connection $connection): void { - // implement update destructive } } diff --git a/src/Storefront/Test/Theme/ThemeTest.php b/src/Storefront/Test/Theme/ThemeTest.php index 64c39581694..39b98ae4a4a 100644 --- a/src/Storefront/Test/Theme/ThemeTest.php +++ b/src/Storefront/Test/Theme/ThemeTest.php @@ -163,6 +163,48 @@ public function testInheritedThemeConfig(): void static::assertEquals($themeInheritedConfig, $theme); } + /** + * Check if a Theme without fieldconfigs will also be updateable + */ + public function testInheritedBlankThemeConfig(): void + { + $criteria = new Criteria(); + $criteria->addFilter(new EqualsFilter('technicalName', StorefrontPluginRegistry::BASE_THEME_NAME)); + + /** @var ThemeEntity $baseTheme */ + $baseTheme = $this->themeRepository->search($criteria, $this->context)->first(); + + $name = $this->createBlankTheme($baseTheme); + + $criteria = new Criteria(); + $criteria->addFilter(new EqualsFilter('name', $name)); + + /** @var ThemeEntity $childTheme */ + $childTheme = $this->themeRepository->search($criteria, $this->context)->first(); + + $this->themeService->updateTheme( + $childTheme->getId(), + [ + 'sw-color-brand-primary' => [ + 'value' => '#ff00ff', + ], + ], + null, + $this->context + ); + + $theme = $this->themeService->getThemeConfiguration($childTheme->getId(), false, $this->context); + $themeInheritedConfig = ThemeFixtures::getThemeInheritedConfig(); + + foreach ($themeInheritedConfig['fields'] as $key => $field) { + if ($field['type'] === 'media') { + $themeInheritedConfig['fields'][$key]['value'] = $theme['fields'][$key]['value']; + } + } + + static::assertEquals($themeInheritedConfig, $theme); + } + public function testInheritedSecondLevelThemeConfig(): void { $criteria = new Criteria(); @@ -564,6 +606,31 @@ private function createTheme(ThemeEntity $parentTheme, array $customConfig = []) return $name; } + private function createBlankTheme(ThemeEntity $parentTheme): string + { + $name = 'test' . Uuid::randomHex(); + + $id = Uuid::randomHex(); + $this->themeRepository->create( + [ + [ + 'id' => $id, + 'parentThemeId' => $parentTheme->getId(), + 'name' => $name, + 'technicalName' => $name, + 'createdAt' => (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT), + 'description' => $parentTheme->getDescription(), + 'author' => $parentTheme->getAuthor(), + 'labels' => $parentTheme->getLabels(), + 'active' => true, + ], + ], + $this->context + ); + + return $name; + } + /** * @throws \Exception */ diff --git a/src/Storefront/Theme/ThemeService.php b/src/Storefront/Theme/ThemeService.php index ecaf78ea0b1..f0fa0e354cf 100644 --- a/src/Storefront/Theme/ThemeService.php +++ b/src/Storefront/Theme/ThemeService.php @@ -264,7 +264,7 @@ private function mergeStaticConfig(ThemeEntity $theme): array if ($theme->getConfigValues() !== null) { foreach ($theme->getConfigValues() as $fieldName => $configValue) { - if (isset($configuredTheme['fields']) && isset($configValue['value'])) { + if (isset($configValue['value'])) { $configuredTheme['fields'][$fieldName]['value'] = $configValue['value']; } }