partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

AdvancedSearchFilter.php (12920B)


      1 <?php
      2 
      3 namespace PartKeepr\DoctrineReflectionBundle\Filter;
      4 
      5 use Doctrine\Common\Persistence\ManagerRegistry;
      6 use Doctrine\ORM\QueryBuilder;
      7 use Dunglas\ApiBundle\Api\IriConverterInterface;
      8 use Dunglas\ApiBundle\Api\ResourceInterface;
      9 use Dunglas\ApiBundle\Doctrine\Orm\Filter\AbstractFilter;
     10 use PartKeepr\DoctrineReflectionBundle\Services\FilterService;
     11 use Symfony\Component\HttpFoundation\RequestStack;
     12 use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
     13 
     14 /**
     15  * Class AdvancedSearchFilter.
     16  *
     17  * Allows filtering by different operators and nested associations. Expects a query parameter "filter" which includes
     18  * JSON in the following format:
     19  *
     20  * [{
     21  *      property: 'comments.authors.name',
     22  *      operator: 'LIKE',
     23  *      value: '%heiner%'
     24  * }]
     25  *
     26  * You can also specify multiple filters with different operators and values.
     27  */
     28 class AdvancedSearchFilter extends AbstractFilter
     29 {
     30     /**
     31      * @var IriConverterInterface
     32      */
     33     private $iriConverter;
     34 
     35     /**
     36      * @var PropertyAccessorInterface
     37      */
     38     private $propertyAccessor;
     39 
     40     /**
     41      * @var FilterService
     42      */
     43     private $filterService;
     44 
     45     private $aliases = [];
     46 
     47     private $parameterCount = 0;
     48 
     49     private $requestStack;
     50 
     51     private $joins = [];
     52 
     53     /**
     54      * @param ManagerRegistry           $managerRegistry
     55      * @param IriConverterInterface     $iriConverter
     56      * @param PropertyAccessorInterface $propertyAccessor
     57      * @param RequestStack              $requestStack
     58      * @param null|array                $properties       Null to allow filtering on all properties with the exact
     59      *                                                    strategy or a map of property name with strategy.
     60      */
     61     public function __construct(
     62         ManagerRegistry $managerRegistry,
     63         IriConverterInterface $iriConverter,
     64         PropertyAccessorInterface $propertyAccessor,
     65         RequestStack $requestStack,
     66         FilterService $filterService,
     67         array $properties = null
     68     ) {
     69         parent::__construct($managerRegistry, $properties);
     70         $this->requestStack = $requestStack;
     71         $this->iriConverter = $iriConverter;
     72         $this->filterService = $filterService;
     73         $this->propertyAccessor = $propertyAccessor;
     74     }
     75 
     76     public function getDescription(ResourceInterface $resource)
     77     {
     78         return [];
     79     }
     80 
     81     /**
     82      * {@inheritdoc}
     83      */
     84     public function apply(ResourceInterface $resource, QueryBuilder $queryBuilder)
     85     {
     86         $request = $this->requestStack->getCurrentRequest();
     87         if (null === $request) {
     88             return;
     89         }
     90 
     91         if ($request->query->has('filter')) {
     92             $filter = json_decode($request->query->get("filter"));
     93         } else {
     94             $filter = null;
     95         }
     96 
     97         if ($request->query->has('order')) {
     98             $order = json_decode($request->query->get("order"));
     99         } else {
    100             $order = null;
    101         }
    102 
    103         $properties = $this->extractConfiguration($filter, $order);
    104 
    105         $filters = $properties['filters'];
    106         $sorters = $properties['sorters'];
    107 
    108         $this->filter($queryBuilder, $filters, $sorters);
    109     }
    110 
    111     public function filter(QueryBuilder $queryBuilder, $filters, $sorters)
    112     {
    113         foreach ($filters as $filter) {
    114             /**
    115              * @var Filter
    116              */
    117             $queryBuilder->andWhere(
    118                 $this->getFilterExpression($queryBuilder, $filter)
    119             );
    120         }
    121 
    122         foreach ($sorters as $sorter) {
    123             /**
    124              * @var Sorter
    125              */
    126             if ($sorter->getAssociation() !== null) {
    127                 // Pull in associations
    128                 $this->addJoins($queryBuilder, $sorter);
    129             }
    130 
    131             $this->applyOrderByExpression($queryBuilder, $sorter);
    132         }
    133     }
    134 
    135     /**
    136      * Gets the ID from an URI or a raw ID.
    137      *
    138      * @param string $value
    139      *
    140      * @return string
    141      */
    142     private function getFilterValueFromUrl($value)
    143     {
    144         if (is_array($value)) {
    145             $items = [];
    146 
    147             foreach ($value as $iri) {
    148                 try {
    149                     if ($item = $this->iriConverter->getItemFromIri($iri)) {
    150                         $items[] = $this->propertyAccessor->getValue($item, 'id');
    151                     } else {
    152                         $items[] = $iri;
    153                     }
    154                 } catch (\InvalidArgumentException $e) {
    155                     $items[] = $iri;
    156                 }
    157             }
    158 
    159             return $items;
    160         }
    161 
    162         try {
    163             if ($item = $this->iriConverter->getItemFromIri($value)) {
    164                 return $this->propertyAccessor->getValue($item, 'id');
    165             }
    166         } catch (\InvalidArgumentException $e) {
    167             // Do nothing, return the raw value
    168         }
    169 
    170         return $value;
    171     }
    172 
    173     /**
    174      * Adds all required joins to the queryBuilder.
    175      *
    176      * @param QueryBuilder $queryBuilder
    177      * @param              $filter
    178      */
    179     private function addJoins(QueryBuilder $queryBuilder, AssociationPropertyInterface $filter)
    180     {
    181         if (in_array($filter->getAssociation(), $this->joins)) {
    182             // Association already added, return
    183             return;
    184         }
    185 
    186         $associations = explode('.', $filter->getAssociation());
    187 
    188         $fullAssociation = 'o';
    189 
    190         foreach ($associations as $key => $association) {
    191             if (isset($associations[$key - 1])) {
    192                 $parent = $associations[$key - 1];
    193             } else {
    194                 $parent = 'o';
    195             }
    196 
    197             $fullAssociation .= '.'.$association;
    198 
    199             $alias = $this->getAlias($fullAssociation);
    200 
    201             $queryBuilder->join($parent.'.'.$association, $alias);
    202         }
    203 
    204         $this->joins[] = $filter->getAssociation();
    205     }
    206 
    207     /**
    208      * Returns the expression for a specific filter.
    209      *
    210      * @param QueryBuilder $queryBuilder
    211      * @param              $filter
    212      *
    213      * @throws \Exception
    214      *
    215      * @return \Doctrine\ORM\Query\Expr\Comparison|\Doctrine\ORM\Query\Expr\Func
    216      */
    217     private function getFilterExpression(QueryBuilder $queryBuilder, Filter $filter)
    218     {
    219         if ($filter->hasSubFilters()) {
    220             $subFilterExpressions = [];
    221 
    222             foreach ($filter->getSubFilters() as $subFilter) {
    223 
    224                 /**
    225                  * @var Filter
    226                  */
    227                 if ($subFilter->getAssociation() !== null) {
    228                     $this->addJoins($queryBuilder, $subFilter);
    229                 }
    230 
    231                 $subFilterExpressions[] = $this->getFilterExpression($queryBuilder, $subFilter);
    232             }
    233 
    234             if ($filter->getType() == Filter::TYPE_AND) {
    235                 return call_user_func_array([$queryBuilder->expr(), "andX"], $subFilterExpressions);
    236             } else {
    237                 return call_user_func_array([$queryBuilder->expr(), "orX"], $subFilterExpressions);
    238             }
    239         }
    240 
    241         if ($filter->getAssociation() !== null) {
    242             $this->addJoins($queryBuilder, $filter);
    243             $alias = $this->getAlias('o.'.$filter->getAssociation()).'.'.$filter->getProperty();
    244         } else {
    245             $alias = 'o.'.$filter->getProperty();
    246         }
    247 
    248         if (strtolower($filter->getOperator()) == Filter::OPERATOR_IN) {
    249             if (!is_array($filter->getValue())) {
    250                 throw new \Exception('Value needs to be an array for the IN operator');
    251             }
    252 
    253             return $queryBuilder->expr()->in($alias, $filter->getValue());
    254         } else {
    255             $paramName = ':param'.$this->parameterCount;
    256             $this->parameterCount++;
    257             $queryBuilder->setParameter($paramName, $filter->getValue());
    258 
    259             return $this->filterService->getExpressionForFilter($filter, $alias, $paramName);
    260         }
    261     }
    262 
    263     /**
    264      * Returns the expression for a specific sort order.
    265      *
    266      * @param QueryBuilder $queryBuilder
    267      * @param              $sorter
    268      *
    269      * @throws \Exception
    270      *
    271      * @return \Doctrine\ORM\Query\Expr\Comparison|\Doctrine\ORM\Query\Expr\Func
    272      */
    273     private function applyOrderByExpression(QueryBuilder $queryBuilder, Sorter $sorter)
    274     {
    275         if ($sorter->getAssociation() !== null) {
    276             $alias = $this->getAlias('o.'.$sorter->getAssociation()).'.'.$sorter->getProperty();
    277         } else {
    278             $alias = 'o.'.$sorter->getProperty();
    279         }
    280 
    281         return $queryBuilder->addOrderBy($alias, $sorter->getDirection());
    282     }
    283 
    284     /**
    285      * {@inheritdoc}
    286      */
    287     public function extractConfiguration($filterData, $sorterData)
    288     {
    289         $filters = [];
    290 
    291         if (is_array($filterData)) {
    292             foreach ($filterData as $filter) {
    293                 $filters[] = $this->extractJSONFilters($filter);
    294             }
    295         } elseif (is_object($filterData)) {
    296             $filters[] = $this->extractJSONFilters($filterData);
    297         }
    298 
    299         $sorters = [];
    300 
    301         if (is_array($sorterData)) {
    302             foreach ($sorterData as $sorter) {
    303                 $sorters[] = $this->extractJSONSorters($sorter);
    304             }
    305         } elseif (is_object($sorterData)) {
    306             $sorters[] = $this->extractJSONSorters($sorterData);
    307         }
    308 
    309         return ['filters' => $filters, 'sorters' => $sorters];
    310     }
    311 
    312     /**
    313      * Returns an alias for the given association property.
    314      *
    315      * @param string $property The property in FQDN format, e.g. "comments.authors.name"
    316      *
    317      * @return string The table alias
    318      */
    319     private function getAlias(
    320         $property
    321     ) {
    322         if (!array_key_exists($property, $this->aliases)) {
    323             $this->aliases[$property] = 't'.count($this->aliases);
    324         }
    325 
    326         return $this->aliases[$property];
    327     }
    328 
    329     /**
    330      * Extracts the filters from the JSON object.
    331      *
    332      * @param $data
    333      *
    334      * @throws \Exception
    335      *
    336      * @return Filter
    337      */
    338     private function extractJSONFilters(
    339         $data
    340     ) {
    341         $filter = new Filter();
    342 
    343         if (property_exists($data, 'property')) {
    344             if (strpos($data->property, '.') !== false) {
    345                 $associations = explode('.', $data->property);
    346 
    347                 $property = array_pop($associations);
    348 
    349                 $filter->setAssociation(implode('.', $associations));
    350                 $filter->setProperty($property);
    351             } else {
    352                 $filter->setAssociation(null);
    353                 $filter->setProperty($data->property);
    354             }
    355         } elseif (property_exists($data, "subfilters")) {
    356             if (property_exists($data, 'type')) {
    357                 $filter->setType(strtolower($data->type));
    358             }
    359 
    360             if (is_array($data->subfilters)) {
    361                 $subfilters = [];
    362                 foreach ($data->subfilters as $subfilter) {
    363                     $subfilters[] = $this->extractJSONFilters($subfilter);
    364                 }
    365                 $filter->setSubFilters($subfilters);
    366 
    367                 return $filter;
    368             } else {
    369                 throw new \Exception("The subfilters must be an array of objects");
    370             }
    371         } else {
    372             throw new \Exception('You need to set the filter property');
    373         }
    374 
    375         if (property_exists($data, 'operator')) {
    376             $filter->setOperator($data->operator);
    377         } else {
    378             $filter->setOperator(Filter::OPERATOR_EQUALS);
    379         }
    380 
    381         if (property_exists($data, 'value')) {
    382             $filter->setValue($this->getFilterValueFromUrl($data->value));
    383         } else {
    384             throw new \Exception('No value specified');
    385         }
    386 
    387         return $filter;
    388     }
    389 
    390     /**
    391      * Extracts the sorters from the JSON object.
    392      *
    393      * @param $data
    394      *
    395      * @throws \Exception
    396      *
    397      * @return Sorter A Sorter object
    398      */
    399     private function extractJSONSorters(
    400         $data
    401     ) {
    402         $sorter = new Sorter();
    403 
    404         if ($data->property) {
    405             if (strpos($data->property, '.') !== false) {
    406                 $associations = explode('.', $data->property);
    407 
    408                 $property = array_pop($associations);
    409 
    410                 $sorter->setAssociation(implode('.', $associations));
    411                 $sorter->setProperty($property);
    412             } else {
    413                 $sorter->setAssociation(null);
    414                 $sorter->setProperty($data->property);
    415             }
    416         } else {
    417             throw new \Exception('You need to set the filter property');
    418         }
    419 
    420         if ($data->direction) {
    421             switch (strtoupper($data->direction)) {
    422                 case 'DESC':
    423                     $sorter->setDirection("DESC");
    424                     break;
    425                 case 'ASC':
    426                 default:
    427                     $sorter->setDirection("ASC");
    428                     break;
    429             }
    430         } else {
    431             $sorter->setDirection("ASC");
    432         }
    433 
    434         return $sorter;
    435     }
    436 }