Skip to content

Commit

Permalink
Add basic filtering by attribute, add fetch attributes collection end…
Browse files Browse the repository at this point in the history
…point
  • Loading branch information
tbuczen committed Nov 19, 2021
1 parent c988a39 commit 022bace
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 12 deletions.
265 changes: 265 additions & 0 deletions src/Doctrine/Orm/Filter/AttributeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
<?php

/*
* This file was created by developers working at BitBag
* Do you need more information about us and what we do? Visit our https://bitbag.io website!
* We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career
*/

declare(strict_types=1);

namespace BitBag\SyliusGraphqlPlugin\Doctrine\Orm\Filter;

use ApiPlatform\Core\Api\FilterInterface;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\PropertyHelperTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Sylius\Component\Attribute\AttributeType\CheckboxAttributeType;
use Sylius\Component\Attribute\AttributeType\DateAttributeType;
use Sylius\Component\Attribute\AttributeType\DatetimeAttributeType;
use Sylius\Component\Attribute\AttributeType\IntegerAttributeType;
use Sylius\Component\Attribute\AttributeType\PercentAttributeType;
use Sylius\Component\Attribute\AttributeType\SelectAttributeType;
use Sylius\Component\Attribute\Model\AttributeInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

/**
* Filters the collection by value of given attribute IRI
*/
class AttributeFilter extends AbstractContextAwareFilter implements FilterInterface
{

use PropertyHelperTrait;

private IriConverterInterface $iriConverter;

public const OPERATOR_EXACT = 'exact';

public const OPERATOR_PARTIAL = 'partial';

public const ATTRIBUTE_ID = 'attribute_id';

public const VALUE = 'value';

public function __construct(
ManagerRegistry $managerRegistry,
IriConverterInterface $iriConverter,
?RequestStack $requestStack = null,
LoggerInterface $logger = null,
array $properties = null,
NameConverterInterface $nameConverter = null
)
{
parent::__construct(
$managerRegistry,
$requestStack,
$logger,
$properties,
$nameConverter
);

$this->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;
}
}
2 changes: 2 additions & 0 deletions src/Resources/api_resources/Product.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ We are hiring developers from all over the world. Join us and start your new, ex
<attribute>sylius.api.product_taxon_code_filter</attribute>
<attribute>sylius.api.product_taxon_slug_filter</attribute>
<attribute>sylius.api.product_price_filter</attribute>
<attribute>sylius.api.product_attribute_filter</attribute>
</attribute>
</operation>

Expand All @@ -34,6 +35,7 @@ We are hiring developers from all over the world. Join us and start your new, ex
<attribute>sylius.api.product_taxon_code_filter</attribute>
<attribute>sylius.api.product_taxon_slug_filter</attribute>
<attribute>sylius.api.product_price_filter</attribute>
<attribute>sylius.api.product_attribute_filter</attribute>
</attribute>

<attribute name="validation_groups">sylius</attribute>
Expand Down
22 changes: 16 additions & 6 deletions src/Resources/api_resources/ProductAttribute.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ We are hiring developers from all over the world. Join us and start your new, ex
<resource class="%sylius.model.product_attribute.class%" shortName="ProductAttribute">
<graphql>
<operation name="item_query" />
</graphql>

<attribute name="normalization_context">
<attribute name="groups">
<attribute>shop:product:read</attribute>
</attribute>
</attribute>
<operation name="collection_query">
<attribute name="pagination_type">page</attribute>
</operation>
</graphql>

<property name="id" identifier="false" writable="false" />
<property name="code" identifier="true" required="true" />
Expand All @@ -28,5 +26,17 @@ We are hiring developers from all over the world. Join us and start your new, ex
<property name="configuration" required="false" />
<property name="position" required="false" />
<property name="translatable" required="false" />
<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="name">string</attribute>
<attribute name="slug">string</attribute>
<attribute name="locale">string</attribute>
</attribute>
</attribute>
</attribute>
</property>
</resource>
</resources>
26 changes: 26 additions & 0 deletions src/Resources/api_resources/ProductAttributeTranslation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" ?>

<!--
This file was created by developers working at BitBag
Do you need more information about us and what we do? Visit our https://bitbag.io website!
We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career
-->

<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">
<graphql>
<operation name="collection_query">
<attribute name="pagination_type">page</attribute>
</operation>

<operation name="item_query" />
</graphql>

<property name="id" identifier="true" writable="false" />
<property name="name" readable="true" required="true" />
<property name="locale" readable="true" required="true" />
</resource>
</resources>
2 changes: 1 addition & 1 deletion src/Resources/serialization/PaymentMethod.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<class name="Sylius\Component\Core\Model\PaymentMethod">
<class name="%sylius.model.payment_method.class%">
<attribute name="id">
<group>admin:payment_method:read</group>
<group>shop:payment_method:read</group>
Expand Down
24 changes: 24 additions & 0 deletions src/Resources/services/doctrine_orm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
This file was created by developers working at BitBag
Do you need more information about us and what we do? Visit our https://bitbag.io website!
We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career
-->

<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="false" autoconfigure="false" public="false" />

<service id="bitbag_sylius_graphql.doctrine.orm.attribute_filter" class="BitBag\SyliusGraphqlPlugin\Doctrine\Orm\Filter\AttributeFilter" public="false" abstract="true">
<argument type="service" id="doctrine" />
<argument type="service" id="api_platform.iri_converter" />
<argument>null</argument>
<argument type="service" id="logger" on-invalid="ignore" />
<argument key="$nameConverter" type="service" id="api_platform.name_converter" on-invalid="ignore" />
</service>
<service id="BitBag\SyliusGraphqlPlugin\Doctrine\Orm\Filter\AttributeFilter" alias="bitbag_sylius_graphql.doctrine.orm.attribute_filter" />

</services>
</container>
Loading

0 comments on commit 022bace

Please sign in to comment.