Merge branch '1.13' into 2.0

* 1.13:
  [Api][Attribute] Add types enum to product attribute schema
  [Api][Attribute] Minor cleanup
  [API] Add contract tests for creating and validating attributes types
  [API] Add example configurations for all product attributes
  [API] Cover the impossibility to edit the code and type of an existing product attribute
  [API] Add possibility for optional message to ProductAttributeCannotBeRemoved exception
  [Behat] Minor improvements in product attributes scenarios and implementation
  [Api][Attribute] Switch storage type setting from persister to event subscriber
  [Api][Admin] Sorting product attributes
  [Api][Admin] Validating product attributes
  [Api][Admin] Updating product attributes
  [Api][Admin] Creating product attributes
  [Api][Admin] Cover product attributes removing
  [Api][Admin] Cover product attributes index
This commit is contained in:
Grzegorz Sadowski 2023-09-27 07:07:02 +02:00
commit 5231f241a7
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
73 changed files with 2797 additions and 105 deletions

View file

@ -8,7 +8,7 @@ Feature: Adding a new checkbox product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new checkbox product attribute
When I want to create a new checkbox product attribute
And I specify its code as "t_shirt_with_cotton"
@ -17,7 +17,7 @@ Feature: Adding a new checkbox product attribute
Then I should be notified that it has been successfully created
And the checkbox attribute "T-Shirt with cotton" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding a checkbox product attribute
When I want to create a new checkbox product attribute
Then the type field should be disabled

View file

@ -8,7 +8,7 @@ Feature: Adding a new float product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new float product attribute
When I want to create a new float product attribute
And I specify its code as "display_size"
@ -17,7 +17,7 @@ Feature: Adding a new float product attribute
Then I should be notified that it has been successfully created
And the float attribute "Display Size" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding a float product attribute
When I want to create a new float product attribute
Then the type field should be disabled

View file

@ -8,7 +8,7 @@ Feature: Adding a new integer product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new integer product attribute
When I want to create a new integer product attribute
And I specify its code as "book_pages"
@ -17,7 +17,7 @@ Feature: Adding a new integer product attribute
Then I should be notified that it has been successfully created
And the integer attribute "Book pages" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding a integer product attribute
When I want to create a new integer product attribute
Then the type field should be disabled

View file

@ -8,7 +8,7 @@ Feature: Adding a new non-translatable product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new non-translatable product attribute
When I want to create a new integer product attribute
And I specify its code as "damage"

View file

@ -8,7 +8,7 @@ Feature: Adding a new percent product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new percent product attribute
When I want to create a new percent product attribute
And I specify its code as "t_shirt_cotton_content"
@ -17,7 +17,7 @@ Feature: Adding a new percent product attribute
Then I should be notified that it has been successfully created
And the percent attribute "T-Shirt cotton content" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding a percent product attribute
When I want to create a new percent product attribute
Then the type field should be disabled

View file

@ -8,7 +8,7 @@ Feature: Adding a new select product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui @javascript
@ui @javascript @api
Scenario: Adding a new select product attribute
When I want to create a new select product attribute
And I specify its code as "mug_material"
@ -18,7 +18,7 @@ Feature: Adding a new select product attribute
Then I should be notified that it has been successfully created
And the select attribute "Mug material" should appear in the store
@ui @javascript
@ui @javascript @api
Scenario: Adding multiple select product attribute
When I want to create a new select product attribute
And I specify its code as "mug_material"
@ -30,7 +30,7 @@ Feature: Adding a new select product attribute
Then I should be notified that it has been successfully created
And the select attribute "Mug material" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding a select product attribute
When I want to create a new select product attribute
Then the type field should be disabled

View file

@ -8,7 +8,7 @@ Feature: Adding a new text product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new text product attribute
When I want to create a new text product attribute
And I specify its code as "t_shirt_brand"
@ -17,7 +17,7 @@ Feature: Adding a new text product attribute
Then I should be notified that it has been successfully created
And the text attribute "T-Shirt brand" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding text a product attribute
When I want to create a new text product attribute
Then the type field should be disabled

View file

@ -8,7 +8,7 @@ Feature: Adding a new textarea product attribute
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Adding a new textarea product attribute
When I want to create a new textarea product attribute
And I specify its code as "t_shirt_details"
@ -17,7 +17,7 @@ Feature: Adding a new textarea product attribute
Then I should be notified that it has been successfully created
And the textarea attribute "T-Shirt details" should appear in the store
@ui
@ui @no-api
Scenario: Seeing disabled type field while adding a textarea product attribute
When I want to create a new textarea product attribute
Then the type field should be disabled

View file

@ -10,7 +10,7 @@ Feature: Browsing product attributes
And the store has a integer product attribute "Book pages" with code "book_pages"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Browsing all product attributes in store
When I want to see all product attributes in store
Then I should see 3 product attributes in the list

View file

@ -10,7 +10,7 @@ Feature: Deleting multiple product attributes
And the store has a integer product attribute "Pages"
And I am logged in as an administrator
@ui @javascript
@ui @javascript @no-api
Scenario: Deleting multiple product attributes at once
When I browse product attributes
And I check the "Publisher" product attribute

View file

@ -1,5 +1,5 @@
@managing_product_attributes
Feature:Removing a attribute
Feature: Deleting a product attribute
In order to keep my collection of product attributes not cluttered
As an administrator
I want to be able to remove an attribute that is not assigned to any of the products
@ -8,14 +8,14 @@ Feature:Removing a attribute
Given I am logged in as an administrator
And the store has a product "44 Magnum"
@ui
Scenario: Try deleting a attribute from the registry when product use him
@ui @api
Scenario: Trying to delete an attribute from the registry when a product uses it
Given this product has a text attribute "Gun caliber" with value "11 mm"
When I delete this product attribute
Then I should be notified that it has been failed deleted "product attribute"
Then I should be notified that it is in use
@ui
Scenario: Deleting a text product attribute when not by used
@ui @api
Scenario: Deleting a text product attribute when it's not used
Given the store has a text product attribute "Gun caliber"
When I delete this product attribute
Then I should be notified that it has been successfully deleted

View file

@ -1,15 +0,0 @@
@managing_product_attributes
Feature: Deleting a text product attribute
In order to remove test, obsolete or incorrect text product attribute
As an Administrator
I want to be able to delete a text product attribute
Background:
Given I am logged in as an administrator
@ui
Scenario: Deleting a text product attribute from the registry
Given the store has a text product attribute "T-Shirt cotton brand"
When I delete this product attribute
Then I should be notified that it has been successfully deleted
And this product attribute should no longer exist in the registry

View file

@ -9,7 +9,7 @@ Feature: Editing a select product attribute
And the store has a select product attribute "T-Shirt material" with value "Banana skin"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Editing a select product attribute name
When I want to edit this product attribute
And I change its name to "T-Shirt material" in "English (United States)"
@ -17,7 +17,7 @@ Feature: Editing a select product attribute
Then I should be notified that it has been successfully edited
And the select attribute "T-Shirt material" should appear in the store
@ui
@ui @api
Scenario: Editing a select product attribute value
When I want to edit this product attribute
And I change its value "Banana skin" to "Orange skin"
@ -25,7 +25,7 @@ Feature: Editing a select product attribute
Then I should be notified that it has been successfully edited
And this product attribute should have value "Orange skin"
@ui @javascript
@ui @javascript @api
Scenario: Adding a new value to an existing select product attribute
When I want to edit this product attribute
And I add value "Orange skin" in "English (United States)"
@ -33,7 +33,7 @@ Feature: Editing a select product attribute
Then I should be notified that it has been successfully edited
And this product attribute should have value "Orange skin"
@ui @javascript
@ui @javascript @api
Scenario: Deleting a value from an existing select product attribute
When I want to edit this product attribute
And I delete value "Banana skin"
@ -41,12 +41,12 @@ Feature: Editing a select product attribute
Then I should be notified that it has been successfully edited
And this product attribute should not have value "Banana skin"
@ui
Scenario: Seeing disabled code field while editing a product attribute
@ui @api
Scenario: Being unable to change code of an existing product attribute
When I want to edit this product attribute
Then the code field should be disabled
Then I should not be able to edit its code
@ui
Scenario: Seeing disabled type field while editing a product attribute
@ui @api
Scenario: Being unable to change type of an existing product attribute
When I want to edit this product attribute
Then the type field should be disabled
Then I should not be able to edit its type

View file

@ -6,25 +6,23 @@ Feature: Editing a text product attribute
Background:
Given the store is available in "English (United States)"
And the store has a text product attribute "T-Shirt cotton brand" with code "t_shirt_brand"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Editing product attribute name
Given the store has a text product attribute "T-Shirt cotton brand"
When I want to edit this product attribute
And I change its name to "T-Shirt material" in "English (United States)"
And I save my changes
Then I should be notified that it has been successfully edited
And the text attribute "T-Shirt material" should appear in the store
@ui
Scenario: Seeing disabled code field while editing a product attribute
Given the store has a text product attribute "T-Shirt cotton brand" with code "t_shirt_brand"
@ui @api
Scenario: Being unable to change code of an existing product attribute
When I want to edit this product attribute
Then the code field should be disabled
Then I should not be able to edit its code
@ui
Scenario: Seeing disabled type field while editing a product attribute
Given the store has a text product attribute "T-Shirt cotton brand" with code "t_shirt_brand"
@ui @api
Scenario: Being unable to change type of an existing product attribute
When I want to edit this product attribute
Then the type field should be disabled
Then I should not be able to edit its type

View file

@ -8,7 +8,7 @@ Feature: Product attribute unique code validation
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Trying to add a new product attribute with taken code
Given the store has a text product attribute "T-Shirt cotton material" with code "t_shirt_material"
When I want to create a new text product attribute

View file

@ -8,7 +8,7 @@ Feature: Seeing correct select attribute values in different locale than default
Given the store is available in "French (France)"
And I am logged in as an administrator
@ui @javascript
@ui @javascript @api
Scenario: Seeing correct attribute values in different locale than default one
When I want to create a new select product attribute
And I specify its code as "mug_material"

View file

@ -8,7 +8,7 @@ Feature: Select product attribute validation
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui @javascript
@ui @javascript @api
Scenario: Trying to add a new select product attribute with wrong max entries value
When I want to create a new select product attribute
And I name it "Mug material" in "English (United States)"
@ -22,7 +22,7 @@ Feature: Select product attribute validation
Then I should be notified that max entries value must be greater or equal to the min entries value
And the attribute with code "mug_material" should not appear in the store
@ui @javascript
@ui @javascript @api
Scenario: Trying to add a new select product attribute with wrong min entries value
When I want to create a new select product attribute
And I name it "Mug material" in "English (United States)"
@ -36,7 +36,7 @@ Feature: Select product attribute validation
Then I should be notified that min entries value must be lower or equal to the number of added choices
And the attribute with code "mug_material" should not appear in the store
@ui @javascript
@ui @javascript @api
Scenario: Trying to add a new select product attribute with specified entries values but without multiple option
When I want to create a new select product attribute
And I name it "Mug material" in "English (United States)"

View file

@ -10,21 +10,21 @@ Feature: Sorting listed product attributes by position
And the store has a integer product attribute "Book pages" at position 0
And I am logged in as an administrator
@ui
@ui @api
Scenario: Product attributes are sorted by position in ascending order by default
When I want to see all product attributes in store
Then I should see 3 product attributes in the list
And the first product attribute on the list should have name "Book pages"
And the last product attribute on the list should have name "T-Shirt with cotton"
@ui
@ui @api
Scenario: Product attribute added at no position gets put at the bottom of the list
Given the store has also a text product attribute "Drive type"
When I want to see all product attributes in store
Then I should see 4 product attributes in the list
And the last product attribute on the list should have name "Drive type"
@ui
@ui @api
Scenario: Product attribute added at position 0 is added as the first one
Given the store has also a text product attribute "Drive type" at position 0
When I want to see all product attributes in store

View file

@ -8,7 +8,7 @@ Feature: Text product attribute validation
Given the store is available in "English (United States)"
And I am logged in as an administrator
@ui
@ui @api
Scenario: Trying to add a new text product attribute without name
When I want to create a new text product attribute
And I specify its code as "t_shirt_with_cotton"
@ -17,7 +17,7 @@ Feature: Text product attribute validation
Then I should be notified that name is required
And the attribute with code "t_shirt_with_cotton" should not appear in the store
@ui
@ui @api
Scenario: Trying to add a new text product attribute without code
When I want to create a new text product attribute
And I name it "T-Shirt brand" in "English (United States)"
@ -26,16 +26,16 @@ Feature: Text product attribute validation
Then I should be notified that code is required
And the attribute with name "T-Shirt brand" should not appear in the store
@ui
@ui @api
Scenario: Trying to remove name for existing text product attribute
Given the store has a text product attribute "T-Shirt cotton brand"
When I want to edit this product attribute
And I remove its name from "English (United States)" translation
And I try to save my changes
Then I should be notified that name is required
And the attribute with code "t_shirt_with_cotton" should not appear in the store
And the text attribute "T-Shirt cotton brand" should still be in the store
@ui
@ui @api
Scenario: Trying to add a new text product attribute with wrong configuration
When I want to create a new text product attribute
And I name it "T-Shirt brand" in "English (United States)"

View file

@ -770,6 +770,26 @@ parameters:
count: 1
path: src/Sylius/Bundle/ApiBundle/DataPersister/MessengerDataPersister.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:persist\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:remove\\(\\) has no return type specified\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:remove\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php
-
message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductTaxonDataPersister\\:\\:persist\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
count: 1

View file

@ -0,0 +1,465 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Behat\Context\Api\Admin;
use Behat\Behat\Context\Context;
use Ramsey\Uuid\Uuid;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Client\ResponseCheckerInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Component\Product\Model\ProductAttributeInterface;
use Webmozart\Assert\Assert;
final class ManagingProductAttributesContext implements Context
{
public function __construct(
private ApiClientInterface $client,
private ResponseCheckerInterface $responseChecker,
private SharedStorageInterface $sharedStorage,
) {
}
/**
* @When I want to see all product attributes in store
*/
public function iWantToBrowseProductAttributes(): void
{
$this->client->index(Resources::PRODUCT_ATTRIBUTES);
}
/**
* @When /^I(?:| try to) delete (this product attribute)$/
*/
public function iDeleteThisProductAttribute(ProductAttributeInterface $attribute): void
{
$this->client->delete(Resources::PRODUCT_ATTRIBUTES, $attribute->getCode());
}
/**
* @When I want to create a new :type product attribute
*/
public function iWantToCreateANewTypedProductAttribute(string $type): void
{
$this->client->buildCreateRequest(Resources::PRODUCT_ATTRIBUTES);
$this->client->addRequestData('type', $type);
}
/**
* @When I specify its code as :code
*/
public function iSpecifyItsCodeAs(string $code): void
{
$this->client->addRequestData('code', $code);
}
/**
* @When I name it :name in :localeCode
* @When I change its name to :name in :localeCode
* @When I do not name it
* @When I remove its name from :localeCode translation
*/
public function iNameItIn(string $name = '', string $localeCode = 'en_US'): void
{
$this->client->updateRequestData(['translations' => [$localeCode => ['name' => $name, 'locale' => $localeCode]]]);
}
/**
* @When I (also) add value :value in :localeCode
*/
public function iAddValueIn(string $value, string $localeCode): void
{
$uuid = Uuid::uuid4()->toString();
$this->client->addRequestData('configuration', ['choices' => [$uuid => [$localeCode => $value]]]);
}
/**
* @When I disable its translatability
*/
public function iDisableItsTranslatability(): void
{
$this->client->addRequestData('translatable', false);
}
/**
* @When I check multiple option
*/
public function iCheckMultipleOption(): void
{
$this->client->addRequestData('configuration', ['multiple' => true]);
}
/**
* @When I do not check multiple option
* @When I do not specify its code
*/
public function intentionallyBlank(): void
{
// Intentionally left blank
}
/**
* @When I specify its :limitType entries value as :count
* @When I specify its :limitType length as :count
*/
public function iSpecifyItsLimitTypeEntriesAs(string $limitType, int $count): void
{
$this->client->addRequestData('configuration', [$limitType => $count]);
}
/**
* @When /^I want to edit (this product attribute)$/
*/
public function iWantToEditThisProductAttribute(ProductAttributeInterface $productAttribute): void
{
$this->sharedStorage->set('product_attribute', $productAttribute);
$this->client->buildUpdateRequest(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode());
}
/**
* @When I add it
* @When I try to add it
*/
public function iAddIt(): void
{
$this->client->create();
}
/**
* @When /^I change (its) value "([^"]+)" to "([^"]+)"$/
*/
public function iChangeItsValueTo(
ProductAttributeInterface $productAttribute,
string $oldValue,
string $newValue,
): void {
$response = $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode());
$configuration = $this->responseChecker->getValue($response, 'configuration');
$choices = $configuration['choices'];
foreach ($choices as $key => $choice) {
if ($choice['en_US'] === $oldValue) {
$choices[$key]['en_US'] = $newValue;
break;
}
}
$this->client->updateRequestData(['configuration' => ['choices' => $choices]]);
}
/**
* @When I delete value :value
*/
public function iDeleteValue(string $value): void
{
/** @var ProductAttributeInterface $productAttribute */
$productAttribute = $this->sharedStorage->get('product_attribute');
$response = $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode());
$configuration = $this->responseChecker->getValue($response, 'configuration');
$choices = $configuration['choices'];
foreach ($choices as $key => $choice) {
if ($choice['en_US'] === $value) {
unset($choices[$key]);
break;
}
}
$this->client->setRequestData(['configuration' => ['choices' => $choices]]);
}
/**
* @Then I should see :count product attributes in the list
*/
public function iShouldSeeCountProductAttributesInTheList(int $count): void
{
Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count);
}
/**
* @Then the first product attribute on the list should have name :name
*/
public function theFirstProductAttributeOnTheListShouldHaveName(string $name): void
{
$first = $this->responseChecker->getCollection($this->client->getLastResponse())[0];
Assert::same($first['translations']['en_US']['name'], $name);
}
/**
* @Then the last product attribute on the list should have name :name
*/
public function theLastProductAttributeOnTheListShouldHaveName(string $name): void
{
$collection = $this->responseChecker->getCollection($this->client->getLastResponse());
$last = end($collection);
Assert::same($last['translations']['en_US']['name'], $name);
}
/**
* @Then I should see the product attribute :attributeName in the list
*/
public function iShouldSeeTheProductAttributeInTheList(string $attributeName): void
{
Assert::true($this->responseChecker->hasItemWithTranslation(
$this->client->getLastResponse(),
'en_US',
'name',
$attributeName,
));
}
/**
* @Then I should be notified that it has been successfully deleted
*/
public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void
{
$this->responseChecker->isDeletionSuccessful($this->client->getLastResponse());
}
/**
* @Then /^(this product attribute) should no longer exist in the registry$/
*/
public function thisProductAttributeShouldNoLongerExistInTheRegistry(ProductAttributeInterface $productAttribute): void
{
$response = $this->client->index(Resources::PRODUCT_ATTRIBUTES);
Assert::false(
$this->responseChecker->hasItemWithValue($response, 'code', $productAttribute->getCode()),
sprintf('Product attribute with code %s exists, but should not', $productAttribute->getCode()),
);
}
/**
* @Then I should be notified that it is in use
*/
public function iShouldBeNotifiedThatItIsInUse(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Cannot delete, the product attribute is in use.',
);
}
/**
* @Then I should be notified that it has been successfully created
*/
public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void
{
Assert::true(
$this->responseChecker->isCreationSuccessful($this->client->getLastResponse()),
'Product attribute could not be created',
);
}
/**
* @Then the :type attribute :name should appear in the store
* @Then the :type attribute :name should still be in the store
*/
public function theAttributeShouldAppearInTheStore(string $type, string $name): void
{
$response = $this->client->index(Resources::PRODUCT_ATTRIBUTES);
/** @var array<string, mixed> $item */
foreach ($this->responseChecker->getCollection($response) as $item) {
if ($item['type'] === $type && $item['translations']['en_US']['name'] === $name) {
return;
}
}
throw new \InvalidArgumentException(sprintf(
'Product attribute of type "%s" with name "%s" has not been found',
$type,
$name,
));
}
/**
* @Then the attribute with :field :value should not appear in the store
*/
public function theAttributeWithCodeShouldNotAppearInTheStore(string $field, string $value): void
{
$response = $this->client->index(Resources::PRODUCT_ATTRIBUTES);
Assert::false(
$this->responseChecker->hasItemWithValue($response, $field, $value),
sprintf('Product attribute with %s %s exists, but should not', $field, $value),
);
}
/**
* @Then I should see the value :value
*/
public function iShouldSeeTheValue(string $value): void
{
$content = $this->responseChecker->getResponseContent($this->client->getLastResponse());
$choices = $content['configuration']['choices'];
foreach ($choices as $values) {
if (in_array($value, $values)) {
return;
}
}
throw new \InvalidArgumentException(sprintf(
'Product attribute value "%s" has not been found in choices: %s',
$value,
json_encode($choices),
));
}
/**
* @Then /^(this product attribute) should have value "([^"]+)"$/
*/
public function thisProductAttributeShouldHaveValue(
ProductAttributeInterface $productAttribute,
string $value,
): void {
$this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode());
$this->iShouldSeeTheValue($value);
}
/**
* @Then /^(this product attribute) should not have value "([^"]+)"$/
*/
public function thisProductAttributeShouldNotHaveValue(
ProductAttributeInterface $productAttribute,
string $value,
): void {
$response = $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode());
$content = $this->responseChecker->getResponseContent($response);
$choices = $content['configuration']['choices'];
foreach ($choices as $values) {
if (in_array($value, $values)) {
throw new \InvalidArgumentException(sprintf(
'Product attribute value "%s" has been found but should not',
$value,
));
}
}
}
/**
* @Then I should be notified that :element is required
*/
public function iShouldBeNotifiedThatFieldIsRequired(string $field): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
sprintf('Please enter attribute %s.', $field),
);
}
/**
* @Then I should be notified that product attribute with this code already exists
*/
public function iShouldBeNotifiedThatProductAttributeWithThisCodeAlreadyExists(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'This code is already in use.',
);
}
/**
* @Then I should be notified that max length must be greater or equal to the min length
*/
public function iShouldBeNotifiedThatMaxLengthMustBeGreaterOrEqualToTheMinLength(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Configuration max length must be greater or equal to the min length.',
);
}
/**
* @Then there should still be only one product attribute with code :code
*/
public function thereShouldStillBeOnlyOneProductAttributeWithCode(string $code): void
{
$items = $this->responseChecker->getCollectionItemsWithValue(
$this->client->index(Resources::PRODUCT_ATTRIBUTES),
'code',
$code,
);
Assert::count($items, 1, sprintf('More than one attribute with code %s found', $code));
}
/**
* @Then I should be notified that max entries value must be greater or equal to the min entries value
*/
public function iShouldBeNotifiedThatMaxEntriesValueMustBeGreaterOrEqualToTheMinEntriesValue(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Configuration max entries value must be greater or equal to the min entries value.',
);
}
/**
* @Then I should be notified that min entries value must be lower or equal to the number of added choices
*/
public function iShouldBeNotifiedThatMinEntriesValueMustBeLowerOrEqualToTheNumberOfAddedChoices(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Configuration min entries value must be lower or equal to the number of added choices.',
);
}
/**
* @Then I should be notified that multiple must be true if min or max entries values are specified
*/
public function iShouldBeNotifiedThatMultipleMustBeTrueIfMinOrMaxEntriesValuesAreSpecified(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'Configuration multiple must be true if min or max entries values are specified.',
);
}
/**
* @Then I should not be able to edit its code
*/
public function iShouldNotBeAbleToEditItsCode(): void
{
$this->client->updateRequestData(['code' => 'NEW_CODE']);
Assert::false(
$this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE'),
'The code field with value NEW_CODE exist',
);
}
/**
* @Then I should not be able to edit its type
*/
public function iShouldNotBeAbleToEditItsType(): void
{
$this->client->updateRequestData(['type' => 'percent']);
Assert::false(
$this->responseChecker->hasValue($this->client->update(), 'type', 'percent'),
'The product attribute has new type select set.',
);
}
}

View file

@ -55,6 +55,8 @@ final class Resources
public const PRODUCT_ASSOCIATION_TYPES = 'product-association-types';
public const PRODUCT_ATTRIBUTES = 'product-attributes';
public const PRODUCT_OPTIONS = 'product-options';
public const PRODUCT_REVIEWS = 'product-reviews';

View file

@ -99,14 +99,6 @@ final class ManagingPaymentMethodsContext implements Context
$this->indexPage->deleteResourceOnPage(['code' => $paymentMethod->getCode(), 'name' => $paymentMethod->getName()]);
}
/**
* @Then I should be notified that it is in use
*/
public function iShouldBeNotifiedThatItIsInUse()
{
$this->notificationChecker->checkNotification('Cannot delete, the Payment method is in use.', NotificationType::failure());
}
/**
* @Then this payment method :element should be :value
*/

View file

@ -115,6 +115,7 @@ final class ManagingProductAttributesContext implements Context
/**
* @Then the :type attribute :name should appear in the store
* @Then the :type attribute :name should still be in the store
*/
public function theAttributeShouldAppearInTheStore($type, $name)
{
@ -152,17 +153,18 @@ final class ManagingProductAttributesContext implements Context
}
/**
* @Then the code field should be disabled
* @Then I should not be able to edit its code
*/
public function theCodeFieldShouldBeDisabled()
public function iShouldNotBeAbleToEditItsCode(): void
{
Assert::true($this->updatePage->isCodeDisabled());
}
/**
* @Then the type field should be disabled
* @Then I should not be able to edit its type
*/
public function theTypeFieldShouldBeDisabled()
public function theTypeFieldShouldBeDisabled(): void
{
/** @var CreatePageInterface|UpdatePageInterface $currentPage */
$currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]);
@ -328,7 +330,7 @@ final class ManagingProductAttributesContext implements Context
}
/**
* @When /^I delete (this product attribute)$/
* @When /^I(?:| try to) delete (this product attribute)$/
*/
public function iDeleteThisProductAttribute(ProductAttributeInterface $productAttribute)
{

View file

@ -448,14 +448,6 @@ final class ManagingShippingMethodsContext implements Context
Assert::false($this->indexPage->isSingleResourceOnPage(['code' => $shippingMethod->getCode()]));
}
/**
* @Then I should be notified that it is in use
*/
public function iShouldBeNotifiedThatItIsInUse()
{
$this->notificationChecker->checkNotification('Cannot delete, the Shipping method is in use.', NotificationType::failure());
}
/**
* @Then I should be notified that amount for :channel channel should not be blank
*/

