diff --git a/src/Doctrine/Orm/Filter/AttributeFilter.php b/src/Doctrine/Orm/Filter/AttributeFilter.php
new file mode 100644
index 00000000..2bfa1930
--- /dev/null
+++ b/src/Doctrine/Orm/Filter/AttributeFilter.php
@@ -0,0 +1,265 @@
+iriConverter = $iriConverter;
+ }
+
+ /**
+ * @param string $property
+ * @param $values
+ * @param QueryBuilder $queryBuilder
+ * @param QueryNameGeneratorInterface $queryNameGenerator
+ * @param string $resourceClass
+ * @param string|null $operationName
+ */
+ protected function filterProperty(
+ string $property,
+ $values,
+ QueryBuilder $queryBuilder,
+ QueryNameGeneratorInterface $queryNameGenerator,
+ string $resourceClass,
+ string $operationName = null
+ ): void
+ {
+ if (
+ !\is_array($values) ||
+ !$this->isPropertyEnabled($property, $resourceClass)
+ ) {
+ return;
+ }
+
+ $attributeId = $this->getAttributeId($values, $property);
+ $value = $this->getValue($values, $property);
+ if (null === $attributeId || null === $value) {
+ return;
+ }
+
+ $alias = $queryBuilder->getRootAliases()[0];
+
+ if ($this->isPropertyNested($property.".id", $resourceClass)) {
+ [$alias] = $this->addJoinsForNestedProperty($property.".id", $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
+ }
+
+ $this->addWhere(
+ $queryBuilder,
+ $queryNameGenerator,
+ $alias,
+ $property,
+ $attributeId,
+ $value
+ );
+ }
+
+ /** @param mixed $value */
+ protected function addWhere(
+ QueryBuilder $queryBuilder,
+ QueryNameGeneratorInterface $queryNameGenerator,
+ string $alias,
+ string $field,
+ string $attributeId,
+ $value,
+ string $operator = self::OPERATOR_EXACT
+ ): void
+ {
+ $valueParameter = $queryNameGenerator->generateParameterName($field);
+
+ /** @var AttributeInterface $attribute */
+ $attribute = $this->iriConverter->getItemFromIri($attributeId);
+ $attributeType = $attribute->getType();
+ $value = $this->normalizeValue($value, $attributeType);
+
+ switch ($operator) {
+ case self::OPERATOR_EXACT:
+ $queryBuilder
+ ->andWhere(
+ sprintf('%s.%s = :%s',
+ $alias,
+ $attributeType,
+ $valueParameter))
+ ->setParameter($valueParameter,$value);
+
+ break;
+ case self::OPERATOR_PARTIAL:
+ if (null === $value) {
+ return;
+ }
+
+ $queryBuilder
+ ->andWhere(sprintf('%s.%s > :%s', $alias, $field, $valueParameter))
+ ->setParameter($valueParameter, $value);
+
+ break;
+ }
+ }
+
+ public function getDescription(string $resourceClass): array
+ {
+ $description = [];
+
+ $properties = $this->getProperties();
+ if (null === $properties) {
+ $properties = array_fill_keys($this->getClassMetadata($resourceClass)->getFieldNames(), null);
+ }
+
+ /** @var string $property */
+ foreach ($properties as $property => $unused) {
+ $description += $this->getFilterDescription($property, self::ATTRIBUTE_ID);
+ $description += $this->getFilterDescription($property, self::VALUE);
+ }
+
+ return $description;
+ }
+
+ protected function getFilterDescription(string $fieldName, string $operator): array
+ {
+ /** @var string $propertyName */
+ $propertyName = $this->normalizePropertyName($fieldName);
+
+ return [
+ sprintf('%s[%s]', $propertyName, $operator) => [
+ 'property' => $propertyName,
+ 'type' => 'string',
+ 'required' => false,
+ ],
+ ];
+ }
+
+ private function getAttributeId(array $values, string $property): ?string
+ {
+ if (key_exists(self::ATTRIBUTE_ID, $values)) {
+ /** @var string $attributeId */
+ $attributeId = $values[self::ATTRIBUTE_ID];
+ } else {
+ $this->getLogger()->notice('Invalid filter ignored', [
+ 'exception' => new InvalidArgumentException(
+ sprintf(
+ '%s is required for "%s" property',
+ self::ATTRIBUTE_ID,
+ $property
+ )
+ )
+ ]);
+
+ return null;
+ }
+
+ return $attributeId;
+ }
+
+ private function getValue(array $values, string $property): ?string
+ {
+
+ if (key_exists(self::VALUE, $values)) {
+ /** @var string $value */
+ $value = $values[self::VALUE];
+ } else {
+ $this->getLogger()->notice('Invalid filter ignored', [
+ 'exception' => new InvalidArgumentException(
+ sprintf(
+ '%s is required for "%s" property',
+ self::VALUE,
+ $property
+ )
+ )
+ ]);
+
+ return null;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @return int|float|string|null|bool|\DateTime
+ * @throws \Exception
+ */
+ private function normalizeValue(string $value, string $type)
+ {
+
+ switch ($type) {
+ case CheckboxAttributeType::TYPE:
+ $value = (bool) $value;
+ break;
+ case DateAttributeType::TYPE:
+ case DatetimeAttributeType::TYPE:
+ $value = new \DateTime($value);
+ break;
+ case IntegerAttributeType::TYPE:
+ $value = (int) $value;
+ break;
+ case PercentAttributeType::TYPE:
+ $value = (float) $value;
+ break;
+ case SelectAttributeType::TYPE:
+ //assume IRI ?
+ //TODO::
+ $value = (bool) $value;
+ break;
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Resources/api_resources/Product.xml b/src/Resources/api_resources/Product.xml
index 188dc48c..45e8d6af 100644
--- a/src/Resources/api_resources/Product.xml
+++ b/src/Resources/api_resources/Product.xml
@@ -21,6 +21,7 @@ We are hiring developers from all over the world. Join us and start your new, ex
sylius.api.product_taxon_code_filter
sylius.api.product_taxon_slug_filter
sylius.api.product_price_filter
+ sylius.api.product_attribute_filter
@@ -34,6 +35,7 @@ We are hiring developers from all over the world. Join us and start your new, ex
sylius.api.product_taxon_code_filter
sylius.api.product_taxon_slug_filter
sylius.api.product_price_filter
+ sylius.api.product_attribute_filter
sylius
diff --git a/src/Resources/api_resources/ProductAttribute.xml b/src/Resources/api_resources/ProductAttribute.xml
index e54e29c7..0af9cb07 100644
--- a/src/Resources/api_resources/ProductAttribute.xml
+++ b/src/Resources/api_resources/ProductAttribute.xml
@@ -13,13 +13,11 @@ We are hiring developers from all over the world. Join us and start your new, ex
-
-
-
- shop:product:read
-
-
+
+ page
+
+
@@ -28,5 +26,17 @@ We are hiring developers from all over the world. Join us and start your new, ex
+
+
+ object
+
+
+ string
+ string
+ string
+
+
+
+
diff --git a/src/Resources/api_resources/ProductAttributeTranslation.xml b/src/Resources/api_resources/ProductAttributeTranslation.xml
new file mode 100644
index 00000000..09b20595
--- /dev/null
+++ b/src/Resources/api_resources/ProductAttributeTranslation.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+ page
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/serialization/PaymentMethod.xml b/src/Resources/serialization/PaymentMethod.xml
index 74dea502..ddb65bd8 100644
--- a/src/Resources/serialization/PaymentMethod.xml
+++ b/src/Resources/serialization/PaymentMethod.xml
@@ -10,7 +10,7 @@ We are hiring developers from all over the world. Join us and start your new, ex
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"
>
-
+
admin:payment_method:read
shop:payment_method:read
diff --git a/src/Resources/services/doctrine_orm.xml b/src/Resources/services/doctrine_orm.xml
new file mode 100644
index 00000000..cd3623e3
--- /dev/null
+++ b/src/Resources/services/doctrine_orm.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
diff --git a/src/Resources/services/filters.xml b/src/Resources/services/filters.xml
index f7d89a77..414d4b48 100644
--- a/src/Resources/services/filters.xml
+++ b/src/Resources/services/filters.xml
@@ -21,17 +21,17 @@ We are hiring developers from all over the world. Join us and start your new, ex
-
- partial
- partial
+
-