Added entity filter to filter part response by categories, etc.

This commit is contained in:
Jan Böhmer 2023-10-03 21:37:58 +02:00
parent e339b7d9f0
commit 00708608cd
3 changed files with 162 additions and 1 deletions

View file

@ -96,6 +96,17 @@ whose name starts with "BC", you can use `/api/parts.jsonld?name=BC%25` (the `%2
There are other filters available for some entities, allowing you to search on other fields, or restricting the results
by numeric values or dates. See the endpoint documentation for the available filters.
## Filter by associated entities
To get all parts with a certain category, manufacturer, etc. you can use the `category`, `manufacturer`, etc. query parameters of the `/api/parts` endpoint.
They are so called entitiy filters and accept a comma separated list of IDs of the entities you want to filter by.
For example if you want to get all parts with the category "Resistor" (Category ID 1) and "Capacitor" (Category ID 2), you can use `/api/parts.jsonld?category=1,2`.
Suffix an id with `+` to suffix, to include all direct children categories of the given category. Use the `++` suffix to include all children categories recursively.
To get all parts with the category "Resistor" (Category ID 1) and all children categories of "Capacitor" (Category ID 2), you can use `/api/parts.jsonld?category=1,2++`.
See the endpoint documentation for the available entity filters.
## Ordering results
When retrieving a list of entities, you can order the results by various fields using the `order` query parameter.

View file

@ -0,0 +1,148 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\ApiPlatform\Filter;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
class EntityFilter extends AbstractFilter
{
public function __construct(
ManagerRegistry $managerRegistry,
private NodesListBuilder $nodesListBuilder,
private EntityManagerInterface $entityManager,
LoggerInterface $logger = null,
?array $properties = null,
?NameConverterInterface $nameConverter = null
) {
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
}
protected function filterProperty(
string $property,
$value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
Operation $operation = null,
array $context = []
): void {
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass, true)
) {
return;
}
$metadata = $this->getClassMetadata($resourceClass);
$target_class = $metadata->getAssociationTargetClass($property);
//If it is not an association we can not filter the property
if (!$target_class) {
return;
}
$elements = $this->valueToEntityArray($value, $target_class);
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere(sprintf('o.%s IN (:%s)', $property, $parameterName))
->setParameter($parameterName, $elements);
}
private function valueToEntityArray(string $value, string $target_class): array
{
//Convert value to IDs:
$elements = [];
//Split the given value by comm
foreach (explode(',', $value) as $id) {
if (trim($id) === '') {
continue;
}
//Check if the given value ends with a plus, then we want to include all direct children
$include_children = false;
$include_recursive = false;
if (str_ends_with($id, '++')) { //Plus Plus means include all children recursively
$id = substr($id, 0, -2);
$include_recursive = true;
} elseif (str_ends_with($id, '+')) {
$id = substr($id, 0, -1);
$include_children = true;
}
//Get a (shallow) reference to the entitity
$element = $this->entityManager->getReference($target_class, (int) $id);
$elements[] = $element;
//If $element is not structural we are done
if (!is_a($element, AbstractStructuralDBElement::class)) {
continue;
}
//Get the recursive list of children
if ($include_recursive) {
$elements = array_merge($elements, $this->nodesListBuilder->getChildrenFlatList($element));
} elseif ($include_children) {
$elements = array_merge($elements, $element->getChildren()->toArray());
}
}
return $elements;
}
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["$property"] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,
'required' => false,
'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.',
'openapi' => [
'example' => '',
'allowReserved' => false,// if true, query parameters will be not percent-encoded
'allowEmptyValue' => true,
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
],
];
}
return $description;
}
}

View file

@ -37,6 +37,7 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\DocumentedAPIProperty;
use App\ApiPlatform\Filter\EntityFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\PartRepository;
@ -91,8 +92,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
denormalizationContext: ['groups' => ['part:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])]
#[ApiFilter(BooleanFilter::class, properties: ["favorite" ])]
#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])]
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]