View file

@ -76,14 +76,14 @@ final class NotificationContext implements Context
}
/**
* @Then I should be notified that it has been failed deleted :name
* @Then I should be notified that it is in use
*/
public function iShouldBeNotifiedThatItHasBeenFailedDeleted(string $name): void
public function iShouldBeNotifiedThatItIsInUse(): void
{
$this->testHelper->waitUntilNotificationPopups(
$this->notificationChecker,
NotificationType::failure(),
'Cannot delete, the ' . ucfirst($name) . ' is in use.',
'Cannot delete',
);
}
}

View file

@ -90,6 +90,12 @@
<argument type="service" id="sylius.behat.shared_storage" />
</service>
<service id="sylius.behat.context.api.admin.managing_product_attributes" class="Sylius\Behat\Context\Api\Admin\ManagingProductAttributesContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="sylius.behat.shared_storage" />
</service>
<service id="sylius.behat.context.api.admin.managing_product_options" class="Sylius\Behat\Context\Api\Admin\ManagingProductOptionsContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />

View file

@ -31,6 +31,7 @@ imports:
- suites/api/payment/managing_payments.yml
- suites/api/product/adding_product_review.yml
- suites/api/product/managing_product_association_types.yml
- suites/api/product/managing_product_attributes.yml
- suites/api/product/managing_product_options.yml
- suites/api/product/managing_product_reviews.yml
- suites/api/product/managing_product_variants.yml

View file

@ -0,0 +1,25 @@
# This file is part of the Sylius package.
# (c) Sylius Sp. z o.o.
default:
suites:
api_managing_product_attributes:
contexts:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.transform.locale
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.setup.admin_api_security
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.locale
- sylius.behat.context.setup.product
- sylius.behat.context.setup.product_attribute
- sylius.behat.context.api.admin.managing_product_attributes
- sylius.behat.context.api.admin.response
- sylius.behat.context.api.admin.save
filters:
tags: "@managing_product_attributes&&@api"
javascript: false

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Sylius\Bundle\ApiBundle\Exception\ProductAttributeCannotBeRemoved;
use Sylius\Component\Product\Model\ProductAttributeInterface;
/** @experimental */
final class ProductAttributeDataPersister implements ContextAwareDataPersisterInterface
{
public function __construct(
private ContextAwareDataPersisterInterface $decoratedDataPersister,
) {
}
public function supports($data, array $context = []): bool
{
return $data instanceof ProductAttributeInterface;
}
/** @param ProductAttributeInterface $data */
public function persist($data, array $context = [])
{
return $this->decoratedDataPersister->persist($data, $context);
}
public function remove($data, array $context = [])
{
try {
return $this->decoratedDataPersister->remove($data, $context);
} catch (ForeignKeyConstraintViolationException) {
throw new ProductAttributeCannotBeRemoved();
}
}
}

View file

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use Sylius\Component\Attribute\AttributeType\AttributeTypeInterface;
use Sylius\Component\Attribute\Model\AttributeInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/** @experimental */
final class AttributeEventSubscriber implements EventSubscriberInterface
{
public function __construct(private ServiceRegistryInterface $attributeTypeRegistry)
{
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['assignStorageType', EventPriorities::PRE_VALIDATE],
];
}
public function assignStorageType(ViewEvent $event): void
{
$attribute = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (
!$attribute instanceof AttributeInterface ||
!in_array($method, [Request::METHOD_POST, Request::METHOD_PUT], true)
) {
return;
}
if (null === $attribute->getType() || '' === $attribute->getType()) {
return;
}
if (
null !== $attribute->getStorageType() ||
!$this->attributeTypeRegistry->has($attribute->getType())
) {
return;
}
/** @var AttributeTypeInterface $attributeType */
$attributeType = $this->attributeTypeRegistry->get($attribute->getType());
$attribute->setStorageType($attributeType->getStorageType());
$event->setControllerResult($attribute);
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Exception;
/** @experimental */
final class ProductAttributeCannotBeRemoved extends \RuntimeException
{
public function __construct(
string $message = 'Cannot delete, the product attribute is in use.',
int $code = 0,
\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\OpenApi\Documentation;
use ApiPlatform\OpenApi\OpenApi;
use Sylius\Component\Attribute\AttributeType\AttributeTypeInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
/** @experimental */
final class AttributeTypeDocumentationModifier implements DocumentationModifierInterface
{
public function __construct(
private ServiceRegistryInterface $attributeTypeRegistry,
) {
}
public function modify(OpenApi $docs): OpenApi
{
$components = $docs->getComponents();
$schemas = $components->getSchemas();
if (null === $schemas) {
return $docs;
}
$schemas = $this->updateAttributeTypesSchema($schemas);
return $docs->withComponents(
$components->withSchemas($schemas),
);
}
/**
* @param \ArrayObject<string, mixed> $schemas
*
* @return \ArrayObject<string, mixed>
*/
private function updateAttributeTypesSchema(\ArrayObject $schemas): \ArrayObject
{
$attributeTypes = $this->getAttributeTypes();
$schemasToBeUpdated = [
'ProductAttribute.admin.product_attribute.read',
'ProductAttribute.admin.product_attribute.create',
'ProductAttribute.jsonld-admin.product_attribute.read',
'ProductAttribute.jsonld-admin.product_attribute.create',
];
foreach ($schemasToBeUpdated as $schemaToBeUpdated) {
$schemas[$schemaToBeUpdated]['properties']['type'] = [
'type' => 'string',
'enum' => $attributeTypes,
];
}
return $schemas;
}
/** @return array<string> */
private function getAttributeTypes(): array
{
$attributeTypes = [];
/** @var AttributeTypeInterface $attributeType */
foreach ($this->attributeTypeRegistry->all() as $attributeType) {
$attributeTypes[] = $attributeType->getType();
}
return $attributeTypes;
}
}

View file

@ -16,20 +16,184 @@
xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd"
>
<resource class="%sylius.model.product_attribute.class%" shortName="ProductAttribute">
<attribute name="normalization_context">
<attribute name="groups">
<attribute>shop:product_attribute:read</attribute>
</attribute>
</attribute>
<attribute name="validation_groups">sylius</attribute>
<collectionOperations />
<attribute name="order">
<attribute name="position">ASC</attribute>
</attribute>
<collectionOperations>
<collectionOperation name="admin_get">
<attribute name="method">GET</attribute>
<attribute name="path">/admin/product-attributes</attribute>
<attribute name="normalization_context">
<attribute name="groups">admin:product_attribute:read</attribute>
</attribute>
</collectionOperation>
<collectionOperation name="admin_post">
<attribute name="method">POST</attribute>
<attribute name="path">/admin/product-attributes</attribute>
<attribute name="denormalization_context">
<attribute name="groups">admin:product_attribute:create</attribute>
</attribute>
<attribute name="normalization_context">
<attribute name="groups">admin:product_attribute:read</attribute>
</attribute>
<attribute name="openapi_context">
<attribute name="description">
Example configuration for a `text` type attribute:
```
{
"type": "text",
"configuration": {
"min": 5,
"max": 255
}
}
```
Example configuration for a `datetime` type attribute:
```
{
"type": "datetime",
"configuration": {
"format": "Y-m-d H:i:s"
}
}
```
Example configuration for a `date` type attribute:
```
{
"type": "date",
"configuration": {
"format": "Y-m-d"
}
}
```
Example configuration for a `select` type attribute:
```
{
"type": "select",
"configuration": {
"choices": {
"0afb212e-cd08-11ec-871e-0242ac120005": {
"en_US": "Plastic",
"fr_FR": "Plastique"
},
"3bfb211f-cd08-11ec-871e-0242ac120005": {
"en_US": "Cotton",
"fr_FR": "Coton"
}
},
"multiple": true,
"min": 1,
"max": 3
}
}
```
</attribute>
</attribute>
</collectionOperation>
</collectionOperations>
<itemOperations>
<itemOperation name="admin_get">
<attribute name="method">GET</attribute>
<attribute name="path">/admin/product-attributes/{code}</attribute>
<attribute name="normalization_context">
<attribute name="groups">admin:product_attribute:read</attribute>
</attribute>
</itemOperation>
<itemOperation name="admin_delete">
<attribute name="method">DELETE</attribute>
<attribute name="path">/admin/product-attributes/{code}</attribute>
</itemOperation>
<itemOperation name="admin_put">
<attribute name="method">PUT</attribute>
<attribute name="path">/admin/product-attributes/{code}</attribute>
<attribute name="denormalization_context">
<attribute name="groups">admin:product_attribute:update</attribute>
</attribute>
<attribute name="normalization_context">
<attribute name="groups">admin:product_attribute:read</attribute>
</attribute>
<attribute name="openapi_context">
<attribute name="description">
Example configuration for a `text` type attribute:
```
{
"type": "text",
"configuration": {
"min": 5,
"max": 255
}
}
```
Example configuration for a `datetime` type attribute:
```
{
"type": "datetime",
"configuration": {
"format": "Y-m-d H:i:s"
}
}
```
Example configuration for a `date` type attribute:
```
{
"type": "date",
"configuration": {
"format": "Y-m-d"
}
}
```
Example configuration for a `select` type attribute:
```
{
"type": "select",
"configuration": {
"choices": {
"0afb212e-cd08-11ec-871e-0242ac120005": {
"en_US": "Plastic",
"fr_FR": "Plastique"
},
"3bfb211f-cd08-11ec-871e-0242ac120005": {
"en_US": "Cotton",
"fr_FR": "Coton"
}
},
"multiple": true,
"min": 1,
"max": 3
}
}
```
</attribute>
</attribute>
</itemOperation>
<itemOperation name="shop_get">
<attribute name="method">GET</attribute>
<attribute name="path">/shop/product-attributes/{code}</attribute>
<attribute name="normalization_context">
<attribute name="groups">shop:product_attribute:read</attribute>
</attribute>
</itemOperation>
</itemOperations>
@ -39,6 +203,18 @@
<property name="type" />
<property name="configuration" />
<property name="storageType" />
<property name="translatable" />
<property name="position" />
<property name="translations" readable="true" writable="true">
<attribute name="openapi_context">
<attribute name="type">object</attribute>
<attribute name="example">
<attribute name="en_US">
<attribute name="locale">string</attribute>
<attribute name="name">string</attribute>
</attribute>
</attribute>
</attribute>
</property>
</resource>
</resources>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" ?>
<!--
This file is part of the Sylius package.
(c) Sylius Sp. z o.o.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<resources xmlns="https://api-platform.com/schema/metadata"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd"
>
<resource class="%sylius.model.product_attribute_translation.class%" shortName="ProductAttributeTranslation">
<attribute name="validation_groups">sylius</attribute>
<collectionOperations />
<itemOperations>
<itemOperation name="admin_get">
<attribute name="method">GET</attribute>
<attribute name="path">/admin/product-attribute-translations/{id}</attribute>
<attribute name="normalization_context">
<attribute name="groups">
<attribute>admin:product_attribute:read</attribute>
<attribute>admin:product_attribute_translation:read</attribute>
</attribute>
</attribute>
</itemOperation>
</itemOperations>
<property name="id" identifier="true" writable="false" />
<property name="name" required="true" />
<property name="locale" required="true" />
</resource>
</resources>

View file

@ -52,6 +52,7 @@ api_platform:
Sylius\Bundle\ApiBundle\Exception\CannotRemoveCurrentlyLoggedInUser: 422
Sylius\Bundle\ApiBundle\Exception\OrderItemNotFoundException: 422
Sylius\Bundle\ApiBundle\Exception\OrderNoLongerEligibleForPromotion: 422
Sylius\Bundle\ApiBundle\Exception\ProductAttributeCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ProductCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422

View file

@ -38,6 +38,11 @@
<tag name="sylius.open_api.modifier" />
</service>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\AttributeTypeDocumentationModifier">
<argument type="service" id="sylius.registry.attribute_type" />
<tag name="sylius.open_api.modifier" />
</service>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductDocumentationModifier">
<tag name="sylius.open_api.modifier" />
</service>

View file

@ -24,12 +24,36 @@
</attribute>
<attribute name="code">
<group>shop:product_attribute:read</group>
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
</attribute>
<attribute name="type">
<group>shop:product_attribute:read</group>
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
</attribute>
<attribute name="storageType">
<group>admin:product_attribute:read</group>
</attribute>
<attribute name="configuration">
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
<group>admin:product_attribute:update</group>
</attribute>
<attribute name="translatable">
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
<group>admin:product_attribute:update</group>
</attribute>
<attribute name="position">
<group>shop:product_attribute:read</group>
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:update</group>
</attribute>
<attribute name="translations">
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
<group>admin:product_attribute:update</group>
</attribute>
</class>
</serializer>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" ?>
<!--
This file is part of the Sylius package.
(c) Sylius Sp. z o.o.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
>
<class name="Sylius\Component\Product\Model\ProductAttributeTranslation">
<attribute name="id">
<group>shop:product_attribute:read</group>
<group>admin:product_attribute:read</group>
</attribute>
<attribute name="locale">
<group>shop:product_attribute:read</group>
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
<group>admin:product_attribute:update</group>
</attribute>
<attribute name="name">
<group>shop:product_attribute:read</group>
<group>admin:product_attribute:read</group>
<group>admin:product_attribute:create</group>
<group>admin:product_attribute:update</group>
</attribute>
</class>
</serializer>

View file

@ -53,6 +53,11 @@
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\ProductAttributeDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\ProductTaxonDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<argument type="service" id="sylius.event_bus" />

View file

@ -54,5 +54,10 @@
<argument type="service" id="sylius.generator.taxon_slug" />
<tag name="kernel.event_subscriber" />
</service>
<service id="Sylius\Bundle\ApiBundle\EventSubscriber\AttributeEventSubscriber">
<argument type="service" id="sylius.registry.attribute_type" />
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>

View file

@ -217,5 +217,12 @@
</argument>
<tag name="api_platform.filter" />
</service>
<service id="sylius.api.product_attribute_order_filter" parent="api_platform.doctrine.orm.order_filter" public="true">
<argument type="collection">
<argument key="position" />
</argument>
<tag name="api_platform.filter" />
</service>
</services>
</container>

View file

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Exception\ProductAttributeCannotBeRemoved;
use Sylius\Component\Product\Model\ProductAttributeInterface;
final class ProductAttributeDataPersisterSpec extends ObjectBehavior
{
function let(ContextAwareDataPersisterInterface $persister): void
{
$this->beConstructedWith($persister);
}
function it_is_a_context_aware_persister(): void
{
$this->shouldImplement(ContextAwareDataPersisterInterface::class);
}
function it_supports_only_product_attribute(ProductAttributeInterface $productAttribute): void
{
$this->supports(new \stdClass())->shouldReturn(false);
$this->supports($productAttribute)->shouldReturn(true);
}
function it_uses_inner_persister_to_persist_product_attribute(
ContextAwareDataPersisterInterface $persister,
ProductAttributeInterface $productAttribute,
): void {
$persister->persist($productAttribute, [])->shouldBeCalled();
$this->persist($productAttribute, []);
}
function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal(
ContextAwareDataPersisterInterface $persister,
ProductAttributeInterface $productAttribute,
): void {
$persister
->remove($productAttribute, [])
->willThrow(ForeignKeyConstraintViolationException::class)
;
$this
->shouldThrow(ProductAttributeCannotBeRemoved::class)
->during('remove', [$productAttribute])
;
}
function it_uses_inner_persister_to_remove_product_attribute(
ContextAwareDataPersisterInterface $persister,
ProductAttributeInterface $productAttribute,
): void {
$persister->remove($productAttribute, [])->shouldBeCalled();
$this->remove($productAttribute, []);
}
}

View file

@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\EventSubscriber;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Attribute\AttributeType\AttributeTypeInterface;
use Sylius\Component\Attribute\Model\AttributeInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
final class AttributeEventSubscriberSpec extends ObjectBehavior
{
function let(ServiceRegistryInterface $registry): void
{
$this->beConstructedWith($registry);
}
function it_implements_event_subscriber_interface(): void
{
$this->shouldImplement(EventSubscriberInterface::class);
}
function it_does_nothing_when_controller_result_is_not_an_attribute(
ServiceRegistryInterface $registry,
HttpKernelInterface $kernel,
Request $request,
): void {
$request->getMethod()->shouldBeCalled();
$registry->has(Argument::any())->shouldNotBeCalled();
$this->assignStorageType(new ViewEvent(
$kernel->getWrappedObject(),
$request->getWrappedObject(),
HttpKernelInterface::MAIN_REQUEST,
new \stdClass(),
));
}
function it_does_nothing_when_attribute_has_no_type(
ServiceRegistryInterface $registry,
HttpKernelInterface $kernel,
Request $request,
AttributeInterface $attribute,
): void {
$request->getMethod()->willReturn(Request::METHOD_POST);
$attribute->getType()->willReturn(null);
$registry->has(Argument::any())->shouldNotBeCalled();
$this->assignStorageType(new ViewEvent(
$kernel->getWrappedObject(),
$request->getWrappedObject(),
HttpKernelInterface::MAIN_REQUEST,
$attribute->getWrappedObject(),
));
}
function it_does_nothing_when_attribute_has_a_storage_type(
ServiceRegistryInterface $registry,
HttpKernelInterface $kernel,
Request $request,
AttributeInterface $attribute,
): void {
$request->getMethod()->willReturn(Request::METHOD_POST);
$attribute->getType()->willReturn('text');
$attribute->getStorageType()->willReturn('text');
$registry->has(Argument::any())->shouldNotBeCalled();
$this->assignStorageType(new ViewEvent(
$kernel->getWrappedObject(),
$request->getWrappedObject(),
HttpKernelInterface::MAIN_REQUEST,
$attribute->getWrappedObject(),
));
}
function it_does_nothing_when_attribute_type_is_not_registered(
ServiceRegistryInterface $registry,
HttpKernelInterface $kernel,
Request $request,
AttributeInterface $attribute,
): void {
$request->getMethod()->willReturn(Request::METHOD_POST);
$attribute->getType()->willReturn('foo');
$attribute->getStorageType()->willReturn(null);
$registry->has('foo')->willReturn(false);
$this->assignStorageType(new ViewEvent(
$kernel->getWrappedObject(),
$request->getWrappedObject(),
HttpKernelInterface::MAIN_REQUEST,
$attribute->getWrappedObject(),
));
}
function it_sets_storage_type_based_on_set_attribute_type(
ServiceRegistryInterface $registry,
HttpKernelInterface $kernel,
Request $request,
AttributeInterface $attribute,
AttributeTypeInterface $attributeType,
): void {
$request->getMethod()->willReturn(Request::METHOD_POST);
$attribute->getType()->willReturn('foo');
$attribute->getStorageType()->willReturn(null);
$registry->has('foo')->willReturn(true);
$registry->get('foo')->willReturn($attributeType);
$attributeType->getStorageType()->willReturn('bar');
$attribute->setStorageType('bar')->shouldBeCalled();
$this->assignStorageType(new ViewEvent(
$kernel->getWrappedObject(),
$request->getWrappedObject(),
HttpKernelInterface::MAIN_REQUEST,
$attribute->getWrappedObject(),
));
}
}

View file

@ -40,6 +40,11 @@
<tag name="form.type" />
</service>
<service id="sylius.validator.attribute_type" class="Sylius\Bundle\AttributeBundle\Validator\Constraints\AttributeTypeValidator">
<argument type="service" id="sylius.registry.attribute_type" />
<tag name="validator.constraint_validator" alias="sylius_attribute_type_validator" />
</service>
<service id="sylius.validator.valid_attribute_value" class="Sylius\Bundle\AttributeBundle\Validator\Constraints\ValidAttributeValueValidator">
<argument type="service" id="sylius.registry.attribute_type" />
<tag name="validator.constraint_validator" alias="sylius_valid_attribute_value_validator" />

View file

@ -24,6 +24,9 @@
<constraint name="Sylius\Bundle\AttributeBundle\Validator\Constraints\ValidSelectAttributeConfiguration">
<option name="groups">sylius</option>
</constraint>
<constraint name="Sylius\Bundle\AttributeBundle\Validator\Constraints\AttributeType">
<option name="groups">sylius</option>
</constraint>
<property name="code">
<constraint name="NotBlank">
<option name="message">sylius.attribute.code.not_blank</option>

View file

@ -17,6 +17,8 @@ sylius:
not_blank: Please enter attribute presentation.
min_length: Attribute presentation must be at least 1 character long.|Attribute presentation must be at least {{ limit }} characters long.
max_length: Attribute presentation must not be longer than 1 character.|Attribute presentation must not be longer than {{ limit }} characters.
type:
unregistered: The attribute type "%type%" is not registered.
attribute_value:
attribute:
not_blank: Please select attribute.

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AttributeBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class AttributeType extends Constraint
{
public string $unregisteredAttributeTypeMessage = 'sylius.attribute.type.unregistered';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy(): string
{
return 'sylius_attribute_type_validator';
}
}

View file

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AttributeBundle\Validator\Constraints;
use Sylius\Component\Attribute\Model\AttributeInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
final class AttributeTypeValidator extends ConstraintValidator
{
public function __construct(private ServiceRegistryInterface $attributeTypeRegistry)
{
}
public function validate(mixed $value, Constraint $constraint):void
{
/** @var mixed|AttributeInterface $value */
Assert::isInstanceOf($value, AttributeInterface::class);
/** @var Constraint|AttributeType $constraint */
Assert::isInstanceOf($constraint, AttributeType::class);
$type = $value->getType();
if (null === $type || $this->attributeTypeRegistry->has($type)) {
return;
}
$this->context
->buildViolation($constraint->unregisteredAttributeTypeMessage, ['%type%' => $type])
->atPath('type')
->addViolation()
;
}
}

View file

@ -49,7 +49,7 @@ final class ValidSelectAttributeConfigurationValidator extends ConstraintValidat
return;
}
$multiple = $value->getConfiguration()['multiple'];
$multiple = $value->getConfiguration()['multiple'] ?? false;
if (!$multiple) {
$this->context->addViolation($constraint->messageMultiple);

View file

@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\AttributeBundle\Validator\Constraints;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\AttributeBundle\Validator\Constraints\AttributeType;
use Sylius\Bundle\AttributeBundle\Validator\Constraints\AttributeTypeValidator;
use Sylius\Component\Attribute\Model\AttributeInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
final class AttributeTypeValidatorSpec extends ObjectBehavior
{
function let(ServiceRegistryInterface $attributeTypesRegistry, ExecutionContextInterface $context): void
{
$this->beConstructedWith($attributeTypesRegistry);
$this->initialize($context);
}
function it_is_a_constraint_validator(): void
{
$this->shouldHaveType(ConstraintValidator::class);
}
function it_throws_exception_when_value_is_not_an_attribute(): void
{
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', [new \stdClass(), new AttributeType()])
;
}
function it_throws_exception_when_constraint_is_not_an_attribute_type(
AttributeInterface $attribute,
Constraint $constraint,
): void {
$this
->shouldThrow(\InvalidArgumentException::class)
->during('validate', [$attribute, $constraint])
;
}
function it_does_nothing_when_attribute_type_is_null(
ServiceRegistryInterface $attributeTypesRegistry,
ExecutionContextInterface $context,
AttributeInterface $attribute,
): void {
$attribute->getType()->willReturn(null);
$attributeTypesRegistry->has(Argument::any())->shouldNotBeCalled();
$context->addViolation(Argument::any())->shouldNotBeCalled();
$this->validate($attribute, new AttributeType());
}
function it_does_nothing_when_attribute_type_is_registered(
ServiceRegistryInterface $attributeTypesRegistry,
ExecutionContextInterface $context,
AttributeInterface $attribute,
): void {
$attribute->getType()->willReturn('foo');
$attributeTypesRegistry->has('foo')->willReturn(true);
$context->addViolation(Argument::any())->shouldNotBeCalled();
$this->validate($attribute, new AttributeType());
}
function it_adds_violation_when_attribute_type_is_not_registered(
ServiceRegistryInterface $attributeTypesRegistry,
ExecutionContextInterface $context,
ConstraintViolationBuilderInterface $violationBuilder,
AttributeInterface $attribute,
): void {
$constraint = new AttributeType();
$attribute->getType()->willReturn('foo');
$attributeTypesRegistry->has('foo')->willReturn(false);
$context
->buildViolation($constraint->unregisteredAttributeTypeMessage, ['%type%' => 'foo'])
->shouldBeCalled()
->willReturn($violationBuilder)
;
$violationBuilder->atPath('type')->shouldBeCalled()->willReturn($violationBuilder);
$violationBuilder->addViolation()->shouldBeCalled();
$this->validate($attribute, $constraint);
}
}

View file

@ -85,6 +85,20 @@ final class ValidSelectAttributeConfigurationValidatorSpec extends ObjectBehavio
$this->validate($attribute, $constraint);
}
function it_adds_a_violation_if_multiple_is_not_set_when_min_or_max_entries_values_are_specified(
ExecutionContextInterface $context,
AttributeInterface $attribute,
): void {
$constraint = new ValidSelectAttributeConfiguration();
$attribute->getType()->willReturn(SelectAttributeType::TYPE);
$attribute->getConfiguration()->willReturn(['min' => 4, 'max' => 6]);
$context->addViolation(Argument::any())->shouldBeCalled();
$this->validate($attribute, $constraint);
}
function it_does_nothing_if_an_attribute_is_not_a_select_type(
ExecutionContextInterface $context,
AttributeInterface $attribute,

View file

@ -0,0 +1,689 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Tests\Api\Admin;
use Sylius\Component\Attribute\AttributeType\CheckboxAttributeType;
use Sylius\Component\Attribute\AttributeType\DateAttributeType;
use Sylius\Component\Attribute\AttributeType\DatetimeAttributeType;
use Sylius\Component\Attribute\AttributeType\FloatAttributeType;
use Sylius\Component\Attribute\AttributeType\IntegerAttributeType;
use Sylius\Component\Attribute\AttributeType\PercentAttributeType;
use Sylius\Component\Attribute\AttributeType\SelectAttributeType;
use Sylius\Component\Attribute\AttributeType\TextareaAttributeType;
use Sylius\Component\Attribute\AttributeType\TextAttributeType;
use Sylius\Component\Product\Model\ProductAttributeInterface;
use Sylius\Tests\Api\JsonApiTestCase;
use Sylius\Tests\Api\Utils\AdminUserLoginTrait;
use Symfony\Component\HttpFoundation\Response;
final class ProductAttributesTest extends JsonApiTestCase
{
use AdminUserLoginTrait;
/** @test */
public function it_gets_product_attributes(): void
{
$this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'product/product_attribute.yaml']);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(method: 'GET', uri: '/api/v2/admin/product-attributes', server: $header);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/get_product_attributes_response',
Response::HTTP_OK,
);
}
/** @test */
public function it_gets_a_product_attribute(): void
{
$fixtures = $this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
'product/product_attribute.yaml',
]);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
/** @var ProductAttributeInterface $productAttribute */
$productAttribute = $fixtures['product_attribute_text'];
$this->client->request(
method: 'GET',
uri: '/api/v2/admin/product-attributes/' . $productAttribute->getCode(),
server: $header,
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/get_product_attribute_response',
Response::HTTP_OK,
);
}
/** @test */
public function it_deletes_a_product_attribute(): void
{
$fixtures = $this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
'product/product_attribute.yaml',
]);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
/** @var ProductAttributeInterface $productAttribute */
$productAttribute = $fixtures['product_attribute_text_delete'];
$this->client->request(
method: 'DELETE',
uri: sprintf('/api/v2/admin/product-attributes/%s', $productAttribute->getCode()),
server: $header,
);
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_NO_CONTENT);
}
/** @test */
public function it_creates_a_text_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'configuration' => [
'min' => 5,
'max' => 255,
],
'type' => TextAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_text_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_textarea_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'type' => TextareaAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_textarea_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_checkbox_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'new',
'type' => CheckboxAttributeType::TYPE,
'translatable' => false,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'New',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_checkbox_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_an_integer_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'pages',
'type' => IntegerAttributeType::TYPE,
'translatable' => false,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Pages',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_integer_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_float_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'display_size',
'type' => FloatAttributeType::TYPE,
'translatable' => false,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Display size',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_float_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_percent_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'damage_reduction',
'type' => PercentAttributeType::TYPE,
'translatable' => false,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Damage reduction',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_percent_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_datetime_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'published_at',
'configuration' => [
'format' => 'Y-m-d H:i:s',
],
'type' => DatetimeAttributeType::TYPE,
'translatable' => false,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Published at',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_datetime_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_date_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'published_at',
'configuration' => [
'format' => 'Y-m-d',
],
'type' => DateAttributeType::TYPE,
'translatable' => false,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Published at',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_date_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_creates_a_select_product_attribute(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'configuration' => [
'choices' => [
'0afb212e-cd08-11ec-871e-0242ac120005' => [
'en_US' => 'Cotton',
'pl_PL' => 'Bawelna',
],
'0afb4e88-cd08-11ec-bcd4-0242ac120005' => [
'en_US' => 'Wool',
],
],
'multiple' => true,
'min' => 1,
'max' => 3,
],
'type' => SelectAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_select_product_attribute_response',
Response::HTTP_CREATED,
);
}
/** @test */
public function it_does_not_create_a_product_attribute_without_required_data(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'translations' => [
'en_US' => [
'locale' => 'en_US',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_product_attribute_without_required_data_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_does_not_create_a_product_attribute_with_unregistered_type(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'test',
'type' => 'foobar',
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_product_attribute_with_unregistered_type_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_does_not_create_a_text_product_attribute_with_invalid_configuration(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'configuration' => [
'min' => 255,
'max' => 250,
],
'type' => TextAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_text_product_attribute_with_invalid_configuration_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_does_not_create_a_select_product_attribute_with_disabled_multiple_option(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'configuration' => [
'choices' => [
'0afb212e-cd08-11ec-871e-0242ac120005' => [
'en_US' => 'Cotton',
'pl_PL' => 'Bawelna',
],
'0afb4e88-cd08-11ec-bcd4-0242ac120005' => [
'en_US' => 'Wool',
],
],
'multiple' => false,
'min' => 1,
'max' => 3,
],
'type' => SelectAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_select_product_attribute_with_disabled_multiple_option_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_does_not_create_a_select_product_attribute_with_invalid_max_entries_configuration(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'configuration' => [
'choices' => [
'0afb212e-cd08-11ec-871e-0242ac120005' => [
'en_US' => 'Cotton',
'pl_PL' => 'Bawelna',
],
'0afb4e88-cd08-11ec-bcd4-0242ac120005' => [
'en_US' => 'Wool',
],
],
'multiple' => true,
'min' => 3,
'max' => 1,
],
'type' => SelectAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_select_product_attribute_with_invalid_max_entries_configuration_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_does_not_create_a_select_product_attribute_with_invalid_min_entries_configuration(): void
{
$this->loadFixturesFromFile('authentication/api_administrator.yaml');
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
$this->client->request(
method: 'POST',
uri: '/api/v2/admin/product-attributes',
server: $header,
content: json_encode([
'code' => 'material',
'configuration' => [
'choices' => [
'0afb212e-cd08-11ec-871e-0242ac120005' => [
'en_US' => 'Cotton',
'pl_PL' => 'Bawelna',
],
'0afb4e88-cd08-11ec-bcd4-0242ac120005' => [
'en_US' => 'Wool',
],
],
'multiple' => true,
'min' => 3,
],
'type' => SelectAttributeType::TYPE,
'translatable' => true,
'translations' => [
'en_US' => [
'locale' => 'en_US',
'name' => 'Material',
],
],
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/post_select_product_attribute_with_invalid_min_entries_configuration_response',
Response::HTTP_UNPROCESSABLE_ENTITY,
);
}
/** @test */
public function it_updates_a_product_attribute(): void
{
$fixtures = $this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
'product/product_attribute.yaml',
]);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
/** @var ProductAttributeInterface $productAttribute */
$productAttribute = $fixtures['product_attribute_select'];
$this->client->request(
method: 'PUT',
uri: '/api/v2/admin/product-attributes/' . $productAttribute->getCode(),
server: $header,
content: json_encode([
'configuration' => [
'choices' => [
'0afb212e-cd08-11ec-871e-0242ac120005' => [
'en_US' => 'handmade',
'fr_FR' => 'fait la main',
],
'0afb4e88-cd08-11ec-bcd4-0242ac120005' => [
'fr_FR' => 'coffret cadeau',
'en_US' => 'gift wrapping',
'pl_PL' => 'pakowanie na prezent',
],
'0afb4e44-cd08-11ec-ad3f-0242ac120005' => [
'en_US' => 'eco approved',
],
],
'min' => 1,
'max' => 3,
'multiple' => true,
],
'translatable' => true,
'position' => 0,
'translations' => [
'en_US' => [
'@id' => sprintf(
'/api/v2/admin/product-attribute-translations/%s',
$productAttribute->getTranslation('en_US')->getId(),
),
'locale' => 'en_US',
'name' => 'Additional information',
],
'pl_PL' => [
'locale' => 'pl_PL',
'name' => 'Dodatkowe informacje',
],
]
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/put_product_attribute_response',
Response::HTTP_OK,
);
}
/** @test */
public function it_does_not_update_the_code_and_type_of_existing_product_attribute(): void
{
$fixtures = $this->loadFixturesFromFiles([
'authentication/api_administrator.yaml',
'product/product_attribute.yaml',
]);
$header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER);
/** @var ProductAttributeInterface $productAttribute */
$productAttribute = $fixtures['product_attribute_text'];
$this->client->request(
method: 'PUT',
uri: '/api/v2/admin/product-attributes/' . $productAttribute->getCode(),
server: $header,
content: json_encode([
'code' => 'NEW_CODE',
'type' => TextAreaAttributeType::TYPE,
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_attribute/put_product_attribute_with_code_and_type_response',
Response::HTTP_OK,
);
}
}

View file

@ -89,13 +89,22 @@ Sylius\Component\Product\Model\ProductAttribute:
storage_type: 'text'
configuration: []
translatable: true
product_attribute_text_delete:
fallbackLocale: en_US
currentLocale: en_US
code: 'text_delete'
name: 'text delete'
type: 'text'
storage_type: 'text'
configuration: []
translatable: false
Sylius\Component\Product\Model\ProductAttributeTranslation:
product_attribute_translation_checkbox:
locale: pl_PL
name: 'mozna myc w zmywarce'
translatable: '@product_attribute_checkbox'
Sylius\Component\Product\Model\ProductAttributeValue:
product_attribute_value_checkbox:
localeCode: pl_PL

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes/origin",
"@type": "ProductAttribute",
"code": "origin",
"configuration": [],
"position": 7,
"storageType": "text",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "origin",
"locale": "en_US"
}
},
"type": "text"
}

View file

@ -0,0 +1,198 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "\/api\/v2\/admin\/product-attributes/dishwasher_safe",
"@type": "ProductAttribute",
"code": "dishwasher_safe",
"configuration": [],
"position": 0,
"storageType": "boolean",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "dishwasher safe",
"locale": "en_US"
},
"pl_PL": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "mozna myc w zmywarce",
"locale": "pl_PL"
}
},
"type": "checkbox"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/manufactured",
"@type": "ProductAttribute",
"code": "manufactured",
"configuration": [],
"position": 1,
"storageType": "date",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "manufactured",
"locale": "en_US"
}
},
"type": "date"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/available_until",
"@type": "ProductAttribute",
"code": "available_until",
"configuration": [],
"position": 2,
"storageType": "datetime",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "available until",
"locale": "en_US"
}
},
"type": "datetime"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/capacity",
"@type": "ProductAttribute",
"code": "capacity",
"configuration": [],
"position": 3,
"storageType": "integer",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "capacity",
"locale": "en_US"
}
},
"type": "integer"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/yixing_clay",
"@type": "ProductAttribute",
"code": "yixing_clay",
"configuration": [],
"position": 4,
"storageType": "float",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "yixing clay",
"locale": "en_US"
}
},
"type": "percent"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/extra_info",
"@type": "ProductAttribute",
"code": "extra_info",
"configuration": {
"choices": {
"0afb212e-cd08-11ec-871e-0242ac120005": {
"fr_FR": "fait a la main",
"en_US": "handmade"
},
"0afb4e88-cd08-11ec-bcd4-0242ac120005": {
"fr_FR": "coffret cadeau",
"en_US": "gift box",
"pl_PL": "opakowanie prezentowe"
}
}
},
"position": 5,
"storageType": "json",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "extra info",
"locale": "en_US"
}
},
"type": "select"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/note",
"@type": "ProductAttribute",
"code": "note",
"configuration": [],
"position": 6,
"storageType": "text",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "note",
"locale": "en_US"
}
},
"type": "textarea"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/origin",
"@type": "ProductAttribute",
"code": "origin",
"configuration": [],
"position": 7,
"storageType": "text",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "origin",
"locale": "en_US"
}
},
"type": "text"
},
{
"@id": "\/api\/v2\/admin\/product-attributes/text_delete",
"@type": "ProductAttribute",
"code": "text_delete",
"configuration": [],
"position": 8,
"storageType": "text",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "text delete",
"locale": "en_US"
}
},
"type": "text"
}
],
"hydra:totalItems": 9
}

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/new",
"@type": "ProductAttribute",
"code": "new",
"configuration": {},
"position": 0,
"storageType": "boolean",
"type": "checkbox",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "New"
}
}
}

