Skip to content

Commit

Permalink
Merge pull request #2062 from carlobeltrame/filter-schedule-entries-b…
Browse files Browse the repository at this point in the history
…y-day

Filter schedule entries by day
  • Loading branch information
carlobeltrame committed Oct 25, 2021
2 parents 06a95e0 + 8e40f16 commit 14db9bc
Show file tree
Hide file tree
Showing 31 changed files with 1,775 additions and 404 deletions.
1 change: 1 addition & 0 deletions api/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"lexik/jwt-authentication-bundle": "2.13.0",
"nelmio/cors-bundle": "2.1.1",
"phpdocumentor/reflection-docblock": "5.3.0",
"rize/uri-template": "0.3.3",
"stof/doctrine-extensions-bundle": "1.6.0",
"swaggest/json-schema": "0.12.39",
"symfony/asset": "5.3.4",
Expand Down
50 changes: 49 additions & 1 deletion api/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions api/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ services:
- '@api_platform.route_name_resolver'
- '@api_platform.filter_locator'
- '@serializer.name_converter.metadata_aware'
- '@Rize\UriTemplate'

App\Serializer\Normalizer\CircularReferenceDetectingHalItemNormalizer:
decorates: 'api_platform.hal.normalizer.item'

App\Serializer\Normalizer\URITemplateNormalizer:
App\Serializer\Normalizer\UriTemplateNormalizer:
decorates: 'api_platform.hal.normalizer.entrypoint'

App\Serializer\Normalizer\CollectionItemsNormalizer:
Expand Down Expand Up @@ -108,9 +109,6 @@ services:
App\Service\MailService:
public: true

Symfony\Component\String\Inflector\EnglishInflector:
public: false

App\EventListener\JWTCreatedListener:
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated }
Expand All @@ -119,5 +117,15 @@ services:
App\Doctrine\FilterByCurrentUserExtension:
tags:
# FilterEagerLoadingExtension has Priority -17 and breaks the DQL generated in ContentNodeRepository => Priority -20 ensures this runs after FilterEagerLoadingExtension
- { name: api_platform.doctrine.orm.query_extension.collection, priority: -20 }
- { name: api_platform.doctrine.orm.query_extension.item }
- { name: api_platform.doctrine.orm.query_extension.collection, priority: -20 }
- { name: api_platform.doctrine.orm.query_extension.item }

App\Metadata\Resource\Factory\UriTemplateFactory:
arguments:
- '@api_platform.filter_locator'

Symfony\Component\String\Inflector\EnglishInflector:
public: false

Rize\UriTemplate:
public: false
20 changes: 10 additions & 10 deletions api/fixtures/periods.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ App\Entity\Period:
period1:
camp: '@camp1'
description: Hauptlager
start: '<dateTimeBetween("now", "now")>'
end: '<dateTimeBetween("+2 days", "+2 days")>'
start: '<(new DateTime((date("Y")+1)."-05-01"))>'
end: '<(new DateTime((date("Y")+1)."-05-03"))>'
period2:
camp: '@camp1'
description: Vorabend
start: '<dateTimeBetween("-2 days", "-2 days")>'
end: '<dateTimeBetween("-2 days", "-2 days")>'
start: '<(new DateTime((date("Y")+1)."-04-15"))>'
end: '<(new DateTime((date("Y")+1)."-04-15"))>'
period1camp2:
camp: '@camp2'
description: Vorweekend
start: '<dateTimeBetween("now", "now")>'
end: '<dateTimeBetween("+2 days", "+14 days")>'
start: '<(new DateTime((date("Y")+1)."-11-10"))>'
end: '<(new DateTime((date("Y")+1)."-11-11"))>'
period1campUnrelated:
camp: '@campUnrelated'
description: Hauptlager
start: '<dateTimeBetween("now", "now")>'
end: '<dateTimeBetween("+2 days", "+14 days")>'
start: '<(new DateTime((date("Y")+2)."-02-20"))>'
end: '<(new DateTime((date("Y")+2)."-03-03"))>'
period1campPrototype:
camp: '@campPrototype'
description: Hauptlager
start: '<dateTimeBetween("now", "now")>'
end: '<dateTimeBetween("+2 days", "+14 days")>'
start: '<(new DateTime((date("Y")-1)."-01-01"))>'
end: '<(new DateTime((date("Y")-1)."-01-15"))>'
13 changes: 9 additions & 4 deletions api/fixtures/scheduleEntries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ App\Entity\ScheduleEntry:
scheduleEntry1:
period: '@period1'
activity: '@activity1'
periodOffset: 180
periodOffset: 480
length: 60
scheduleEntry1period1camp2:
period: '@period1camp2'
activity: '@activity1camp2'
periodOffset: 180
periodOffset: 540
length: 60
scheduleEntry1period1campUnrelated:
period: '@period1campUnrelated'
activity: '@activity1campUnrelated'
periodOffset: 180
periodOffset: 600
length: 60
scheduleEntry1period1campPrototype:
period: '@period1campPrototype'
activity: '@activity1campPrototype'
periodOffset: 180
periodOffset: 660
length: 60
scheduleEntry2period1campPrototype:
period: '@period1campPrototype'
activity: '@activity1campPrototype'
periodOffset: 720
length: 60
179 changes: 179 additions & 0 deletions api/src/Doctrine/Filter/ExpressionDateFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

namespace App\Doctrine\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\DateFilterTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use Doctrine\ORM\QueryBuilder;

/**
* Filters a computed property on the collection by date intervals.
*/
class ExpressionDateFilter extends AbstractContextAwareFilter implements DateFilterInterface {
use DateFilterTrait;

/**
* {@inheritdoc}
*/
public function getDescription(string $resourceClass): array {
$description = [];

$properties = $this->getProperties();
if (null === $properties) {
$properties = [];
}

foreach ($properties as $property => $expression) {
if (!$expression) {
continue;
}

$description += $this->getFilterDescription($property, DateFilterInterface::PARAMETER_BEFORE);
$description += $this->getFilterDescription($property, DateFilterInterface::PARAMETER_STRICTLY_BEFORE);
$description += $this->getFilterDescription($property, DateFilterInterface::PARAMETER_AFTER);
$description += $this->getFilterDescription($property, DateFilterInterface::PARAMETER_STRICTLY_AFTER);
}

return $description;
}

/**
* {@inheritdoc}
*/
protected function filterProperty(
string $property,
$values,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
string $operationName = null
) {
if (
!\is_array($values)
|| !$this->isPropertyEnabled($property, $resourceClass)
) {
return;
}

$alias = $queryBuilder->getRootAliases()[0];
$field = $property;

$expression = $this->properties[$property] ?? null;

if (isset($values[DateFilterInterface::PARAMETER_BEFORE])) {
$this->addWhere(
$queryBuilder,
$queryNameGenerator,
$alias,
$field,
DateFilterInterface::PARAMETER_BEFORE,
$values[DateFilterInterface::PARAMETER_BEFORE],
$expression
);
}

if (isset($values[DateFilterInterface::PARAMETER_STRICTLY_BEFORE])) {
$this->addWhere(
$queryBuilder,
$queryNameGenerator,
$alias,
$field,
DateFilterInterface::PARAMETER_STRICTLY_BEFORE,
$values[DateFilterInterface::PARAMETER_STRICTLY_BEFORE],
$expression
);
}

if (isset($values[DateFilterInterface::PARAMETER_AFTER])) {
$this->addWhere(
$queryBuilder,
$queryNameGenerator,
$alias,
$field,
DateFilterInterface::PARAMETER_AFTER,
$values[DateFilterInterface::PARAMETER_AFTER],
$expression
);
}

if (isset($values[DateFilterInterface::PARAMETER_STRICTLY_AFTER])) {
$this->addWhere(
$queryBuilder,
$queryNameGenerator,
$alias,
$field,
DateFilterInterface::PARAMETER_STRICTLY_AFTER,
$values[DateFilterInterface::PARAMETER_STRICTLY_AFTER],
$expression
);
}
}

/**
* Adds the where clause based on the chosen expression.
*/
protected function addWhere(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $alias,
string $field,
string $operator,
string $value,
string $expression
) {
try {
$value = new \DateTime($value);
} catch (\Exception $e) {
// Silently ignore this filter if it can not be transformed to a \DateTime
$this->logger->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('The field "%s" has a wrong date format. Use one accepted by the \DateTime constructor', $field)),
]);

return;
}

$valueParameter = $queryNameGenerator->generateParameterName($field);
$operatorValue = [
DateFilterInterface::PARAMETER_BEFORE => '<=',
DateFilterInterface::PARAMETER_STRICTLY_BEFORE => '<',
DateFilterInterface::PARAMETER_AFTER => '>=',
DateFilterInterface::PARAMETER_STRICTLY_AFTER => '>',
];
$compiledExpression = $this->compileExpression($queryBuilder, $queryNameGenerator, $alias, $expression);
$baseWhere = sprintf('(%s) %s :%s', $compiledExpression, $operatorValue[$operator], $valueParameter);

$queryBuilder->andWhere($baseWhere);

$queryBuilder->setParameter($valueParameter, $value);
}

/**
* Replaces placeholders in the given expression and adds joins for them.
*/
protected function compileExpression(
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $alias,
string $expression
): string {
// Replace {} with the alias of the current entity
$expression = preg_replace('/\{\}/', $alias, $expression);

// Add joins for all {xyz.abc} property references in the expression
$matches = [];
if (preg_match_all('/\{([^\}]+\.[^\}]+)\}/', $expression, $matches)) {
$relations = array_unique($matches[1] ?? []);

// Replace each instance of {xyz.abc} with its respective joined alias
foreach ($relations as $relation) {
[$joinAlias, $property] = $this->addJoinsForNestedProperty($relation, $alias, $queryBuilder, $queryNameGenerator);
$expression = preg_replace('/\{'.preg_quote($relation, '/').'\}/', "{$joinAlias}.{$property}", $expression);
}
}

return $expression;
}
}
Loading

0 comments on commit 14db9bc

Please sign in to comment.