Custom FIlter that matches two fields - symfony

I simply want an API endpoint that matches two fields, registrationId and hash.
I cannot find any examples of this online, I can only single matches, regex matching, OR clauses etc. Nowhere is there an example of just just querying the database to return a row that matches two columns.
I have a table called registrationhashes. In the entity I have added this line:
#[ApiFilter(RegistrationSearchFilter::class, properties: ['registrationId', 'hash'])]
And then I have gotten so far with writing a custom filter:
final class RegistrationSearchFilter extends AbstractFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
if ($property !== 'search') {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.registrationId = :search AND %s.hash = :hash', $alias, $alias));
}
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["regexp_$property"] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,
'required' => false,
'description' => 'Filter using a regex. This will appear in the OpenApi documentation!',
'openapi' => [
'example' => 'Custom example that will be in the documentation and be the default value of the sandbox',
'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;
}
}
I have just put the description function in there copied from somewhere else for now.
But as you can see from the WHERE clause I ned to match registrationId AND hash, but I only have one value to match with.
How can I achieve this?

Look at the search filter docs, you will see an example of combining two search filters. I believe this accomplishes your task without the need for a custom filter.
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['registrationId' => 'exact', 'hash' => 'exact'])]
class RegistrationHash
{
// ...
}
The endpoint would then be something like:
http://localhost:8000/api/registrationhashes?registrationId=10&hash=3C76B43F

Related

How to create a JMSSerialization Handler for for base types, e.g. array <-> csv string

How to create a custom JMSSerializen handler for base types like strings or arrays? The goal would be to (de)serialize some properties in non-default ways. For example to specify that one array property should be deserialized to a CSV string instead to a default JSON array or one string string property to to an encrypted / encoded string, etc.
While creating such a handler for a custom class was no problem, I struggle to do the same for base types.
class SyncableEntityHandler implements SubscribingHandlerInterface {
public static function getSubscribingMethods() {
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'SomeClass',
'method' => 'serializeSomeClassToJson',
],
[
'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => 'SomeClass',
'method' => 'deserializeSomeClassFromJson',
],
];
}
public function serializeSomeClassToJson(JsonSerializationVisitor $visitor, AbstractSyncableEntity $entity, array $type, Context $context) {
...
}
public function deserializeSomeClassFromJson(JsonDeserializationVisitor $visitor, $entityGuid, array $type, Context $context) {
...
}
}
class OtherClass {
/*
* #JMS\Type("SomeClass")
*/
protected $someProperty;
/*
* #JMS\Type("array")
*/
protected $serializeToDefaultArray;
/*
* #JMS\Type("csv_array") // How to do this?
*/
protected $serializeToCSVString;
}
While I can create a handler with 'type' => 'array' to change the (de)serializiation of all arrays, I do not know to only select some arrays.
I already thought about using getters and setters instead (e.g. getPropertyAsCsvString() and setPropertyFromCsvString()). While this would work with custom classes, I would like to serialize some third-party classes as well where I specify the serialization options not with annotations but in .yaml files. Adding getter and setters to these classes is not (easily) possible. Additionally creating these getters and setters would add a lot of overhead.
So, is there a way to create and specify special handlers for some properties?
The implementation is quite straightforward:
class CsvArrayHandler implements SubscribingHandlerInterface {
public static function getSubscribingMethods() {
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'csv_array',
'method' => 'serializeToJson',
],
[
'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => 'csv_array',
'method' => 'deserializeFromJson',
],
];
}
public function serializeSomeClassToJson(JsonSerializationVisitor $visitor, array $array, array $type, Context $context) {
return implode(',', $array);
}
public function deserializeSomeClassFromJson(JsonDeserializationVisitor $visitor, string $csvString, array $type, Context $context) {
return explode(',', $csvString);
}
}
Then just annotate your property with #JMS\Type("csv_array") and register the handler.
Note that using explode and implode does not escape the input so you may want to use something like league/csv instead.

Easyadmin add an option for every filter to be inclusive or exclusive (use OR or AND)

I'm trying to modify easyadmin filters to include an option to change de behaviour of the query from "andWhere" to "orWhere" and give the possibility to make the filters non-exclusive.
I'm trying with the class ComparysonFilterType adding a checkbox:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('comparison', $options['comparison_type'], $options['comparison_type_options']);
$builder->add('value', FormTypeHelper::getTypeClass($options['value_type']), $options['value_type_options'] + [
'label' => false,
]);
#I've added this field
$builder->add('useOr', CheckboxType::class, [
'label' => 'no excluyente',
'required' => false
]);
}
And then I'm changing the filter function with a simple conditional:
public function filter(QueryBuilder $queryBuilder, FormInterface $form, array $metadata)
{
$alias = current($queryBuilder->getRootAliases());
$property = $metadata['property'];
$paramName = static::createAlias($property);
$data = $form->getData();
#I've added this lines
if(true === $data["useOr"]){
$queryBuilder->orWhere(sprintf('%s.%s %s :%s', $alias, $property, $data['comparison'], $paramName))
->setParameter($paramName, $data['value']);
}else{
$queryBuilder->andWhere(sprintf('%s.%s %s :%s', $alias, $property, $data['comparison'], $paramName))
->setParameter($paramName, $data['value']);
}
}
This works but maybe there is another approach with custom filters more clean to achieve this goal. I'm trying not to modify the original classes.
I would like to give this option in any filterType and not only in "ComparisonFilterType"
Any suggests

Symfony Forms: CollectionType - Don't transform null to empty array

We are using Symfony Forms for our API to validate request data. At the moment we are facing a problem with the CollectionType which is converting the supplied value null to an empty array [].
As it is important for me to differentiate between the user suppling null or an empty array I would like to disable this behavior.
I already tried to set the 'empty_data' to null - unfortunately without success.
This is how the configuration of my field looks like:
$builder->add(
'subjects',
Type\CollectionType::class,
[
'entry_type' => Type\IntegerType::class,
'entry_options' => [
'label' => 'subjects',
'required' => true,
'empty_data' => null,
],
'required' => false,
'allow_add' => true,
'empty_data' => null,
]
);
The form get's handled like this:
$data = $apiRequest->getData();
$form = $this->formFactory->create($formType, $data, ['csrf_protection' => false, 'allow_extra_fields' => true]);
$form->submit($data);
$formData = $form->getData();
The current behavior is:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => [] }
My desired behavior would be:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => null }
After several tries I finally found a solution by creating a From Type Extension in combination with a Data Transformer
By creating this form type extension I'm able to extend the default configuration of the CollectionType FormType. This way I can set a custom build ModelTransformer to handle my desired behavior.
This is my Form Type Extension:
class KeepNullFormTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer(new KeepNullDataTransformer());
}
}
This one needs to be registered with the 'form.type_extension' tag in your service.yml:
PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension:
class: PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension
tags: ['form.type_extension']
Please note that you still use the CollectionType in your FormType and not the KeepNullFormTypeExtension as Symfony takes care about the extending...
In the KeepNullFormTypeExtension you can see that I set a custom model transformer with addModelTransformer which is called KeepNullDataTransformer
The KeepNullDataTransformer is responsible for keeping the input null as the output value - it looks like this:
class KeepNullDataTransformer implements DataTransformerInterface
{
protected $initialInputValue = 'unset';
/**
* {#inheritdoc}
*/
public function transform($data)
{
$this->initialInputValue = $data;
return $data;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->initialInputValue === null) ? null : $data;
}
}
And that's it - this way a supplied input of the type null will stay as null.
More details about this can be found in the linked Symfony documentation:
https://symfony.com/doc/current/form/create_form_type_extension.html
https://symfony.com/doc/2.3/cookbook/form/data_transformers.html

Filter on collection

Is there way to filter collection, e.g.
class Company{
/**
* #ORM\OneToMany(targetEntity="App\Entity\User", mappedBy="company", cascade={"persist","remove"})
*/
public $users;
}
on way to check that company have users. But i need to filter on company side, so send request /api/companies?somefilter.
So point is there way to check is collection empty?
You can add a boolean column in companies where you set to true when create a user relation.
So you can add a BooleanFilter to you company entity for check companies which have users.
/**
* #ApiResource
* #ApiFilter(BooleanFilter::class, properties={"hasUsers"})
*/
Or you can create a CustomFilter where you input true o false and get companies with users throught queryBuilder
https://api-platform.com/docs/core/filters/#creating-custom-doctrine-orm-filters
<?php
// api/src/Filter/RegexpFilter.php
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
final class RegexpFilter 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)
) {
return;
}
$parameterName = $queryNameGenerator->generateParameterName($property);
// Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
->setParameter($parameterName, $value);
}
// 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["regexp_$property"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter using a regex. This will appear in the Swagger documentation!',
'name' => 'Custom name to use in the Swagger documentation',
'type' => 'Will appear below the name in the Swagger documentation',
],
];
}
return $description;
}
}

How do you default to the empty_value choice in a custom symfony form field type?

I have a created a custom form field dropdown list for filtering by year. One of the things I want to do is to allow the user to filter by all years, which is the default option. I am adding this as an empty_value. However, when I render the form, it defaults on the first item that's not the empty value. The empty value is there, just above it in the list. How do I make the page default to, in my case 'All' when the page initially loads? Code is below.
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class YearType extends AbstractType
{
private $yearChoices;
public function __construct()
{
$thisYear = date('Y');
$startYear = '2012';
$this->yearChoices = range($thisYear, $startYear);
}
public function getDefaultOptions(array $options)
{
return array(
'empty_value' => 'All',
'choices' => $this->yearChoices,
);
}
public function getParent(array $options)
{
return 'choice';
}
public function getName()
{
return 'year';
}
}
I'm rendering my form in twig with a simple {{ form_widget(filter_form) }}
Try adding empty_data option to null, so it comes first. I have many fields of this type and it's working, for example:
class GenderType extends \Symfony\Component\Form\AbstractType
{
public function getDefaultOptions(array $options)
{
return array(
'empty_data' => null,
'empty_value' => "Non specificato",
'choices' => array('m' => 'Uomo', 'f' => 'Donna'),
'required' => false,
);
}
public function getParent(array $options) { return 'choice'; }
public function getName() { return 'gender'; }
}
EDIT: Another possibility (i suppose) would be setting preferred_choices. This way you'll get "All" option to the top. But i don't know if it can work with null empty_data, but you can change empty_data to whatever you want:
public function getDefaultOptions(array $options)
{
return array(
'empty_value' => 'All',
'empty_data' => null,
'choices' => $this->yearChoices,
'preferred_choices' => array(null) // Match empty_data
);
}
When I've needed a simple cities dropwdown from database without using relations, I've ended up using this config for city field (adding null as first element of choices array), as empty_data param didn't do work for me:
$builder->add('city',
ChoiceType::class,
[
'label' => 'ui.city',
'choices' => array_merge([null], $this->cityRepository->findAll()),
'choice_label' => static function (?City $city) {
return null === $city ? '' : $city->getName();
},
'choice_value' => static function(?City $city) {
return null === $city ? null : $city->getId();
},
]);

Resources