View file

@ -0,0 +1,22 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/published_at",
"@type": "ProductAttribute",
"code": "published_at",
"configuration": {
"format": "Y-m-d"
},
"position": 0,
"storageType": "date",
"type": "date",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Published at"
}
}
}

View file

@ -0,0 +1,22 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/published_at",
"@type": "ProductAttribute",
"code": "published_at",
"configuration": {
"format": "Y-m-d H:i:s"
},
"position": 0,
"storageType": "datetime",
"type": "datetime",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Published at"
}
}
}

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/display_size",
"@type": "ProductAttribute",
"code": "display_size",
"configuration": {},
"position": 0,
"storageType": "float",
"type": "float",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Display size"
}
}
}

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/pages",
"@type": "ProductAttribute",
"code": "pages",
"configuration": {},
"position": 0,
"storageType": "integer",
"type": "integer",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Pages"
}
}
}

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/damage_reduction",
"@type": "ProductAttribute",
"code": "damage_reduction",
"configuration": {},
"position": 0,
"storageType": "float",
"type": "percent",
"translatable": false,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Damage reduction"
}
}
}

View file

@ -0,0 +1,13 @@
{
"@context": "\/api\/v2\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "type: The attribute type \"foobar\" is not registered.",
"violations":[
{
"propertyPath": "type",
"message": "The attribute type \"foobar\" is not registered.",
"code": null
}
]
}

View file

@ -0,0 +1,18 @@
{
"@context": "\/api\/v2\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description":"code: Please enter attribute code.\ntranslations[en_US].name: Please enter attribute name.",
"violations":[
{
"propertyPath":"code",
"message":"Please enter attribute code.",
"code": @string@
},
{
"propertyPath":"translations[en_US].name",
"message":"Please enter attribute name.",
"code": @string@
}
]
}

View file

@ -0,0 +1,33 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/material",
"@type": "ProductAttribute",
"code": "material",
"configuration": {
"choices": {
"0afb212e-cd08-11ec-871e-0242ac120005": {
"en_US": "Cotton",
"pl_PL": "Bawelna"
},
"0afb4e88-cd08-11ec-bcd4-0242ac120005": {
"en_US": "Wool"
}
},
"multiple": true,
"min": 1,
"max": 3
},
"position": 0,
"storageType": "json",
"type": "select",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Material"
}
}
}

View file

@ -0,0 +1,13 @@
{
"@context": "\/api\/v2\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "Configuration multiple must be true if min or max entries values are specified.",
"violations":[
{
"propertyPath": "",
"message": "Configuration multiple must be true if min or max entries values are specified.",
"code": null
}
]
}

View file

@ -0,0 +1,13 @@
{
"@context": "\/api\/v2\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "Configuration max entries value must be greater or equal to the min entries value.",
"violations":[
{
"propertyPath": "",
"message": "Configuration max entries value must be greater or equal to the min entries value.",
"code": null
}
]
}

