Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No resource returned when using nested filter with ULID type #1958

Closed
machacjan opened this issue Jun 30, 2021 · 2 comments
Closed

No resource returned when using nested filter with ULID type #1958

machacjan opened this issue Jun 30, 2021 · 2 comments

Comments

@machacjan
Copy link

API Platform version(s) affected: 2.6.5

Description
When filtering on nested properties of Symfony\Component\Uid\Ulid type, for example /api/orders?plan.id=01F9ERJY0Y0KHXC0Z8DHYJ47HS no Order resource is returned, even though /api/plans/01F9ERJY0Y0KHXC0Z8DHYJ47HS returns the Plan resource as expected.

How to reproduce
Have 2 entities: entity Order is in ManyToOne relation with entity Plan. Both use Symfony\Component\Uid\Ulid as primary key.

<?php

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Component\Uid\Ulid;

/**
 * @ApiFilter(SearchFilter::class, properties={
 *  "plan.id": "exact"
 * })
 */
class Order
{
    /**
     * @ORM\Id()
     * @ORM\Column(type="ulid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class=UlidGenerator::class)
     */
    private Ulid $id; 

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Plan", inversedBy="orders")
     * @ApiSubresource(maxDepth=1)
     */
    private ?Plan $plan = null;
}

/**
 * @ApiFilter(SearchFilter::class, properties={
 *  "id": "exact"
 * })
 */
class Plan
{
    /**
     * @ORM\Id()
     * @ORM\Column(type="ulid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class=UlidGenerator::class)
     */
    private Ulid $id; 

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Order", mappedBy="plan")
     * 
     * @var Collection<Order>
     */
    private Collection $orders;
}

Additional Context
Symfony version is 5.3.3, PHP version is 8.0.7

Postman returned no records:

no-records

Postman returned single record (expected):

record-found

@machacjan
Copy link
Author

After some extended time researching this issue throughout the internet, it seems this behaviour is not going to get fixed soon, as it is quite niche. Workaround would be to create a Ulid filter, as described in this StackOverflow question. I'll paste the unedited code here as well, just in case the question is deleted in the future.

Filter explanation: when you call for example /api/orders?plan=01F9ERJY0Y0KHXC0Z8DHYJ47HS, Ulid filter is asked if plan property is defined for it in Order entity (second code, replace Field with whatever property you need, in this case it's plan). If so, add new condition to database query, where the value will be properly transformed to Ulid.

I'll close this issue, so the maintainers' time could be spent fixing more pressing issues.

The filter itself:

<?php

namespace App\Api\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Uid\Ulid;

final class UlidFilter extends AbstractContextAwareFilter
{
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        // otherwise filter is applied to order and page as well
        if (
            !$this->isPropertyEnabled($property, $resourceClass) ||
            !$this->isPropertyMapped($property, $resourceClass, true)
        ) {
            return;
        }

        // Generate a unique parameter name to avoid collisions with other filters
        $parameterName = $queryNameGenerator->generateParameterName($property);
        $queryBuilder
            ->andWhere(sprintf('o.%s = :%s', $property, $parameterName))
            ->setParameter($parameterName, (new Ulid($value))->toBinary());
    }

    // This function is only used to hook in documentation generators (supported by Swagger and Hydra)
    public function getDescription(string $resourceClass): array
    {
        if (!$this->properties) {
            return [];
        }

        $description = [];
        foreach ($this->properties as $property => $strategy) {
            $description[$property] = [
                'property' => $property,
                'type' => 'string',
                'required' => false,
                'swagger' => [
                    'description' => 'Filter Ulid property.',
                    'name' => 'Ulid Search filter',
                    'type' => '',
                ],
            ];
        }

        return $description;
    }
}

Use in entity:

use App\Api\Filter\UlidFilter;
...
/**
* @ApiResource()
* @ApiFilter(UlidFilter::class, properties={"Field": "exact"})
**/

Tagging in services.yaml (in my case it was not necessary):

App\Api\Filter\UlidFilter:
        tags: [ 'api_platform.filter' ]

@james75
Copy link

james75 commented Aug 20, 2021

This might help someone. To get this filter working with on postgres I had to use the following:

       $queryBuilder
            ->andWhere(sprintf('o.%s = :%s', $property, $parameterName))
            ->setParameter($parameterName, (Ulid::fromString($value))->toRfc4122());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants