@@ -0,0 +1,282 @@
<?php
namespace App \Filter ;
use ApiPlatform \Core \Api \IriConverterInterface ;
use ApiPlatform \Core \Bridge \Doctrine \Orm \Filter \SearchFilter ;
use ApiPlatform \Core \Bridge \Doctrine \Orm \Util \QueryBuilderHelper ;
use ApiPlatform \Core \Bridge \Doctrine \Orm \Util \QueryNameGeneratorInterface ;
use ApiPlatform \Core \Exception \InvalidArgumentException ;
use Doctrine \Common \Persistence \ManagerRegistry ;
use Doctrine \DBAL \Types \Type ;
use Doctrine \ORM \QueryBuilder ;
use Psr \Log \LoggerInterface ;
use Symfony \Component \HttpFoundation \RequestStack ;
use Symfony \Component \PropertyAccess \PropertyAccessorInterface ;
class StrategyFilter extends SearchFilter
{
protected $ propertiesWithMultipleStrategies = [];
/**
* {@inheritdoc}
*/
public function __construct (ManagerRegistry $ managerRegistry , RequestStack $ requestStack = null , IriConverterInterface $ iriConverter , PropertyAccessorInterface $ propertyAccessor = null , LoggerInterface $ logger = null , array $ properties = null )
{
$ props = [];
// Remap properties when multiple strategies were applied
foreach ($ properties as $ property => $ strategies ) {
// Property with only one strategy
if (strpos ($ strategies , '| ' ) === false ) {
$ props [$ property ] = $ strategies ;
continue ;
}
// Process multiple strategies
foreach (explode ('| ' , $ strategies ) as $ strategy ) {
$ propertyName = sprintf ('%s[%s] ' , $ property , $ strategy );
$ props [$ propertyName ] = $ strategy ;
// Store first defined strategy for BC
if (!isset ($ props [$ property ])) {
$ props [$ property ] = $ strategy ;
// Avoid creating another filter with the first one
continue ;
}
// Store this properties with multiple strategies for further usage
$ this ->propertiesWithMultipleStrategies [$ propertyName ] = [
'property ' => $ property ,
'strategy ' => $ strategy ,
];
}
}
parent ::__construct ($ managerRegistry , $ requestStack , $ iriConverter , $ propertyAccessor , $ logger , $ props );
}
/**
* {@inheritdoc}
*/
public function getDescription (string $ resourceClass ): array
{
$ description = parent ::getDescription ($ resourceClass );
foreach ($ this ->propertiesWithMultipleStrategies as $ parameterName => $ mapping ) {
$ property = $ mapping ['property ' ];
$ strategy = $ mapping ['strategy ' ];
if (!$ this ->isPropertyMapped ($ property , $ resourceClass , true )) {
continue ;
}
if ($ this ->isPropertyNested ($ property , $ resourceClass )) {
$ propertyParts = $ this ->splitPropertyParts ($ property , $ resourceClass );
$ field = $ propertyParts ['field ' ];
$ metadata = $ this ->getNestedMetadata ($ resourceClass , $ propertyParts ['associations ' ]);
} else {
$ field = $ property ;
$ metadata = $ this ->getClassMetadata ($ resourceClass );
}
if ($ metadata ->hasField ($ field )) {
$ typeOfField = $ this ->getType ($ metadata ->getTypeOfField ($ field ));
$ filterParameterNames = [$ parameterName ];
foreach ($ filterParameterNames as $ filterParameterName ) {
$ description [$ filterParameterName ] = [
'property ' => $ property ,
'type ' => $ typeOfField ,
'required ' => false ,
'strategy ' => $ strategy ,
];
}
} elseif ($ metadata ->hasAssociation ($ field )) {
$ filterParameterNames = [
$ parameterName ,
];
foreach ($ filterParameterNames as $ filterParameterName ) {
$ description [$ filterParameterName ] = [
'property ' => $ property ,
'type ' => 'string ' ,
'required ' => false ,
'strategy ' => self ::STRATEGY_EXACT ,
];
}
}
}
return $ description ;
}
/**
* {@inheritdoc}
*/
protected function filterProperty (string $ property , $ value , QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , string $ operationName = null )
{
if (
null === $ value ||
!$ this ->isPropertyEnabled ($ property , $ resourceClass ) ||
!$ this ->isPropertyMapped ($ property , $ resourceClass , true )
) {
return ;
}
$ alias = $ queryBuilder ->getRootAliases ()[0 ];
$ field = $ property ;
$ strategy = NULL ;
if ($ this ->isPropertyNested ($ property , $ resourceClass )) {
list ($ alias , $ field , $ associations ) = $ this ->addJoinsForNestedProperty ($ property , $ alias , $ queryBuilder , $ queryNameGenerator , $ resourceClass );
$ metadata = $ this ->getNestedMetadata ($ resourceClass , $ associations );
} else {
$ metadata = $ this ->getClassMetadata ($ resourceClass );
}
if (is_array ($ value ) && sizeof ($ value ) == 1 ) {
$ strategy = array_keys ($ value )[0 ];
$ value = array_values ($ value )[0 ];
}
$ values = $ this ->normalizeValues ((array ) $ value );
if (empty ($ values )) {
$ this ->logger ->notice ('Invalid filter ignored ' , [
'exception ' => new InvalidArgumentException (sprintf ('At least one value is required, multiple values should be in "%1$s[]=firstvalue&%1$s[]=secondvalue" format ' , $ property )),
]);
return ;
}
$ caseSensitive = true ;
if ($ metadata ->hasField ($ field )) {
if ('id ' === $ field ) {
$ values = array_map ([$ this , 'getIdFromValue ' ], $ values );
}
if (!$ this ->hasValidValues ($ values , $ this ->getDoctrineFieldType ($ property , $ resourceClass ))) {
$ this ->logger ->notice ('Invalid filter ignored ' , [
'exception ' => new InvalidArgumentException (sprintf ('Values for field "%s" are not valid according to the doctrine type. ' , $ field )),
]);
return ;
}
if ( ! $ strategy ) {
$ strategy = $ this ->properties [$ property ] ?? self ::STRATEGY_EXACT ;
}
// prefixing the strategy with i makes it case insensitive
if (0 === strpos ($ strategy , 'i ' )) {
$ strategy = substr ($ strategy , 1 );
$ caseSensitive = false ;
}
if (1 === \count ($ values )) {
$ this ->addWhereByStrategy ($ strategy , $ queryBuilder , $ queryNameGenerator , $ alias , $ field , $ values [0 ], $ caseSensitive );
return ;
}
if (self ::STRATEGY_EXACT !== $ strategy ) {
$ this ->logger ->notice ('Invalid filter ignored ' , [
'exception ' => new InvalidArgumentException (sprintf ('"%s" strategy selected for "%s" property, but only "%s" strategy supports multiple values ' , $ strategy , $ property , self ::STRATEGY_EXACT )),
]);
return ;
}
$ wrapCase = $ this ->createWrapCase ($ caseSensitive );
$ valueParameter = $ queryNameGenerator ->generateParameterName ($ field );
$ queryBuilder
->andWhere (sprintf ($ wrapCase ('%s.%s ' ).' IN (:%s) ' , $ alias , $ field , $ valueParameter ))
->setParameter ($ valueParameter , $ caseSensitive ? $ values : array_map ('strtolower ' , $ values ));
}
// metadata doesn't have the field, nor an association on the field
if (!$ metadata ->hasAssociation ($ field )) {
return ;
}
$ values = array_map ([$ this , 'getIdFromValue ' ], $ values );
if (!$ this ->hasValidValues ($ values , $ this ->getDoctrineFieldType ($ property , $ resourceClass ))) {
$ this ->logger ->notice ('Invalid filter ignored ' , [
'exception ' => new InvalidArgumentException (sprintf ('Values for field "%s" are not valid according to the doctrine type. ' , $ field )),
]);
return ;
}
$ association = $ field ;
$ valueParameter = $ queryNameGenerator ->generateParameterName ($ association );
if ($ metadata ->isCollectionValuedAssociation ($ association )) {
$ associationAlias = QueryBuilderHelper::addJoinOnce ($ queryBuilder , $ queryNameGenerator , $ alias , $ association );
$ associationField = 'id ' ;
} else {
$ associationAlias = $ alias ;
$ associationField = $ field ;
}
if (1 === \count ($ values )) {
$ queryBuilder
->andWhere (sprintf ('%s.%s = :%s ' , $ associationAlias , $ associationField , $ valueParameter ))
->setParameter ($ valueParameter , $ values [0 ]);
} else {
$ queryBuilder
->andWhere (sprintf ('%s.%s IN (:%s) ' , $ associationAlias , $ associationField , $ valueParameter ))
->setParameter ($ valueParameter , $ values );
}
}
/**
* Converts a Doctrine type in PHP type.
*
* @param string $doctrineType
*
* @return string
*/
private function getType (string $ doctrineType ): string
{
switch ($ doctrineType ) {
case Type::TARRAY :
return 'array ' ;
case Type::BIGINT :
case Type::INTEGER :
case Type::SMALLINT :
return 'int ' ;
case Type::BOOLEAN :
return 'bool ' ;
case Type::DATE :
case Type::TIME :
case Type::DATETIME :
case Type::DATETIMETZ :
return \DateTimeInterface::class;
case Type::FLOAT :
return 'float ' ;
}
if (\defined (Type::class.'::DATE_IMMUTABLE ' )) {
switch ($ doctrineType ) {
case Type::DATE_IMMUTABLE :
case Type::TIME_IMMUTABLE :
case Type::DATETIME_IMMUTABLE :
case Type::DATETIMETZ_IMMUTABLE :
return \DateTimeInterface::class;
}
}
return 'string ' ;
}
}