View file

@ -0,0 +1,13 @@
{
"@context": "\/api\/v2\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "Configuration min entries value must be lower or equal to the number of added choices.",
"violations":[
{
"propertyPath": "",
"message": "Configuration min entries value must be lower or equal to the number of added choices.",
"code": null
}
]
}

View file

@ -0,0 +1,23 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/material",
"@type": "ProductAttribute",
"code": "material",
"configuration": {
"min": 5,
"max": 255
},
"position": 0,
"storageType": "text",
"type": "text",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Material"
}
}
}

View file

@ -0,0 +1,13 @@
{
"@context": "\/api\/v2\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "Configuration max length must be greater or equal to the min length.",
"violations":[
{
"propertyPath": "",
"message": "Configuration max length must be greater or equal to the min length.",
"code": null
}
]
}

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/material",
"@type": "ProductAttribute",
"code": "material",
"configuration": {},
"position": 0,
"storageType": "text",
"type": "textarea",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Material"
}
}
}

View file

@ -0,0 +1,45 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes\/extra_info",
"@type": "ProductAttribute",
"code": "extra_info",
"configuration": {
"choices": {
"0afb212e-cd08-11ec-871e-0242ac120005": {
"en_US": "handmade",
"fr_FR": "fait la main"
},
"0afb4e88-cd08-11ec-bcd4-0242ac120005": {
"en_US": "gift wrapping",
"fr_FR": "coffret cadeau",
"pl_PL": "pakowanie na prezent"
},
"0afb4e44-cd08-11ec-ad3f-0242ac120005": {
"en_US": "eco approved"
}
},
"min": 1,
"max": 3,
"multiple": true
},
"position": 0,
"storageType": "json",
"type": "select",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "en_US",
"name": "Additional information"
},
"pl_PL": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"locale": "pl_PL",
"name": "Dodatkowe informacje"
}
}
}

View file

@ -0,0 +1,20 @@
{
"@context": "\/api\/v2\/contexts\/ProductAttribute",
"@id": "\/api\/v2\/admin\/product-attributes/origin",
"@type": "ProductAttribute",
"code": "origin",
"configuration": [],
"position": 7,
"storageType": "text",
"translatable": true,
"translations": {
"en_US": {
"@id": "\/api\/v2\/admin\/product-attribute-translations\/@integer@",
"@type": "ProductAttributeTranslation",
"id": "@integer@",
"name": "origin",
"locale": "en_US"
}
},
"type": "text"
}