From c1a58f4bd6045cdbc78b2abc28a2f2e4c52e6fb2 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 11 Feb 2020 22:48:30 +0100 Subject: [PATCH 01/16] fluent repository prototype --- src/Event/HandleDatabaseModifierEvent.php | 34 +++++++++++ src/Event/HandleModifierEvent.php | 35 +++++++++++ src/Exception/InvalidOptionException.php | 13 ++++ .../Database/LimitDatabaseHandler.php | 27 +++++++++ .../Database/WithIdDatabaseHandler.php | 45 ++++++++++++++ src/Handlers/ModifierHandler.php | 10 ++++ src/Modifiers/Limit.php | 30 ++++++++++ src/Modifiers/Modifier.php | 8 +++ src/Modifiers/WithId.php | 31 ++++++++++ .../Database/GenericLineRepository.php | 60 +++++++++++++++---- src/Provider/FluentRepository.php | 11 ++++ src/Provider/LineRepository.php | 4 +- src/Provider/Repository.php | 3 +- 13 files changed, 294 insertions(+), 17 deletions(-) create mode 100644 src/Event/HandleDatabaseModifierEvent.php create mode 100644 src/Event/HandleModifierEvent.php create mode 100644 src/Exception/InvalidOptionException.php create mode 100644 src/Handlers/Database/LimitDatabaseHandler.php create mode 100644 src/Handlers/Database/WithIdDatabaseHandler.php create mode 100644 src/Handlers/ModifierHandler.php create mode 100644 src/Modifiers/Limit.php create mode 100644 src/Modifiers/Modifier.php create mode 100644 src/Modifiers/WithId.php create mode 100644 src/Provider/FluentRepository.php diff --git a/src/Event/HandleDatabaseModifierEvent.php b/src/Event/HandleDatabaseModifierEvent.php new file mode 100644 index 0000000..74bbc81 --- /dev/null +++ b/src/Event/HandleDatabaseModifierEvent.php @@ -0,0 +1,34 @@ +builder = $builder; + } + + public function getBuilder(): QueryBuilder + { + return $this->builder; + } + + public function replaceBuilder(QueryBuilder $builder): void + { + $this->builder = $builder; + } +} diff --git a/src/Event/HandleModifierEvent.php b/src/Event/HandleModifierEvent.php new file mode 100644 index 0000000..af28988 --- /dev/null +++ b/src/Event/HandleModifierEvent.php @@ -0,0 +1,35 @@ +repository = $repository; + $this->modifier = $modifier; + $this->meta = $meta; + } + + public function getModifier(): Modifier + { + return $this->modifier; + } + + public function getRepository() + { + return $this->repository; + } + + public function getMeta(): array + { + return $this->meta; + } +} diff --git a/src/Exception/InvalidOptionException.php b/src/Exception/InvalidOptionException.php new file mode 100644 index 0000000..b052f10 --- /dev/null +++ b/src/Exception/InvalidOptionException.php @@ -0,0 +1,13 @@ +getModifier(); + $builder = $event->getBuilder(); + + $builder + ->setFirstResult($modifier->getOffset()) + ->setMaxResults($modifier->getCount()) + ; + } +} diff --git a/src/Handlers/Database/WithIdDatabaseHandler.php b/src/Handlers/Database/WithIdDatabaseHandler.php new file mode 100644 index 0000000..1cbb324 --- /dev/null +++ b/src/Handlers/Database/WithIdDatabaseHandler.php @@ -0,0 +1,45 @@ +id = $id; + } + + public function process(HandleModifierEvent $event) + { + if (!$event instanceof HandleDatabaseModifierEvent) { + return; + } + + /** @var WithId $modifier */ + $modifier = $event->getModifier(); + $builder = $event->getBuilder(); + $alias = $event->getMeta()['alias']; + $provider = $event->getMeta()['provider']; + + $id = $modifier->getId(); + $mapper = apply([$this->id, 'generate'], $provider); + + $builder + ->where($modifier->isMultiple() ? "{$alias} in (:id)" : "{$alias} = :id") + ->setParameter(':id', $modifier->isMultiple() ? array_map($mapper, $id) : $mapper($id)); + ; + } +} diff --git a/src/Handlers/ModifierHandler.php b/src/Handlers/ModifierHandler.php new file mode 100644 index 0000000..b4e015e --- /dev/null +++ b/src/Handlers/ModifierHandler.php @@ -0,0 +1,10 @@ +offset = $offset; + $this->count = $count; + } + + public function getOffset() + { + return $this->offset; + } + + public function getCount() + { + return $this->count; + } + + public static function count(int $count) + { + return new static(0, $count); + } +} diff --git a/src/Modifiers/Modifier.php b/src/Modifiers/Modifier.php new file mode 100644 index 0000000..a8ddd7c --- /dev/null +++ b/src/Modifiers/Modifier.php @@ -0,0 +1,8 @@ +id = $id instanceof \Traversable ? iterator_to_array($id) : $id; + } + + public function getId() + { + return $this->id; + } + + public function isMultiple() + { + return is_array($this->id); + } +} diff --git a/src/Provider/Database/GenericLineRepository.php b/src/Provider/Database/GenericLineRepository.php index e9bab49..66d3cc0 100644 --- a/src/Provider/Database/GenericLineRepository.php +++ b/src/Provider/Database/GenericLineRepository.php @@ -3,8 +3,15 @@ namespace App\Provider\Database; use App\Entity\LineEntity; +use App\Event\HandleDatabaseModifierEvent; +use App\Handlers\Database\LimitDatabaseHandler; +use App\Handlers\Database\WithIdDatabaseHandler; +use App\Handlers\ModifierHandler; use App\Model\Line; +use App\Modifiers\Limit; +use App\Modifiers\WithId; use App\Provider\LineRepository; +use App\Modifiers\Modifier; use Tightenco\Collect\Support\Collection; use Kadet\Functional as f; @@ -12,25 +19,52 @@ class GenericLineRepository extends DatabaseRepository implements LineRepository { public function getAll(): Collection { - $repository = $this->em->getRepository(LineEntity::class); - $lines = $repository->findAll(); - - return collect($lines)->map(f\ref([$this, 'convert'])); + return $this->all(); } public function getById($id): ?Line { - $repository = $this->em->getRepository(LineEntity::class); - return $this->convert($repository->find($id)); + return $this->first(new WithId($id)); } public function getManyById($ids): Collection { - $ids = collect($ids)->map(f\apply(f\ref([$this->id, 'generate']), $this->provider)); - - $repository = $this->em->getRepository(LineEntity::class); - $lines = $repository->findBy(['id' => $ids->all()]); - - return collect($lines)->map(f\ref([$this, 'convert'])); + return $this->all(new WithId($ids)); } -} \ No newline at end of file + + public function first(Modifier ...$modifiers) + { + return $this->all(Limit::count(1), ...$modifiers)->first(); + } + + public function all(Modifier ...$modifiers) + { + $builder = $this->em + ->createQueryBuilder() + ->from(LineEntity::class, 'line') + ->select('line') + ; + + foreach ($modifiers as $modifier) { + $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, [ + 'alias' => 'line', + 'provider' => $this->provider, + ]); + + $handler = $this->getHandlers()[get_class($modifier)]; + + $handler->process($event); + } + + return collect($builder->getQuery()->execute())->map(f\ref([$this, 'convert'])); + } + + /** @return ModifierHandler[] */ + private function getHandlers() + { + return [ + WithId::class => new WithIdDatabaseHandler($this->id), + Limit::class => new LimitDatabaseHandler(), + ]; + } +} diff --git a/src/Provider/FluentRepository.php b/src/Provider/FluentRepository.php new file mode 100644 index 0000000..e24545d --- /dev/null +++ b/src/Provider/FluentRepository.php @@ -0,0 +1,11 @@ + Date: Wed, 12 Feb 2020 20:08:56 +0100 Subject: [PATCH 02/16] Add service subscriber to database repository --- .../UnsupportedModifierException.php | 14 +++++ ...andler.php => IdFilterDatabaseHandler.php} | 7 +-- src/Modifiers/{WithId.php => IdFilter.php} | 2 +- src/Provider/Database/DatabaseRepository.php | 61 +++++++++++++++++-- .../Database/GenericLineRepository.php | 29 ++++----- .../Database/GenericOperatorRepository.php | 7 ++- .../Database/GenericScheduleRepository.php | 5 ++ .../Database/GenericStopRepository.php | 5 ++ .../Database/GenericTrackRepository.php | 7 ++- .../Database/GenericTripRepository.php | 5 ++ 10 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 src/Exception/UnsupportedModifierException.php rename src/Handlers/Database/{WithIdDatabaseHandler.php => IdFilterDatabaseHandler.php} (86%) rename src/Modifiers/{WithId.php => IdFilter.php} (94%) diff --git a/src/Exception/UnsupportedModifierException.php b/src/Exception/UnsupportedModifierException.php new file mode 100644 index 0000000..4dbd2bf --- /dev/null +++ b/src/Exception/UnsupportedModifierException.php @@ -0,0 +1,14 @@ +getModifier(); $builder = $event->getBuilder(); $alias = $event->getMeta()['alias']; diff --git a/src/Modifiers/WithId.php b/src/Modifiers/IdFilter.php similarity index 94% rename from src/Modifiers/WithId.php rename to src/Modifiers/IdFilter.php index 883db0f..e2f570c 100644 --- a/src/Modifiers/WithId.php +++ b/src/Modifiers/IdFilter.php @@ -5,7 +5,7 @@ namespace App\Modifiers; use App\Exception\InvalidOptionException; use App\Modifiers\Modifier; -class WithId implements Modifier +class IdFilter implements Modifier { /** @var string|array */ private $id; diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index 3a97eff..4f4dce3 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -2,15 +2,19 @@ namespace App\Provider\Database; -use App\Entity\Entity; use App\Entity\ProviderEntity; +use App\Event\HandleDatabaseModifierEvent; +use App\Exception\UnsupportedModifierException; use App\Model\Referable; +use App\Provider\Repository; use App\Service\Converter; use App\Service\IdUtils; use Doctrine\ORM\EntityManagerInterface; -use Kadet\Functional as f; +use Doctrine\ORM\QueryBuilder; +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; -class DatabaseRepository +abstract class DatabaseRepository implements ServiceSubscriberInterface, Repository { /** @var EntityManagerInterface */ protected $em; @@ -24,22 +28,30 @@ class DatabaseRepository /** @var Converter */ protected $converter; + /** @var ContainerInterface */ + protected $handlers; + /** * DatabaseRepository constructor. * * @param EntityManagerInterface $em */ - public function __construct(EntityManagerInterface $em, IdUtils $id, Converter $converter) - { + public function __construct( + EntityManagerInterface $em, + IdUtils $id, + Converter $converter, + ContainerInterface $handlers + ) { $this->em = $em; $this->id = $id; $this->converter = $converter; + $this->handlers = $handlers; } /** @return static */ public function withProvider(ProviderEntity $provider) { - $result = clone $this; + $result = clone $this; $result->provider = $provider; return $result; @@ -56,4 +68,41 @@ class DatabaseRepository return $this->em->getReference($class, $id); } + + protected function processQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = []) + { + foreach ($modifiers as $modifier) { + $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([ + 'provider' => $this->provider, + ], $meta)); + + $class = get_class($modifier); + + if (!$this->handlers->has($class)) { + throw UnsupportedModifierException::createFromModifier($modifier, $this); + } + + $handler = $this->handlers->get($class); + + $handler->process($event); + } + } + + /** + * Returns array describing handlers for each modifier type. Syntax is as follows: + * [ IdFilter::class => IdFilterDatabaseHandler::class ] + * + * It is internally used as part of service subscriber. + * + * @return array + */ + protected abstract static function getHandlers(); + + /** + * @inheritDoc + */ + public static function getSubscribedServices() + { + return static::getHandlers(); + } } diff --git a/src/Provider/Database/GenericLineRepository.php b/src/Provider/Database/GenericLineRepository.php index 66d3cc0..6982cb1 100644 --- a/src/Provider/Database/GenericLineRepository.php +++ b/src/Provider/Database/GenericLineRepository.php @@ -5,11 +5,11 @@ namespace App\Provider\Database; use App\Entity\LineEntity; use App\Event\HandleDatabaseModifierEvent; use App\Handlers\Database\LimitDatabaseHandler; -use App\Handlers\Database\WithIdDatabaseHandler; +use App\Handlers\Database\IdFilterDatabaseHandler; use App\Handlers\ModifierHandler; use App\Model\Line; use App\Modifiers\Limit; -use App\Modifiers\WithId; +use App\Modifiers\IdFilter; use App\Provider\LineRepository; use App\Modifiers\Modifier; use Tightenco\Collect\Support\Collection; @@ -24,12 +24,12 @@ class GenericLineRepository extends DatabaseRepository implements LineRepository public function getById($id): ?Line { - return $this->first(new WithId($id)); + return $this->first(new IdFilter($id)); } public function getManyById($ids): Collection { - return $this->all(new WithId($ids)); + return $this->all(new IdFilter($ids)); } public function first(Modifier ...$modifiers) @@ -45,26 +45,21 @@ class GenericLineRepository extends DatabaseRepository implements LineRepository ->select('line') ; - foreach ($modifiers as $modifier) { - $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, [ - 'alias' => 'line', - 'provider' => $this->provider, - ]); - - $handler = $this->getHandlers()[get_class($modifier)]; - - $handler->process($event); - } + $this->processQueryBuilder($builder, $modifiers, [ + 'alias' => 'line', + 'entity' => LineEntity::class, + 'type' => Line::class, + ]); return collect($builder->getQuery()->execute())->map(f\ref([$this, 'convert'])); } /** @return ModifierHandler[] */ - private function getHandlers() + protected static function getHandlers() { return [ - WithId::class => new WithIdDatabaseHandler($this->id), - Limit::class => new LimitDatabaseHandler(), + IdFilter::class => IdFilterDatabaseHandler::class, + Limit::class => LimitDatabaseHandler::class, ]; } } diff --git a/src/Provider/Database/GenericOperatorRepository.php b/src/Provider/Database/GenericOperatorRepository.php index 946eff6..929d994 100644 --- a/src/Provider/Database/GenericOperatorRepository.php +++ b/src/Provider/Database/GenericOperatorRepository.php @@ -30,4 +30,9 @@ class GenericOperatorRepository extends DatabaseRepository implements OperatorRe return collect($operators); } -} \ No newline at end of file + + protected static function getHandlers() + { + return []; + } +} diff --git a/src/Provider/Database/GenericScheduleRepository.php b/src/Provider/Database/GenericScheduleRepository.php index 551c288..e1c2c29 100644 --- a/src/Provider/Database/GenericScheduleRepository.php +++ b/src/Provider/Database/GenericScheduleRepository.php @@ -70,4 +70,9 @@ class GenericScheduleRepository extends DatabaseRepository implements ScheduleRe ]); }); } + + protected static function getHandlers() + { + return []; + } } diff --git a/src/Provider/Database/GenericStopRepository.php b/src/Provider/Database/GenericStopRepository.php index 8dc1972..fe7b017 100644 --- a/src/Provider/Database/GenericStopRepository.php +++ b/src/Provider/Database/GenericStopRepository.php @@ -83,4 +83,9 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository $stop->setDestinations($destinations[$this->id->generate($this->provider, $stop->getId())]); }); } + + protected static function getHandlers() + { + return []; + } } diff --git a/src/Provider/Database/GenericTrackRepository.php b/src/Provider/Database/GenericTrackRepository.php index 9a25d44..53bd235 100644 --- a/src/Provider/Database/GenericTrackRepository.php +++ b/src/Provider/Database/GenericTrackRepository.php @@ -64,4 +64,9 @@ class GenericTrackRepository extends DatabaseRepository implements TrackReposito return collect($tracks)->map(f\ref([$this, 'convert'])); } -} \ No newline at end of file + + protected static function getHandlers() + { + return []; + } +} diff --git a/src/Provider/Database/GenericTripRepository.php b/src/Provider/Database/GenericTripRepository.php index e85f178..3944e18 100644 --- a/src/Provider/Database/GenericTripRepository.php +++ b/src/Provider/Database/GenericTripRepository.php @@ -25,4 +25,9 @@ class GenericTripRepository extends DatabaseRepository implements TripRepository return $this->convert($trip); } + + protected static function getHandlers() + { + return []; + } } From 67f7ba2a88ff5aec628a35beefb47810dae73f0a Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Wed, 12 Feb 2020 20:16:26 +0100 Subject: [PATCH 03/16] Remove unnecessary methods from LineRepository --- src/Provider/Database/DatabaseRepository.php | 5 ++++- src/Provider/Database/GenericLineRepository.php | 17 +---------------- src/Provider/FluentRepository.php | 3 ++- src/Provider/LineRepository.php | 8 -------- .../ZtmGdansk/ZtmGdanskDepartureRepository.php | 4 +++- 5 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index 4f4dce3..bcc596b 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -96,7 +96,10 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit * * @return array */ - protected abstract static function getHandlers(); + protected static function getHandlers() + { + return []; + } /** * @inheritDoc diff --git a/src/Provider/Database/GenericLineRepository.php b/src/Provider/Database/GenericLineRepository.php index 6982cb1..244b8e0 100644 --- a/src/Provider/Database/GenericLineRepository.php +++ b/src/Provider/Database/GenericLineRepository.php @@ -17,27 +17,12 @@ use Kadet\Functional as f; class GenericLineRepository extends DatabaseRepository implements LineRepository { - public function getAll(): Collection - { - return $this->all(); - } - - public function getById($id): ?Line - { - return $this->first(new IdFilter($id)); - } - - public function getManyById($ids): Collection - { - return $this->all(new IdFilter($ids)); - } - public function first(Modifier ...$modifiers) { return $this->all(Limit::count(1), ...$modifiers)->first(); } - public function all(Modifier ...$modifiers) + public function all(Modifier ...$modifiers): Collection { $builder = $this->em ->createQueryBuilder() diff --git a/src/Provider/FluentRepository.php b/src/Provider/FluentRepository.php index e24545d..d0644fe 100644 --- a/src/Provider/FluentRepository.php +++ b/src/Provider/FluentRepository.php @@ -3,9 +3,10 @@ namespace App\Provider; use App\Modifiers\Modifier; +use Tightenco\Collect\Support\Collection; interface FluentRepository extends Repository { public function first(Modifier ...$modifiers); - public function all(Modifier ...$modifiers); + public function all(Modifier ...$modifiers): Collection; } diff --git a/src/Provider/LineRepository.php b/src/Provider/LineRepository.php index 80bebbd..2ab6884 100644 --- a/src/Provider/LineRepository.php +++ b/src/Provider/LineRepository.php @@ -3,14 +3,6 @@ namespace App\Provider; - -use App\Model\Line; -use Tightenco\Collect\Support\Collection; - interface LineRepository extends FluentRepository { - public function getAll(): Collection; - - public function getById($id): ?Line; - public function getManyById($ids): Collection; } diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index 0e956bd..165e571 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -6,6 +6,7 @@ use App\Model\Departure; use App\Model\Line; use App\Model\Stop; use App\Model\Vehicle; +use App\Modifiers\IdFilter; use App\Provider\Database\GenericScheduleRepository; use App\Provider\DepartureRepository; use App\Provider\LineRepository; @@ -65,7 +66,8 @@ class ZtmGdanskDepartureRepository implements DepartureRepository $lines = $estimates->map(function ($delay) { return $delay['routeId']; })->unique(); - $lines = $this->lines->getManyById($lines)->keyBy(t\property('id')); + + $lines = $this->lines->all(new IdFilter($lines))->keyBy(t\property('id')); return collect($estimates)->map(function ($delay) use ($stop, $lines) { $scheduled = (new Carbon($delay['theoreticalTime'], 'Europe/Warsaw'))->tz('UTC'); From 847e3a078f782d7c9af2ffde2a4fcf34bd18254d Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 16 Feb 2020 21:07:16 +0100 Subject: [PATCH 04/16] Make StopRepository more fluent --- config/services.yaml | 2 +- resources/ts/components/picker.ts | 2 +- resources/ts/urls.ts | 6 +- .../Api/v1/DeparturesController.php | 5 +- src/Controller/Api/v1/StopsController.php | 51 +++++++---- src/Event/HandleDatabaseModifierEvent.php | 2 +- src/Event/HandleModifierEvent.php | 2 +- src/Event/PostProcessEvent.php | 27 ++++++ src/Exception/NonExistentServiceException.php | 5 +- src/Exception/NotSupportedException.php | 4 +- .../UnsupportedModifierException.php | 4 +- .../Database/FieldFilterDatabaseHandler.php | 52 +++++++++++ .../Database/IdFilterDatabaseHandler.php | 6 +- .../IncludeDestinationsDatabaseHandler.php | 76 ++++++++++++++++ .../Database/LimitDatabaseHandler.php | 6 +- src/{Handlers => Handler}/ModifierHandler.php | 2 +- src/Handler/PostProcessingHandler.php | 11 +++ src/Modifier/FieldFilter.php | 37 ++++++++ src/{Modifiers => Modifier}/IdFilter.php | 4 +- src/Modifier/IncludeDestinations.php | 7 ++ src/{Modifiers => Modifier}/Limit.php | 2 +- src/{Modifiers => Modifier}/Modifier.php | 2 +- src/Provider/Database/DatabaseRepository.php | 74 ++++++++++++--- .../Database/GenericLineRepository.php | 30 ++----- .../Database/GenericStopRepository.php | 89 ++++--------------- src/Provider/Dummy/DummyStopRepository.php | 11 +++ src/Provider/FluentRepository.php | 2 +- src/Provider/StopRepository.php | 6 +- .../ZtmGdanskDepartureRepository.php | 2 +- src/Service/IdUtils.php | 3 +- 30 files changed, 374 insertions(+), 158 deletions(-) create mode 100644 src/Event/PostProcessEvent.php create mode 100644 src/Handler/Database/FieldFilterDatabaseHandler.php rename src/{Handlers => Handler}/Database/IdFilterDatabaseHandler.php (91%) create mode 100644 src/Handler/Database/IncludeDestinationsDatabaseHandler.php rename src/{Handlers => Handler}/Database/LimitDatabaseHandler.php (85%) rename src/{Handlers => Handler}/ModifierHandler.php (84%) create mode 100644 src/Handler/PostProcessingHandler.php create mode 100644 src/Modifier/FieldFilter.php rename src/{Modifiers => Modifier}/IdFilter.php (91%) create mode 100644 src/Modifier/IncludeDestinations.php rename src/{Modifiers => Modifier}/Limit.php (95%) rename src/{Modifiers => Modifier}/Modifier.php (56%) diff --git a/config/services.yaml b/config/services.yaml index 853ef0a..4337891 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,7 +23,7 @@ services: # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' - exclude: '../src/{DependencyInjection,Entity,Model,Migrations,Tests,Functions,Kernel.php}' + exclude: '../src/{DependencyInjection,Exception,Modifiers,Entity,Model,Migrations,Tests,Functions,Kernel.php}' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class diff --git a/resources/ts/components/picker.ts b/resources/ts/components/picker.ts index 476d07a..93493d1 100644 --- a/resources/ts/components/picker.ts +++ b/resources/ts/components/picker.ts @@ -80,7 +80,7 @@ export class FinderComponent extends Vue { this.state = 'fetching'; - const response = await fetch(urls.prepare(urls.stops.grouped, { name: this.filter })); + const response = await fetch(urls.prepare(urls.stops.grouped, { name: this.filter, 'include-destinations': true })); if (response.ok) { this.found = (await response.json()).reduce((accumulator, { name, stops }) => Object.assign(accumulator, { [name]: stops }), {}); diff --git a/resources/ts/urls.ts b/resources/ts/urls.ts index e3bb5d1..106a0a1 100644 --- a/resources/ts/urls.ts +++ b/resources/ts/urls.ts @@ -8,7 +8,11 @@ export function query(params: UrlParams = { }) { function *simplify(name: string, param: any): IterableIterator { if (typeof param === 'string') { yield [ name, param ]; - } else if (typeof param === 'number') { + } else if (typeof param === 'boolean') { + if (param) { + yield [ name, '1' ]; + } + } else if (typeof param === 'number') { yield [ name, param.toString() ]; } else if (param instanceof Array) { for (let entry of param) { diff --git a/src/Controller/Api/v1/DeparturesController.php b/src/Controller/Api/v1/DeparturesController.php index 410a753..da07d63 100644 --- a/src/Controller/Api/v1/DeparturesController.php +++ b/src/Controller/Api/v1/DeparturesController.php @@ -5,6 +5,7 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; use App\Model\Departure; +use App\Modifier\IdFilter; use App\Provider\DepartureRepository; use App\Provider\StopRepository; use App\Service\SerializerContextFactory; @@ -34,7 +35,7 @@ class DeparturesController extends Controller */ public function stop(DepartureRepository $departures, StopRepository $stops, $stop) { - $stop = $stops->getById($stop); + $stop = $stops->first(new IdFilter($stop)); return $this->json($departures->getForStop($stop)); } @@ -65,7 +66,7 @@ class DeparturesController extends Controller public function stops(DepartureRepository $departures, StopRepository $stops, Request $request) { $stops = $stops - ->getManyById($request->query->get('stop')) + ->all(new IdFilter($request->query->get('stop'))) ->flatMap(ref([ $departures, 'getForStop' ])) ->sortBy(property('departure')); diff --git a/src/Controller/Api/v1/StopsController.php b/src/Controller/Api/v1/StopsController.php index 76c19a1..0fca3e4 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -7,6 +7,9 @@ use App\Controller\Controller; use App\Model\Stop; use App\Model\Track; use App\Model\StopGroup; +use App\Modifier\IdFilter; +use App\Modifier\FieldFilter; +use App\Modifier\IncludeDestinations; use App\Provider\StopRepository; use App\Provider\TrackRepository; use App\Service\Proxy\ReferenceFactory; @@ -46,16 +49,9 @@ class StopsController extends Controller */ public function index(Request $request, StopRepository $stops) { - switch (true) { - case $request->query->has('id'): - $result = $stops->getManyById($request->query->get('id')); - break; + $modifiers = $this->getModifiersFromRequest($request); - default: - $result = $stops->getAll(); - } - - return $this->json($result->all()); + return $this->json($stops->all(...$modifiers)->toArray()); } /** @@ -76,16 +72,9 @@ class StopsController extends Controller */ public function groups(Request $request, StopRepository $stops) { - switch (true) { - case $request->query->has('name'): - $result = $stops->findByName($request->query->get('name')); - break; + $modifiers = $this->getModifiersFromRequest($request); - default: - $result = $stops->getAll(); - } - - return $this->json(static::group($result)->all()); + return $this->json(static::group($stops->all(...$modifiers))->toArray()); } /** @@ -106,7 +95,7 @@ class StopsController extends Controller */ public function one(Request $request, StopRepository $stops, $id) { - return $this->json($stops->getById($id)); + return $this->json($stops->first(new IdFilter($id), new IncludeDestinations())); } /** @@ -145,4 +134,28 @@ class StopsController extends Controller return $group; })->values(); } + + /** + * @param Request $request + * + * @return array + */ + private function getModifiersFromRequest(Request $request): array + { + $modifiers = []; + + if ($request->query->has('name')) { + $modifiers[] = FieldFilter::contains('name', $request->query->get('name')); + } + + if ($request->query->has('id')) { + $modifiers[] = new IdFilter($request->query->get('id')); + } + + if ($request->query->has('include-destinations')) { + $modifiers[] = new IncludeDestinations(); + } + + return $modifiers; + } } diff --git a/src/Event/HandleDatabaseModifierEvent.php b/src/Event/HandleDatabaseModifierEvent.php index 74bbc81..c548d59 100644 --- a/src/Event/HandleDatabaseModifierEvent.php +++ b/src/Event/HandleDatabaseModifierEvent.php @@ -3,7 +3,7 @@ namespace App\Event; use App\Event\HandleModifierEvent; -use App\Modifiers\Modifier; +use App\Modifier\Modifier; use App\Provider\Repository; use Doctrine\ORM\QueryBuilder; diff --git a/src/Event/HandleModifierEvent.php b/src/Event/HandleModifierEvent.php index af28988..5544af4 100644 --- a/src/Event/HandleModifierEvent.php +++ b/src/Event/HandleModifierEvent.php @@ -2,7 +2,7 @@ namespace App\Event; -use App\Modifiers\Modifier; +use App\Modifier\Modifier; use App\Provider\Repository; class HandleModifierEvent diff --git a/src/Event/PostProcessEvent.php b/src/Event/PostProcessEvent.php new file mode 100644 index 0000000..5d23040 --- /dev/null +++ b/src/Event/PostProcessEvent.php @@ -0,0 +1,27 @@ +data = $data; + } + + public function getData() + { + return $this->data; + } + + public function setData($data): void + { + $this->data = $data; + } +} diff --git a/src/Exception/NonExistentServiceException.php b/src/Exception/NonExistentServiceException.php index c2c6ba3..b19aef2 100644 --- a/src/Exception/NonExistentServiceException.php +++ b/src/Exception/NonExistentServiceException.php @@ -4,7 +4,6 @@ namespace App\Exception; -class NonExistentServiceException extends \Exception +class NonExistentServiceException extends \LogicException { - -} \ No newline at end of file +} diff --git a/src/Exception/NotSupportedException.php b/src/Exception/NotSupportedException.php index 4ab7b89..1af7250 100644 --- a/src/Exception/NotSupportedException.php +++ b/src/Exception/NotSupportedException.php @@ -2,6 +2,6 @@ namespace App\Exception; -class NotSupportedException extends \RuntimeException +class NotSupportedException extends \LogicException { -} \ No newline at end of file +} diff --git a/src/Exception/UnsupportedModifierException.php b/src/Exception/UnsupportedModifierException.php index 4dbd2bf..f1e7145 100644 --- a/src/Exception/UnsupportedModifierException.php +++ b/src/Exception/UnsupportedModifierException.php @@ -2,10 +2,10 @@ namespace App\Exception; -use App\Modifiers\Modifier; +use App\Modifier\Modifier; use App\Provider\Repository; -class UnsupportedModifierException extends \Exception +class UnsupportedModifierException extends \LogicException { public static function createFromModifier(Modifier $modifier, Repository $repository) { diff --git a/src/Handler/Database/FieldFilterDatabaseHandler.php b/src/Handler/Database/FieldFilterDatabaseHandler.php new file mode 100644 index 0000000..cb77a66 --- /dev/null +++ b/src/Handler/Database/FieldFilterDatabaseHandler.php @@ -0,0 +1,52 @@ + [ + 'name' => 'name', + ], + ]; + + public function process(HandleModifierEvent $event) + { + if (!$event instanceof HandleDatabaseModifierEvent) { + return; + } + + /** @var FieldFilter $modifier */ + $modifier = $event->getModifier(); + $builder = $event->getBuilder(); + $alias = $event->getMeta()['alias']; + + $field = $this->mapFieldName($event->getMeta()['type'], $modifier->getField()); + $operator = $modifier->getOperator(); + $value = $modifier->getValue(); + + $parameter = sprintf(":%s_%s", $alias, $field); + + $builder + ->where(sprintf("%s.%s %s %s", $alias, $field, $operator, $parameter)) + ->setParameter($parameter, $value) + ; + } + + protected function mapFieldName(string $class, string $field) + { + if (!isset($this->mapping[$class][$field])) { + throw new \InvalidArgumentException( + sprintf("Unable to map field %s of %s into entity field.", $field, $class) + ); + } + + return $this->mapping[$class][$field]; + } +} diff --git a/src/Handlers/Database/IdFilterDatabaseHandler.php b/src/Handler/Database/IdFilterDatabaseHandler.php similarity index 91% rename from src/Handlers/Database/IdFilterDatabaseHandler.php rename to src/Handler/Database/IdFilterDatabaseHandler.php index 58a89db..10f124c 100644 --- a/src/Handlers/Database/IdFilterDatabaseHandler.php +++ b/src/Handler/Database/IdFilterDatabaseHandler.php @@ -1,9 +1,9 @@ em = $entityManager; + $this->converter = $converter; + $this->id = $id; + } + + public function process(PostProcessEvent $event) + { + $provider = $event->getMeta()['provider']; + $stops = $event + ->getData() + ->map(t\property('id')) + ->map(f\apply([$this->id, 'generate'], $provider)) + ->all(); + + $destinations = collect($this->em->createQueryBuilder() + ->select('t', 'tl', 'f', 'fs', 'ts') + ->from(TrackEntity::class, 't') + ->join('t.stopsInTrack', 'ts') + ->join('t.line', 'tl') + ->where('ts.stop IN (:stops)') + ->join('t.final', 'f') + ->join('f.stop', 'fs') + ->getQuery() + ->execute(['stops' => $stops])) + ->reduce(function ($grouped, TrackEntity $track) { + foreach ($track->getStopsInTrack()->map(t\property('stop'))->map(t\property('id')) as $stop) { + $grouped[$stop] = ($grouped[$stop] ?? collect())->add($track); + } + + return $grouped; + }, collect()) + ->map(function (Collection $tracks) { + return $tracks + ->groupBy(function (TrackEntity $track) { + return $track->getFinal()->getStop()->getId(); + })->map(function (Collection $tracks, $id) { + return Destination::createFromArray([ + 'stop' => $this->converter->convert($tracks->first()->getFinal()->getStop()), + 'lines' => $tracks + ->map(t\property('line')) + ->unique(t\property('id')) + ->map(f\ref([$this->converter, 'convert'])) + ->values(), + ]); + })->values(); + }); + + $event->getData()->each(function (Stop $stop) use ($provider, $destinations) { + $stop->setDestinations($destinations[$this->id->generate($provider, $stop->getId())]); + }); + } +} diff --git a/src/Handlers/Database/LimitDatabaseHandler.php b/src/Handler/Database/LimitDatabaseHandler.php similarity index 85% rename from src/Handlers/Database/LimitDatabaseHandler.php rename to src/Handler/Database/LimitDatabaseHandler.php index 202ddce..3f445a9 100644 --- a/src/Handlers/Database/LimitDatabaseHandler.php +++ b/src/Handler/Database/LimitDatabaseHandler.php @@ -1,11 +1,11 @@ field = $field; + $this->value = $value; + $this->operator = $operator; + } + + public static function contains(string $field, string $value) + { + return new static($field, "%$value%", 'LIKE'); + } + + public function getField(): string + { + return $this->field; + } + + public function getValue() + { + return $this->value; + } + + public function getOperator(): string + { + return $this->operator; + } +} diff --git a/src/Modifiers/IdFilter.php b/src/Modifier/IdFilter.php similarity index 91% rename from src/Modifiers/IdFilter.php rename to src/Modifier/IdFilter.php index e2f570c..f7b6a13 100644 --- a/src/Modifiers/IdFilter.php +++ b/src/Modifier/IdFilter.php @@ -1,9 +1,9 @@ $this->provider, - ], $meta)); + $handler = $this->getHandler($modifier); - $class = get_class($modifier); + switch (true) { + case $handler instanceof PostProcessingHandler: + $reducers[] = function ($result) use ($meta, $modifier, $handler) { + $event = new PostProcessEvent($result, $modifier, $this, array_merge([ + 'provider' => $this->provider, + ], $meta)); - if (!$this->handlers->has($class)) { - throw UnsupportedModifierException::createFromModifier($modifier, $this); + $handler->process($event); + + return $event->getData(); + }; + break; + + default: + $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([ + 'provider' => $this->provider, + ], $meta)); + + $handler->process($event); + break; } - - $handler = $this->handlers->get($class); - - $handler->process($event); } + + return collect($reducers); + } + + protected function allFromQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = []) + { + $reducers = $this->processQueryBuilder($builder, $modifiers, $meta); + + return $reducers->reduce(function ($result, $reducer) { + return $reducer($result); + }, collect($builder->getQuery()->execute())->map(\Closure::fromCallable([$this, 'convert']))); + } + + public function first(Modifier ...$modifiers) + { + return $this->all(Limit::count(1), ...$modifiers)->first(); + } + + protected function getHandler(Modifier $modifier) + { + $class = get_class($modifier); + + if (!$this->handlers->has($class)) { + throw UnsupportedModifierException::createFromModifier($modifier, $this); + } + + return $this->handlers->get($class); } /** @@ -106,6 +154,10 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit */ public static function getSubscribedServices() { - return static::getHandlers(); + return array_merge([ + IdFilter::class => IdFilterDatabaseHandler::class, + Limit::class => LimitDatabaseHandler::class, + FieldFilter::class => FieldFilterDatabaseHandler::class, + ], static::getHandlers()); } } diff --git a/src/Provider/Database/GenericLineRepository.php b/src/Provider/Database/GenericLineRepository.php index 244b8e0..9ca147d 100644 --- a/src/Provider/Database/GenericLineRepository.php +++ b/src/Provider/Database/GenericLineRepository.php @@ -4,24 +4,19 @@ namespace App\Provider\Database; use App\Entity\LineEntity; use App\Event\HandleDatabaseModifierEvent; -use App\Handlers\Database\LimitDatabaseHandler; -use App\Handlers\Database\IdFilterDatabaseHandler; -use App\Handlers\ModifierHandler; +use App\Handler\Database\LimitDatabaseHandler; +use App\Handler\Database\IdFilterDatabaseHandler; +use App\Handler\ModifierHandler; use App\Model\Line; -use App\Modifiers\Limit; -use App\Modifiers\IdFilter; +use App\Modifier\Limit; +use App\Modifier\IdFilter; use App\Provider\LineRepository; -use App\Modifiers\Modifier; +use App\Modifier\Modifier; use Tightenco\Collect\Support\Collection; use Kadet\Functional as f; class GenericLineRepository extends DatabaseRepository implements LineRepository { - public function first(Modifier ...$modifiers) - { - return $this->all(Limit::count(1), ...$modifiers)->first(); - } - public function all(Modifier ...$modifiers): Collection { $builder = $this->em @@ -30,21 +25,10 @@ class GenericLineRepository extends DatabaseRepository implements LineRepository ->select('line') ; - $this->processQueryBuilder($builder, $modifiers, [ + return $this->allFromQueryBuilder($builder, $modifiers, [ 'alias' => 'line', 'entity' => LineEntity::class, 'type' => Line::class, ]); - - return collect($builder->getQuery()->execute())->map(f\ref([$this, 'convert'])); - } - - /** @return ModifierHandler[] */ - protected static function getHandlers() - { - return [ - IdFilter::class => IdFilterDatabaseHandler::class, - Limit::class => LimitDatabaseHandler::class, - ]; } } diff --git a/src/Provider/Database/GenericStopRepository.php b/src/Provider/Database/GenericStopRepository.php index fe7b017..62a7a90 100644 --- a/src/Provider/Database/GenericStopRepository.php +++ b/src/Provider/Database/GenericStopRepository.php @@ -3,89 +3,34 @@ namespace App\Provider\Database; use App\Entity\StopEntity; -use App\Entity\TrackEntity; -use App\Model\Destination; +use App\Handler\Database\IncludeDestinationsDatabaseHandler; use App\Model\Stop; +use App\Modifier\Modifier; +use App\Modifier\IncludeDestinations; use App\Provider\StopRepository; -use Kadet\Functional as f; -use Kadet\Functional\Transforms as t; use Tightenco\Collect\Support\Collection; class GenericStopRepository extends DatabaseRepository implements StopRepository { - public function getAll(): Collection + public function all(Modifier ...$modifiers): Collection { - $stops = $this->em->getRepository(StopEntity::class)->findAll(); + $builder = $this->em + ->createQueryBuilder() + ->from(StopEntity::class, 'stop') + ->select('stop') + ; - return collect($stops)->map(f\ref([$this, 'convert'])); - } - - public function getById($id): ?Stop - { - $id = $this->id->generate($this->provider, $id); - $stop = $this->em->getRepository(StopEntity::class)->find($id); - - return $this->convert($stop); - } - - public function getManyById($ids): Collection - { - $ids = collect($ids)->map(f\apply(f\ref([$this->id, 'generate']), $this->provider)); - $stops = $this->em->getRepository(StopEntity::class)->findBy(['id' => $ids->all()]); - - return collect($stops)->map(f\ref([$this, 'convert'])); - } - - public function findByName(string $name): Collection - { - $query = $this->em->createQueryBuilder() - ->select('s') - ->from(StopEntity::class, 's') - ->where('s.name LIKE :name') - ->getQuery(); - - $stops = collect($query->execute([':name' => "%$name%"])); - - $destinations = collect($this->em->createQueryBuilder() - ->select('t', 'tl', 'f', 'fs', 'ts') - ->from(TrackEntity::class, 't') - ->join('t.stopsInTrack', 'ts') - ->join('t.line', 'tl') - ->where('ts.stop IN (:stops)') - ->join('t.final', 'f') - ->join('f.stop', 'fs') - ->getQuery() - ->execute(['stops' => $stops->map(t\property('id'))->all()])) - ->reduce(function ($grouped, TrackEntity $track) { - foreach ($track->getStopsInTrack()->map(t\property('stop'))->map(t\property('id')) as $stop) { - $grouped[$stop] = ($grouped[$stop] ?? collect())->add($track); - } - - return $grouped; - }, collect()) - ->map(function (Collection $tracks) { - return $tracks - ->groupBy(function (TrackEntity $track) { - return $track->getFinal()->getStop()->getId(); - })->map(function (Collection $tracks, $id) { - return Destination::createFromArray([ - 'stop' => $this->convert($tracks->first()->getFinal()->getStop()), - 'lines' => $tracks - ->map(t\property('line')) - ->unique(t\property('id')) - ->map(f\ref([$this, 'convert'])) - ->values(), - ]); - })->values(); - }); - - return collect($stops)->map(f\ref([$this, 'convert']))->each(function (Stop $stop) use ($destinations) { - $stop->setDestinations($destinations[$this->id->generate($this->provider, $stop->getId())]); - }); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'stop', + 'entity' => StopEntity::class, + 'type' => Stop::class, + ]); } protected static function getHandlers() { - return []; + return array_merge(parent::getHandlers(), [ + IncludeDestinations::class => IncludeDestinationsDatabaseHandler::class, + ]); } } diff --git a/src/Provider/Dummy/DummyStopRepository.php b/src/Provider/Dummy/DummyStopRepository.php index caea318..959d5bd 100644 --- a/src/Provider/Dummy/DummyStopRepository.php +++ b/src/Provider/Dummy/DummyStopRepository.php @@ -3,6 +3,7 @@ namespace App\Provider\Dummy; use App\Model\Stop; +use App\Modifier\Modifier; use App\Provider\StopRepository; use App\Service\Proxy\ReferenceFactory; use Tightenco\Collect\Support\Collection; @@ -41,4 +42,14 @@ class DummyStopRepository implements StopRepository { return collect(); } + + public function first(Modifier ...$modifiers) + { + // TODO: Implement first() method. + } + + public function all(Modifier ...$modifiers): Collection + { + // TODO: Implement all() method. + } } diff --git a/src/Provider/FluentRepository.php b/src/Provider/FluentRepository.php index d0644fe..a02831c 100644 --- a/src/Provider/FluentRepository.php +++ b/src/Provider/FluentRepository.php @@ -2,7 +2,7 @@ namespace App\Provider; -use App\Modifiers\Modifier; +use App\Modifier\Modifier; use Tightenco\Collect\Support\Collection; interface FluentRepository extends Repository diff --git a/src/Provider/StopRepository.php b/src/Provider/StopRepository.php index ea87c03..bf6aa3f 100644 --- a/src/Provider/StopRepository.php +++ b/src/Provider/StopRepository.php @@ -7,10 +7,6 @@ namespace App\Provider; use App\Model\Stop; use Tightenco\Collect\Support\Collection; -interface StopRepository extends Repository +interface StopRepository extends FluentRepository { - public function getAll(): Collection; - public function getById($id): ?Stop; - public function getManyById($ids): Collection; - public function findByName(string $name): Collection; } diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index 165e571..e7f7dc6 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -6,7 +6,7 @@ use App\Model\Departure; use App\Model\Line; use App\Model\Stop; use App\Model\Vehicle; -use App\Modifiers\IdFilter; +use App\Modifier\IdFilter; use App\Provider\Database\GenericScheduleRepository; use App\Provider\DepartureRepository; use App\Provider\LineRepository; diff --git a/src/Service/IdUtils.php b/src/Service/IdUtils.php index 61bc00b..e652ad4 100644 --- a/src/Service/IdUtils.php +++ b/src/Service/IdUtils.php @@ -11,6 +11,7 @@ class IdUtils public function generate(ProviderEntity $provider, $id) { + // todo: use array cache if not fast enough return sprintf('%s%s%s', $provider->getId(), self::DELIMITER, $id); } @@ -23,4 +24,4 @@ class IdUtils { return $this->strip($entity->getId()); } -} \ No newline at end of file +} From 950e310096b0155e53d3cbb2dd9de1bb9a4c2f16 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 16 Feb 2020 21:59:38 +0100 Subject: [PATCH 05/16] Move trip and operator repository to fluent pattern --- src/Controller/Api/v1/TripController.php | 3 +- .../Database/GenericOperatorRepository.php | 37 +++++++------------ .../Database/GenericTripRepository.php | 29 ++++++--------- src/Provider/OperatorRepository.php | 7 +--- src/Provider/TripRepository.php | 3 +- 5 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/Controller/Api/v1/TripController.php b/src/Controller/Api/v1/TripController.php index 87b87cd..031f39a 100644 --- a/src/Controller/Api/v1/TripController.php +++ b/src/Controller/Api/v1/TripController.php @@ -4,6 +4,7 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; use App\Model\Trip; +use App\Modifier\IdFilter; use App\Provider\TripRepository; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -18,7 +19,7 @@ class TripController extends Controller */ public function one($id, TripRepository $repository) { - $trip = $repository->getById($id); + $trip = $repository->all(new IdFilter($id)); return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class)); } diff --git a/src/Provider/Database/GenericOperatorRepository.php b/src/Provider/Database/GenericOperatorRepository.php index 929d994..2af85f7 100644 --- a/src/Provider/Database/GenericOperatorRepository.php +++ b/src/Provider/Database/GenericOperatorRepository.php @@ -2,37 +2,26 @@ namespace App\Provider\Database; +use App\Entity\OperatorEntity; use App\Model\Operator; +use App\Modifier\Modifier; use App\Provider\OperatorRepository; use Tightenco\Collect\Support\Collection; class GenericOperatorRepository extends DatabaseRepository implements OperatorRepository { - public function getAll(): Collection + public function all(Modifier ...$modifiers): Collection { - $repository = $this->em->getRepository(Operator::class); - $operators = $repository->findAll(); + $builder = $this->em + ->createQueryBuilder() + ->from(OperatorEntity::class, 'operator') + ->select('operator') + ; - return collect($operators); - } - - public function getById($id): ?Operator - { - $repository = $this->em->getRepository(Operator::class); - - return $repository->find($id); - } - - public function getManyById($ids): Collection - { - $repository = $this->em->getRepository(Operator::class); - $operators = $repository->findBy(['id' => $ids]); - - return collect($operators); - } - - protected static function getHandlers() - { - return []; + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'operator', + 'entity' => OperatorEntity::class, + 'type' => Operator::class, + ]); } } diff --git a/src/Provider/Database/GenericTripRepository.php b/src/Provider/Database/GenericTripRepository.php index 3944e18..00626ad 100644 --- a/src/Provider/Database/GenericTripRepository.php +++ b/src/Provider/Database/GenericTripRepository.php @@ -4,30 +4,25 @@ namespace App\Provider\Database; use App\Entity\TripEntity; use App\Model\Trip; +use App\Modifier\Modifier; use App\Provider\TripRepository; +use Tightenco\Collect\Support\Collection; class GenericTripRepository extends DatabaseRepository implements TripRepository { - public function getById(string $id): Trip + public function all(Modifier ...$modifiers): Collection { - $id = $this->id->generate($this->provider, $id); - - $trip = $this->em + $builder = $this->em ->createQueryBuilder() - ->from(TripEntity::class, 't') - ->join('t.stops', 'ts') + ->from(TripEntity::class, 'trip') + ->join('trip.stops', 'ts') ->join('ts.stop', 's') - ->select('t', 'ts') - ->where('t.id = :id') - ->getQuery() - ->setParameter('id', $id) - ->getOneOrNullResult(); + ->select('t', 'ts'); - return $this->convert($trip); - } - - protected static function getHandlers() - { - return []; + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'operator', + 'entity' => TripEntity::class, + 'type' => Trip::class, + ]); } } diff --git a/src/Provider/OperatorRepository.php b/src/Provider/OperatorRepository.php index 79f8b20..70d88c7 100644 --- a/src/Provider/OperatorRepository.php +++ b/src/Provider/OperatorRepository.php @@ -7,9 +7,6 @@ namespace App\Provider; use App\Model\Operator; use Tightenco\Collect\Support\Collection; -interface OperatorRepository +interface OperatorRepository extends FluentRepository { - public function getAll(): Collection; - public function getById($id): ?Operator; - public function getManyById($ids): Collection; -} \ No newline at end of file +} diff --git a/src/Provider/TripRepository.php b/src/Provider/TripRepository.php index 4a82521..17459d8 100644 --- a/src/Provider/TripRepository.php +++ b/src/Provider/TripRepository.php @@ -4,7 +4,6 @@ namespace App\Provider; use App\Model\Trip; -interface TripRepository +interface TripRepository extends FluentRepository { - public function getById(string $id): Trip; } From a3de2b244f0a550c2de340d3280f24f4ce0e1062 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 17 Feb 2020 21:48:00 +0100 Subject: [PATCH 06/16] Rewrite Track Repository into fluent pattern --- src/Controller/Api/v1/StopsController.php | 1 + src/Controller/Api/v1/TracksController.php | 11 ++- .../Database/FieldFilterDatabaseHandler.php | 8 +- .../Database/IdFilterDatabaseHandler.php | 2 +- .../IncludeDestinationsDatabaseHandler.php | 2 +- .../RelatedFilterDatabaseGenericHandler.php | 96 +++++++++++++++++++ src/Handler/PostProcessingHandler.php | 3 +- src/Modifier/RelatedFilter.php | 27 ++++++ src/Provider/Database/DatabaseRepository.php | 39 ++++---- .../Database/GenericTrackRepository.php | 49 +++------- .../Database/GenericTripRepository.php | 2 +- src/Provider/TrackRepository.php | 10 +- 12 files changed, 177 insertions(+), 73 deletions(-) create mode 100644 src/Handler/Database/RelatedFilterDatabaseGenericHandler.php create mode 100644 src/Modifier/RelatedFilter.php diff --git a/src/Controller/Api/v1/StopsController.php b/src/Controller/Api/v1/StopsController.php index 0fca3e4..64c4107 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -10,6 +10,7 @@ use App\Model\StopGroup; use App\Modifier\IdFilter; use App\Modifier\FieldFilter; use App\Modifier\IncludeDestinations; +use App\Modifier\RelatedFilter; use App\Provider\StopRepository; use App\Provider\TrackRepository; use App\Service\Proxy\ReferenceFactory; diff --git a/src/Controller/Api/v1/TracksController.php b/src/Controller/Api/v1/TracksController.php index 069a338..6f9fbad 100644 --- a/src/Controller/Api/v1/TracksController.php +++ b/src/Controller/Api/v1/TracksController.php @@ -3,8 +3,11 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; +use App\Model\Line; use App\Model\Stop; use App\Model\Track; +use App\Modifier\IdFilter; +use App\Modifier\RelatedFilter; use App\Provider\TrackRepository; use Nelmio\ApiDocBundle\Annotation\Model; use Swagger\Annotations as SWG; @@ -49,7 +52,7 @@ class TracksController extends Controller { $id = encapsulate($request->query->get('id')); - return $this->json($repository->getManyById($id)); + return $this->json($repository->all(new IdFilter($id))); } private function byStop(Request $request, TrackRepository $repository) @@ -63,8 +66,8 @@ class TracksController extends Controller private function byLine(Request $request, TrackRepository $repository) { $line = $request->query->get('line'); - $line = array_map([Stop::class, 'reference'], encapsulate($line)); + $line = Line::reference($line); - return $this->json($repository->getByLine($line)); + return $this->json($repository->all(new RelatedFilter($line))); } -} \ No newline at end of file +} diff --git a/src/Handler/Database/FieldFilterDatabaseHandler.php b/src/Handler/Database/FieldFilterDatabaseHandler.php index cb77a66..0bfc83c 100644 --- a/src/Handler/Database/FieldFilterDatabaseHandler.php +++ b/src/Handler/Database/FieldFilterDatabaseHandler.php @@ -7,6 +7,7 @@ use App\Event\HandleModifierEvent; use App\Handler\ModifierHandler; use App\Model\Stop; use App\Modifier\FieldFilter; +use function App\Functions\encapsulate; class FieldFilterDatabaseHandler implements ModifierHandler { @@ -33,8 +34,13 @@ class FieldFilterDatabaseHandler implements ModifierHandler $parameter = sprintf(":%s_%s", $alias, $field); + if ($operator === 'in' || $operator === 'not in') { + $parameter = "($parameter)"; + $value = encapsulate($value); + } + $builder - ->where(sprintf("%s.%s %s %s", $alias, $field, $operator, $parameter)) + ->andWhere(sprintf("%s.%s %s %s", $alias, $field, $operator, $parameter)) ->setParameter($parameter, $value) ; } diff --git a/src/Handler/Database/IdFilterDatabaseHandler.php b/src/Handler/Database/IdFilterDatabaseHandler.php index 10f124c..9f95d94 100644 --- a/src/Handler/Database/IdFilterDatabaseHandler.php +++ b/src/Handler/Database/IdFilterDatabaseHandler.php @@ -37,7 +37,7 @@ class IdFilterDatabaseHandler implements ModifierHandler $mapper = apply([$this->id, 'generate'], $provider); $builder - ->where($modifier->isMultiple() ? "{$alias} in (:id)" : "{$alias} = :id") + ->andWhere($modifier->isMultiple() ? "{$alias} in (:id)" : "{$alias} = :id") ->setParameter(':id', $modifier->isMultiple() ? array_map($mapper, $id) : $mapper($id)); ; } diff --git a/src/Handler/Database/IncludeDestinationsDatabaseHandler.php b/src/Handler/Database/IncludeDestinationsDatabaseHandler.php index 4d5e1f2..b58287f 100644 --- a/src/Handler/Database/IncludeDestinationsDatabaseHandler.php +++ b/src/Handler/Database/IncludeDestinationsDatabaseHandler.php @@ -27,7 +27,7 @@ class IncludeDestinationsDatabaseHandler implements PostProcessingHandler $this->id = $id; } - public function process(PostProcessEvent $event) + public function postProcess(PostProcessEvent $event) { $provider = $event->getMeta()['provider']; $stops = $event diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php new file mode 100644 index 0000000..cb4627e --- /dev/null +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -0,0 +1,96 @@ + [ + Line::class => 'line', + ], + ]; + + protected $references = [ + Line::class => LineEntity::class, + ]; + + private $em; + private $inner; + private $id; + + public function __construct(ContainerInterface $inner, EntityManagerInterface $em, IdUtils $idUtils) + { + $this->inner = $inner; + $this->em = $em; + $this->id = $idUtils; + } + + public function process(HandleModifierEvent $event) + { + if (!$event instanceof HandleDatabaseModifierEvent) { + return; + } + + /** @var RelatedFilter $modifier */ + $modifier = $event->getModifier(); + $builder = $event->getBuilder(); + $alias = $event->getMeta()['alias']; + $type = $event->getMeta()['type']; + + if (!array_key_exists($type, $this->mapping)) { + throw new \InvalidArgumentException( + sprintf("Relationship filtering for %s is not supported.", $type) + ); + } + + if (!array_key_exists($modifier->getRelationship(), $this->mapping[$type])) { + throw new \InvalidArgumentException( + sprintf("Relationship %s is not supported for .", $type) + ); + } + + $relationship = $this->mapping[$type][$modifier->getRelationship()]; + + $parameter = sprintf(":%s_%s", $alias, $relationship); + $reference = $this->getEntityReference($modifier->getRelated(), $event->getMeta()['provider']); + + $builder + ->join(sprintf('%s.%s', $alias, $relationship), $relationship) + ->andWhere(sprintf("%s = %s", $relationship, $parameter)) + ->setParameter($parameter, $reference) + ; + } + + // todo: extract that to separate service + private function getEntityReference(Referable $object, ProviderEntity $provider) + { + return $this->em->getReference( + $this->references[get_class($object)], + $this->id->generate($provider, $object->getId()) + ); + } + + /** + * @inheritDoc + */ + public static function getSubscribedServices() + { + return [ + TrackRelatedFilterDatabaseHandler::class, + ]; + } +} diff --git a/src/Handler/PostProcessingHandler.php b/src/Handler/PostProcessingHandler.php index 4da8a89..a18f0d8 100644 --- a/src/Handler/PostProcessingHandler.php +++ b/src/Handler/PostProcessingHandler.php @@ -2,10 +2,9 @@ namespace App\Handler; -use App\Event\HandleModifierEvent; use App\Event\PostProcessEvent; interface PostProcessingHandler { - public function process(PostProcessEvent $event); + public function postProcess(PostProcessEvent $event); } diff --git a/src/Modifier/RelatedFilter.php b/src/Modifier/RelatedFilter.php new file mode 100644 index 0000000..f3e9038 --- /dev/null +++ b/src/Modifier/RelatedFilter.php @@ -0,0 +1,27 @@ +object = $object; + $this->relationship = $relation ?: get_class($object); + } + + public function getRelationship(): string + { + return $this->relationship; + } + + public function getRelated(): Referable + { + return $this->object; + } +} diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index e7e7529..60ad5ca 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -9,12 +9,15 @@ use App\Exception\UnsupportedModifierException; use App\Handler\Database\IdFilterDatabaseHandler; use App\Handler\Database\LimitDatabaseHandler; use App\Handler\Database\FieldFilterDatabaseHandler; +use App\Handler\Database\RelatedFilterDatabaseGenericHandler; +use App\Handler\ModifierHandler; use App\Handler\PostProcessingHandler; use App\Model\Referable; use App\Modifier\IdFilter; use App\Modifier\Limit; use App\Modifier\Modifier; use App\Modifier\FieldFilter; +use App\Modifier\RelatedFilter; use App\Provider\Repository; use App\Service\Converter; use App\Service\IdUtils; @@ -85,27 +88,26 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit foreach ($modifiers as $modifier) { $handler = $this->getHandler($modifier); - switch (true) { - case $handler instanceof PostProcessingHandler: - $reducers[] = function ($result) use ($meta, $modifier, $handler) { - $event = new PostProcessEvent($result, $modifier, $this, array_merge([ - 'provider' => $this->provider, - ], $meta)); + if ($handler instanceof ModifierHandler) { + $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([ + 'provider' => $this->provider, + ], $meta)); - $handler->process($event); + $handler->process($event); + } - return $event->getData(); - }; - break; - - default: - $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([ + if ($handler instanceof PostProcessingHandler) { + $reducers[] = function ($result) use ($meta, $modifier, $handler) { + $event = new PostProcessEvent($result, $modifier, $this, array_merge([ 'provider' => $this->provider, ], $meta)); - $handler->process($event); - break; + $handler->postProcess($event); + + return $event->getData(); + }; } + } return collect($reducers); @@ -155,9 +157,10 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit public static function getSubscribedServices() { return array_merge([ - IdFilter::class => IdFilterDatabaseHandler::class, - Limit::class => LimitDatabaseHandler::class, - FieldFilter::class => FieldFilterDatabaseHandler::class, + IdFilter::class => IdFilterDatabaseHandler::class, + Limit::class => LimitDatabaseHandler::class, + FieldFilter::class => FieldFilterDatabaseHandler::class, + RelatedFilter::class => RelatedFilterDatabaseGenericHandler::class, ], static::getHandlers()); } } diff --git a/src/Provider/Database/GenericTrackRepository.php b/src/Provider/Database/GenericTrackRepository.php index 53bd235..82699d7 100644 --- a/src/Provider/Database/GenericTrackRepository.php +++ b/src/Provider/Database/GenericTrackRepository.php @@ -2,36 +2,18 @@ namespace App\Provider\Database; -use App\Entity\LineEntity; use App\Entity\StopEntity; use App\Entity\StopInTrack; use App\Entity\TrackEntity; -use function App\Functions\encapsulate; -use App\Model\Stop; +use App\Modifier\Modifier; use App\Model\Track; use App\Provider\TrackRepository; use Tightenco\Collect\Support\Collection; use Kadet\Functional as f; +use function App\Functions\encapsulate; class GenericTrackRepository extends DatabaseRepository implements TrackRepository { - public function getAll(): Collection - { - $tracks = $this->em->getRepository(TrackEntity::class)->findAll(); - - return collect($tracks)->map(f\ref([$this, 'convert'])); - } - - public function getById($id): Track - { - // TODO: Implement getById() method. - } - - public function getManyById($ids): Collection - { - // TODO: Implement getManyById() method. - } - public function getByStop($stop): Collection { $reference = f\apply(f\ref([$this, 'reference']), StopEntity::class); @@ -49,24 +31,17 @@ class GenericTrackRepository extends DatabaseRepository implements TrackReposito }); } - public function getByLine($line): Collection + public function all(Modifier ...$modifiers): Collection { - $reference = f\apply(f\ref([$this, 'reference']), LineEntity::class); + $builder = $this->em + ->createQueryBuilder() + ->from(TrackEntity::class, 'track') + ->select('track'); - $tracks = $this->em->createQueryBuilder() - ->from(StopInTrack::class, 'st') - ->join('st.track', 't') - ->join('t.stops', 's') - ->where('st.line in (:line)') - ->select(['st', 't', 's']) - ->getQuery() - ->execute(['stop' => array_map($reference, encapsulate($line))]); - - return collect($tracks)->map(f\ref([$this, 'convert'])); - } - - protected static function getHandlers() - { - return []; + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'track', + 'entity' => TrackEntity::class, + 'type' => Track::class, + ]); } } diff --git a/src/Provider/Database/GenericTripRepository.php b/src/Provider/Database/GenericTripRepository.php index 00626ad..6ffd652 100644 --- a/src/Provider/Database/GenericTripRepository.php +++ b/src/Provider/Database/GenericTripRepository.php @@ -20,7 +20,7 @@ class GenericTripRepository extends DatabaseRepository implements TripRepository ->select('t', 'ts'); return $this->allFromQueryBuilder($builder, $modifiers, [ - 'alias' => 'operator', + 'alias' => 'trip', 'entity' => TripEntity::class, 'type' => Trip::class, ]); diff --git a/src/Provider/TrackRepository.php b/src/Provider/TrackRepository.php index 708e329..3e70a03 100644 --- a/src/Provider/TrackRepository.php +++ b/src/Provider/TrackRepository.php @@ -5,13 +5,7 @@ namespace App\Provider; use App\Model\Track; use Tightenco\Collect\Support\Collection; -interface TrackRepository +interface TrackRepository extends FluentRepository { - public function getAll(): Collection; - - public function getById($id): Track; - public function getManyById($ids): Collection; - public function getByStop($stop): Collection; - public function getByLine($line): Collection; -} \ No newline at end of file +} From 9f3f6bf22be7e1f3890dc68cca2fdfb37a0b1a66 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 18 Feb 2020 22:45:16 +0100 Subject: [PATCH 07/16] Add stop filter for tracks --- src/Controller/Api/v1/StopsController.php | 21 ++----- src/Controller/Api/v1/TracksController.php | 63 ++++++++----------- .../RelatedFilterDatabaseGenericHandler.php | 40 ++++++------ .../Database/TrackByStopDatabaseHandler.php | 43 +++++++++++++ src/Service/ReferenceFactory.php | 42 +++++++++++++ 5 files changed, 137 insertions(+), 72 deletions(-) create mode 100644 src/Handler/Database/TrackByStopDatabaseHandler.php create mode 100644 src/Service/ReferenceFactory.php diff --git a/src/Controller/Api/v1/StopsController.php b/src/Controller/Api/v1/StopsController.php index 64c4107..669e58f 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -1,6 +1,5 @@ values(); } - /** - * @param Request $request - * - * @return array - */ - private function getModifiersFromRequest(Request $request): array + private function getModifiersFromRequest(Request $request) { - $modifiers = []; - if ($request->query->has('name')) { - $modifiers[] = FieldFilter::contains('name', $request->query->get('name')); + yield FieldFilter::contains('name', $request->query->get('name')); } if ($request->query->has('id')) { - $modifiers[] = new IdFilter($request->query->get('id')); + yield new IdFilter($request->query->get('id')); } if ($request->query->has('include-destinations')) { - $modifiers[] = new IncludeDestinations(); + yield new IncludeDestinations(); } - - return $modifiers; } } diff --git a/src/Controller/Api/v1/TracksController.php b/src/Controller/Api/v1/TracksController.php index 6f9fbad..71b7ae8 100644 --- a/src/Controller/Api/v1/TracksController.php +++ b/src/Controller/Api/v1/TracksController.php @@ -9,6 +9,7 @@ use App\Model\Track; use App\Modifier\IdFilter; use App\Modifier\RelatedFilter; use App\Provider\TrackRepository; +use App\Service\IterableUtils; use Nelmio\ApiDocBundle\Annotation\Model; use Swagger\Annotations as SWG; use Symfony\Component\HttpFoundation\Request; @@ -31,43 +32,31 @@ class TracksController extends Controller */ public function index(Request $request, TrackRepository $repository) { - switch (true) { - case $request->query->has('stop'): - return $this->byStop($request, $repository); - case $request->query->has('line'): - return $this->byLine($request, $repository); - case $request->query->has('id'): - return $this->byId($request, $repository); - default: - throw new BadRequestHttpException( - sprintf( - 'At least one parameter of %s must be set.', - implode(', ', ['stop', 'line', 'id']) - ) - ); + $modifiers = $this->getModifiersFromRequest($request); + + return $this->json($repository->all(...$modifiers)); + } + + private function getModifiersFromRequest(Request $request) + { + if ($request->query->has('stop')) { + $stop = $request->query->get('stop'); + $stop = Stop::reference($stop); + + yield new RelatedFilter($stop); + } + + if ($request->query->has('line')) { + $line = $request->query->get('line'); + $line = Line::reference($line); + + yield new RelatedFilter($line); + } + + if ($request->query->has('id')) { + $id = encapsulate($request->query->get('id')); + + yield new IdFilter($id); } } - - private function byId(Request $request, TrackRepository $repository) - { - $id = encapsulate($request->query->get('id')); - - return $this->json($repository->all(new IdFilter($id))); - } - - private function byStop(Request $request, TrackRepository $repository) - { - $stop = $request->query->get('stop'); - $stop = array_map([Stop::class, 'reference'], encapsulate($stop)); - - return $this->json($repository->getByStop($stop)); - } - - private function byLine(Request $request, TrackRepository $repository) - { - $line = $request->query->get('line'); - $line = Line::reference($line); - - return $this->json($repository->all(new RelatedFilter($line))); - } } diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php index cb4627e..6a2683d 100644 --- a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -2,16 +2,15 @@ namespace App\Handler\Database; -use App\Entity\LineEntity; -use App\Entity\ProviderEntity; use App\Event\HandleDatabaseModifierEvent; use App\Event\HandleModifierEvent; use App\Handler\ModifierHandler; use App\Model\Line; -use App\Model\Referable; +use App\Model\Stop; use App\Model\Track; use App\Modifier\RelatedFilter; use App\Service\IdUtils; +use App\Service\ReferenceFactory; use Doctrine\ORM\EntityManagerInterface; use Psr\Container\ContainerInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -21,22 +20,25 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub protected $mapping = [ Track::class => [ Line::class => 'line', + Stop::class => TrackByStopDatabaseHandler::class, ], ]; - protected $references = [ - Line::class => LineEntity::class, - ]; - private $em; private $inner; private $id; + private $references; - public function __construct(ContainerInterface $inner, EntityManagerInterface $em, IdUtils $idUtils) - { + public function __construct( + ContainerInterface $inner, + EntityManagerInterface $em, + IdUtils $idUtils, + ReferenceFactory $references + ) { $this->inner = $inner; $this->em = $em; $this->id = $idUtils; + $this->references = $references; } public function process(HandleModifierEvent $event) @@ -65,8 +67,15 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub $relationship = $this->mapping[$type][$modifier->getRelationship()]; + if ($this->inner->has($relationship)) { + /** @var ModifierHandler $inner */ + $inner = $this->inner->get($relationship); + $inner->process($event); + return; + } + $parameter = sprintf(":%s_%s", $alias, $relationship); - $reference = $this->getEntityReference($modifier->getRelated(), $event->getMeta()['provider']); + $reference = $this->references->create($modifier->getRelated(), $event->getMeta()['provider']); $builder ->join(sprintf('%s.%s', $alias, $relationship), $relationship) @@ -75,22 +84,13 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub ; } - // todo: extract that to separate service - private function getEntityReference(Referable $object, ProviderEntity $provider) - { - return $this->em->getReference( - $this->references[get_class($object)], - $this->id->generate($provider, $object->getId()) - ); - } - /** * @inheritDoc */ public static function getSubscribedServices() { return [ - TrackRelatedFilterDatabaseHandler::class, + TrackByStopDatabaseHandler::class, ]; } } diff --git a/src/Handler/Database/TrackByStopDatabaseHandler.php b/src/Handler/Database/TrackByStopDatabaseHandler.php new file mode 100644 index 0000000..12789e5 --- /dev/null +++ b/src/Handler/Database/TrackByStopDatabaseHandler.php @@ -0,0 +1,43 @@ +references = $references; + } + + public function process(HandleModifierEvent $event) + { + if (!$event instanceof HandleDatabaseModifierEvent) { + return; + } + + /** @var RelatedFilter $modifier */ + $modifier = $event->getModifier(); + $builder = $event->getBuilder(); + $alias = $event->getMeta()['alias']; + + $relationship = 'stopsInTrack'; + + $parameter = sprintf(":%s_%s", $alias, $relationship); + $reference = $this->references->create($modifier->getRelated(), $event->getMeta()['provider']); + + $builder + ->join(sprintf("%s.%s", $alias, $relationship), 'stop_in_track') + ->andWhere(sprintf("stop_in_track.stop = %s", $parameter)) + ->setParameter($parameter, $reference) + ; + } +} diff --git a/src/Service/ReferenceFactory.php b/src/Service/ReferenceFactory.php new file mode 100644 index 0000000..3d91cd9 --- /dev/null +++ b/src/Service/ReferenceFactory.php @@ -0,0 +1,42 @@ + LineEntity::class, + Stop::class => StopEntity::class, + ]; + + private $em; + private $id; + + public function __construct(EntityManagerInterface $em, IdUtils $id) + { + $this->em = $em; + $this->id = $id; + } + + public function create(Referable $object, ProviderEntity $provider) + { + $class = get_class($object); + + if (!array_key_exists($class, $this->mapping)) { + throw new \InvalidArgumentException(sprintf("Cannot make entity reference of %s.", $class)); + } + + return $this->em->getReference( + $this->mapping[$class], + $this->id->generate($provider, $object->getId()) + ); + } +} From a9a0f2f413fd6cea18e0eb69afd7b2929f779bf3 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Thu, 20 Feb 2020 17:33:31 +0100 Subject: [PATCH 08/16] Track stops fluent quering --- src/Controller/Api/v1/StopsController.php | 18 ++---- src/Controller/Api/v1/TracksController.php | 36 +++++++++++- src/Entity/TrackEntity.php | 12 ++-- .../{StopInTrack.php => TrackStopEntity.php} | 2 +- .../RelatedFilterDatabaseGenericHandler.php | 21 ++++--- .../Database/TrackByStopDatabaseHandler.php | 6 +- src/Model/Line.php | 2 +- src/Model/ScheduledStop.php | 36 +----------- src/Model/TrackStop.php | 58 +++++++++++++++++++ src/Provider/Database/DatabaseRepository.php | 4 ++ .../Database/GenericScheduleRepository.php | 2 +- .../Database/GenericTrackRepository.php | 29 ++++------ src/Provider/TrackRepository.php | 3 +- .../ZtmGdanskDataUpdateSubscriber.php | 4 +- ...Factory.php => EntityReferenceFactory.php} | 9 ++- src/Service/ScheduledStopConverter.php | 27 ++++++--- 16 files changed, 169 insertions(+), 100 deletions(-) rename src/Entity/{StopInTrack.php => TrackStopEntity.php} (96%) create mode 100644 src/Model/TrackStop.php rename src/Service/{ReferenceFactory.php => EntityReferenceFactory.php} (80%) diff --git a/src/Controller/Api/v1/StopsController.php b/src/Controller/Api/v1/StopsController.php index 669e58f..dc6ac45 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -4,7 +4,7 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; use App\Model\Stop; -use App\Model\Track; +use App\Model\TrackStop; use App\Model\StopGroup; use App\Modifier\IdFilter; use App\Modifier\FieldFilter; @@ -12,7 +12,6 @@ use App\Modifier\IncludeDestinations; use App\Modifier\RelatedFilter; use App\Provider\StopRepository; use App\Provider\TrackRepository; -use App\Service\Proxy\ReferenceFactory; use Nelmio\ApiDocBundle\Annotation\Model; use Swagger\Annotations as SWG; use Symfony\Component\HttpFoundation\Request; @@ -105,21 +104,12 @@ class StopsController extends Controller * @SWG\Response( * response=200, * description="Returns specific stop referenced via identificator.", - * @SWG\Schema(type="object", properties={ - * @SWG\Property(property="track", type="object", ref=@Model(type=Track::class)), - * @SWG\Property(property="order", type="integer", minimum="0") - * }) + * @SWG\Schema(ref=@Model(type=TrackStop::class)) * ) - * - * @SWG\Tag(name="Tracks") */ - public function tracks(ReferenceFactory $reference, TrackRepository $tracks, $id) + public function tracks(TrackRepository $tracks, $id) { - $stop = $reference->get(Stop::class, $id); - - return $this->json($tracks->getByStop($stop)->map(function ($tuple) { - return array_combine(['track', 'order'], $tuple); - })); + return $this->json($tracks->stops(new RelatedFilter(Stop::reference($id)))); } public static function group(Collection $stops) diff --git a/src/Controller/Api/v1/TracksController.php b/src/Controller/Api/v1/TracksController.php index 71b7ae8..71e79c9 100644 --- a/src/Controller/Api/v1/TracksController.php +++ b/src/Controller/Api/v1/TracksController.php @@ -19,6 +19,7 @@ use function App\Functions\encapsulate; /** * @Route("/tracks") + * @SWG\Tag(name="Tracks") */ class TracksController extends Controller { @@ -27,7 +28,6 @@ class TracksController extends Controller * response=200, * description="Returns all tracks for specific provider, e.g. ZTM Gdańsk.", * ) - * @SWG\Tag(name="Tracks") * @Route("/", methods={"GET"}) */ public function index(Request $request, TrackRepository $repository) @@ -37,6 +37,17 @@ class TracksController extends Controller return $this->json($repository->all(...$modifiers)); } + /** + * @Route("/stops", methods={"GET"}) + * @Route("/{track}/stops", methods={"GET"}) + */ + public function stops(Request $request, TrackRepository $repository) + { + $modifiers = $this->getStopsModifiersFromRequest($request); + + return $this->json($repository->stops(...$modifiers)); + } + private function getModifiersFromRequest(Request $request) { if ($request->query->has('stop')) { @@ -59,4 +70,27 @@ class TracksController extends Controller yield new IdFilter($id); } } + + private function getStopsModifiersFromRequest(Request $request) + { + if ($request->query->has('stop')) { + $stop = $request->query->get('stop'); + $stop = Stop::reference($stop); + + yield new RelatedFilter($stop); + } + + if ($request->query->has('track') || $request->attributes->has('track')) { + $track = $request->get('track'); + $track = Track::reference($track); + + yield new RelatedFilter($track); + } + + if ($request->query->has('id')) { + $id = encapsulate($request->query->get('id')); + + yield new IdFilter($id); + } + } } diff --git a/src/Entity/TrackEntity.php b/src/Entity/TrackEntity.php index a0eaa1d..856297b 100644 --- a/src/Entity/TrackEntity.php +++ b/src/Entity/TrackEntity.php @@ -45,16 +45,18 @@ class TrackEntity implements Entity, Fillable /** * Stops in track - * @var StopInTrack[]|Collection - * @ORM\OneToMany(targetEntity=StopInTrack::class, fetch="LAZY", mappedBy="track", cascade={"persist"}) + * + * @var TrackStopEntity[]|Collection + * @ORM\OneToMany(targetEntity=TrackStopEntity::class, fetch="LAZY", mappedBy="track", cascade={"persist"}) * @ORM\OrderBy({"order": "ASC"}) */ private $stopsInTrack; /** * Final stop in this track. - * @var StopInTrack - * @ORM\OneToOne(targetEntity=StopInTrack::class, fetch="LAZY") + * + * @var TrackStopEntity + * @ORM\OneToOne(targetEntity=TrackStopEntity::class, fetch="LAZY") */ private $final; @@ -114,7 +116,7 @@ class TrackEntity implements Entity, Fillable $this->final = $this->stopsInTrack->last(); } - public function getFinal(): StopInTrack + public function getFinal(): TrackStopEntity { return $this->final; } diff --git a/src/Entity/StopInTrack.php b/src/Entity/TrackStopEntity.php similarity index 96% rename from src/Entity/StopInTrack.php rename to src/Entity/TrackStopEntity.php index 26cbd55..b32c84b 100644 --- a/src/Entity/StopInTrack.php +++ b/src/Entity/TrackStopEntity.php @@ -14,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM; * @ORM\UniqueConstraint(name="stop_in_track_idx", columns={"stop_id", "track_id", "sequence"}) * }) */ -class StopInTrack implements Fillable, Referable +class TrackStopEntity implements Fillable, Referable { use FillTrait, ReferableEntityTrait; diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php index 6a2683d..68d2fc9 100644 --- a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -8,9 +8,10 @@ use App\Handler\ModifierHandler; use App\Model\Line; use App\Model\Stop; use App\Model\Track; +use App\Model\TrackStop; use App\Modifier\RelatedFilter; use App\Service\IdUtils; -use App\Service\ReferenceFactory; +use App\Service\EntityReferenceFactory; use Doctrine\ORM\EntityManagerInterface; use Psr\Container\ContainerInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -18,10 +19,14 @@ use Symfony\Contracts\Service\ServiceSubscriberInterface; class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSubscriberInterface { protected $mapping = [ - Track::class => [ + Track::class => [ Line::class => 'line', Stop::class => TrackByStopDatabaseHandler::class, ], + TrackStop::class => [ + Stop::class => 'stop', + Track::class => 'track', + ], ]; private $em; @@ -33,11 +38,11 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub ContainerInterface $inner, EntityManagerInterface $em, IdUtils $idUtils, - ReferenceFactory $references + EntityReferenceFactory $references ) { - $this->inner = $inner; - $this->em = $em; - $this->id = $idUtils; + $this->inner = $inner; + $this->em = $em; + $this->id = $idUtils; $this->references = $references; } @@ -71,6 +76,7 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub /** @var ModifierHandler $inner */ $inner = $this->inner->get($relationship); $inner->process($event); + return; } @@ -80,8 +86,7 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub $builder ->join(sprintf('%s.%s', $alias, $relationship), $relationship) ->andWhere(sprintf("%s = %s", $relationship, $parameter)) - ->setParameter($parameter, $reference) - ; + ->setParameter($parameter, $reference); } /** diff --git a/src/Handler/Database/TrackByStopDatabaseHandler.php b/src/Handler/Database/TrackByStopDatabaseHandler.php index 12789e5..d5476a7 100644 --- a/src/Handler/Database/TrackByStopDatabaseHandler.php +++ b/src/Handler/Database/TrackByStopDatabaseHandler.php @@ -2,18 +2,18 @@ namespace App\Handler\Database; -use App\Entity\StopInTrack; +use App\Entity\TrackStopEntity; use App\Event\HandleDatabaseModifierEvent; use App\Event\HandleModifierEvent; use App\Handler\ModifierHandler; use App\Modifier\RelatedFilter; -use App\Service\ReferenceFactory; +use App\Service\EntityReferenceFactory; class TrackByStopDatabaseHandler implements ModifierHandler { private $references; - public function __construct(ReferenceFactory $references) + public function __construct(EntityReferenceFactory $references) { $this->references = $references; } diff --git a/src/Model/Line.php b/src/Model/Line.php index b339218..f905b7d 100644 --- a/src/Model/Line.php +++ b/src/Model/Line.php @@ -138,4 +138,4 @@ class Line implements Fillable, Referable { $this->operator = $operator; } -} \ No newline at end of file +} diff --git a/src/Model/ScheduledStop.php b/src/Model/ScheduledStop.php index abfda2d..2ae78f2 100644 --- a/src/Model/ScheduledStop.php +++ b/src/Model/ScheduledStop.php @@ -4,22 +4,8 @@ namespace App\Model; use Carbon\Carbon; -class ScheduledStop implements Fillable +class ScheduledStop extends TrackStop { - use FillTrait; - - /** - * Stop (as a place) related to that scheduled bus stop - * @var Stop - */ - private $stop; - - /** - * Order in trip - * @var int - */ - private $order; - /** * Arrival time * @var Carbon @@ -32,26 +18,6 @@ class ScheduledStop implements Fillable */ private $departure; - public function getStop() - { - return $this->stop; - } - - public function setStop($stop): void - { - $this->stop = $stop; - } - - public function getOrder(): int - { - return $this->order; - } - - public function setOrder(int $order): void - { - $this->order = $order; - } - public function getArrival(): Carbon { return $this->arrival; diff --git a/src/Model/TrackStop.php b/src/Model/TrackStop.php new file mode 100644 index 0000000..283151a --- /dev/null +++ b/src/Model/TrackStop.php @@ -0,0 +1,58 @@ +stop; + } + + public function setStop($stop): void + { + $this->stop = $stop; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setOrder(int $order): void + { + $this->order = $order; + } + + public function getTrack(): ?Track + { + return $this->track; + } + + public function setTrack(?Track $track): void + { + $this->track = $track; + } +} diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index 60ad5ca..8156253 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -28,6 +28,8 @@ use Symfony\Contracts\Service\ServiceSubscriberInterface; abstract class DatabaseRepository implements ServiceSubscriberInterface, Repository { + const DEFAULT_LIMIT = 100; + /** @var EntityManagerInterface */ protected $em; @@ -115,6 +117,8 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit protected function allFromQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = []) { + $builder->setMaxResults(self::DEFAULT_LIMIT); + $reducers = $this->processQueryBuilder($builder, $modifiers, $meta); return $reducers->reduce(function ($result, $reducer) { diff --git a/src/Provider/Database/GenericScheduleRepository.php b/src/Provider/Database/GenericScheduleRepository.php index e1c2c29..ed7adbe 100644 --- a/src/Provider/Database/GenericScheduleRepository.php +++ b/src/Provider/Database/GenericScheduleRepository.php @@ -3,7 +3,7 @@ namespace App\Provider\Database; use App\Entity\StopEntity; -use App\Entity\StopInTrack; +use App\Entity\TrackStopEntity; use App\Entity\TrackEntity; use App\Entity\TripEntity; use App\Entity\TripStopEntity; diff --git a/src/Provider/Database/GenericTrackRepository.php b/src/Provider/Database/GenericTrackRepository.php index 82699d7..f25d7f2 100644 --- a/src/Provider/Database/GenericTrackRepository.php +++ b/src/Provider/Database/GenericTrackRepository.php @@ -2,33 +2,28 @@ namespace App\Provider\Database; -use App\Entity\StopEntity; -use App\Entity\StopInTrack; +use App\Entity\TrackStopEntity; use App\Entity\TrackEntity; +use App\Model\TrackStop; use App\Modifier\Modifier; use App\Model\Track; use App\Provider\TrackRepository; use Tightenco\Collect\Support\Collection; -use Kadet\Functional as f; -use function App\Functions\encapsulate; class GenericTrackRepository extends DatabaseRepository implements TrackRepository { - public function getByStop($stop): Collection + public function stops(Modifier ...$modifiers): Collection { - $reference = f\apply(f\ref([$this, 'reference']), StopEntity::class); + $builder = $this->em + ->createQueryBuilder() + ->from(TrackStopEntity::class, 'track_stop') + ->select(['track_stop']); - $tracks = $this->em->createQueryBuilder() - ->from(StopInTrack::class, 'st') - ->join('st.track', 't') - ->where('st.stop in (:stop)') - ->select(['st', 't']) - ->getQuery() - ->execute(['stop' => array_map($reference, encapsulate($stop))]); - - return collect($tracks)->map(function (StopInTrack $entity) { - return [ $this->convert($entity->getTrack()), $entity->getOrder() ]; - }); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'track_stop', + 'entity' => TrackStopEntity::class, + 'type' => TrackStop::class, + ]); } public function all(Modifier ...$modifiers): Collection diff --git a/src/Provider/TrackRepository.php b/src/Provider/TrackRepository.php index 3e70a03..2b66a36 100644 --- a/src/Provider/TrackRepository.php +++ b/src/Provider/TrackRepository.php @@ -3,9 +3,10 @@ namespace App\Provider; use App\Model\Track; +use App\Modifier\Modifier; use Tightenco\Collect\Support\Collection; interface TrackRepository extends FluentRepository { - public function getByStop($stop): Collection; + public function stops(Modifier ...$modifiers): Collection; } diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php b/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php index 146ef55..38bf725 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php @@ -6,7 +6,7 @@ use App\Entity\LineEntity; use App\Entity\OperatorEntity; use App\Entity\ProviderEntity; use App\Entity\StopEntity; -use App\Entity\StopInTrack; +use App\Entity\TrackStopEntity; use App\Entity\TrackEntity; use App\Entity\TripEntity; use App\Entity\TripStopEntity; @@ -216,7 +216,7 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface return !in_array($stop['stopId'], $this->stopBlacklist); }) ->map(function ($stop) use ($entity, $provider) { - return StopInTrack::createFromArray([ + return TrackStopEntity::createFromArray([ 'stop' => $this->em->getReference( StopEntity::class, $this->ids->generate($provider, $stop['stopId']) diff --git a/src/Service/ReferenceFactory.php b/src/Service/EntityReferenceFactory.php similarity index 80% rename from src/Service/ReferenceFactory.php rename to src/Service/EntityReferenceFactory.php index 3d91cd9..d68bcc0 100644 --- a/src/Service/ReferenceFactory.php +++ b/src/Service/EntityReferenceFactory.php @@ -5,16 +5,19 @@ namespace App\Service; use App\Entity\LineEntity; use App\Entity\ProviderEntity; use App\Entity\StopEntity; +use App\Entity\TrackEntity; use App\Model\Line; use App\Model\Referable; use App\Model\Stop; +use App\Model\Track; use Doctrine\ORM\EntityManagerInterface; -final class ReferenceFactory +final class EntityReferenceFactory { protected $mapping = [ - Line::class => LineEntity::class, - Stop::class => StopEntity::class, + Line::class => LineEntity::class, + Stop::class => StopEntity::class, + Track::class => TrackEntity::class, ]; private $em; diff --git a/src/Service/ScheduledStopConverter.php b/src/Service/ScheduledStopConverter.php index da20f28..c00faae 100644 --- a/src/Service/ScheduledStopConverter.php +++ b/src/Service/ScheduledStopConverter.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\Entity\TrackStopEntity; use App\Entity\TripStopEntity; use App\Model\ScheduledStop; @@ -11,18 +12,28 @@ class ScheduledStopConverter implements Converter, RecursiveConverter public function convert($entity) { - /** @var ScheduledStop $entity */ - return ScheduledStop::createFromArray([ - 'arrival' => $entity->getArrival(), - 'departure' => $entity->getDeparture(), - 'stop' => $this->parent->convert($entity->getStop()), - 'order' => $entity->getOrder(), - ]); + if ($entity instanceof TrackStopEntity) { + return ScheduledStop::createFromArray([ + 'stop' => $this->parent->convert($entity->getStop()), + 'track' => $this->parent->convert($entity->getTrack()), + 'order' => $entity->getOrder(), + ]); + } + + if ($entity instanceof TripStopEntity) { + return ScheduledStop::createFromArray([ + 'arrival' => $entity->getArrival(), + 'departure' => $entity->getDeparture(), + 'stop' => $this->parent->convert($entity->getStop()), + 'order' => $entity->getOrder(), + ]); + } } public function supports($entity) { - return $entity instanceof TripStopEntity; + return $entity instanceof TripStopEntity + || $entity instanceof TrackStopEntity; } } From 45004444e6702c43992733bacd8f7fb471d86381 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 23 Feb 2020 15:23:14 +0100 Subject: [PATCH 09/16] Various little fixes and improvements --- docker/php/.env | 1 - docker/php/Dockerfile | 5 ++++- webpack.config.js | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docker/php/.env b/docker/php/.env index eb54481..ef3fd93 100644 --- a/docker/php/.env +++ b/docker/php/.env @@ -1,2 +1 @@ -XDEBUG_CONFIG=remote_host=172.17.0.1 remote_port=9001 PHP_IDE_CONFIG=serverName=czydojade diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 6142fa0..08a914e 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,5 +1,7 @@ FROM php:7.3-fpm +ARG XDEBUG_REMOTE_HOST="172.17.0.1" + RUN apt-get update && \ apt-get install -y --no-install-recommends git zip libzip-dev @@ -7,7 +9,8 @@ RUN docker-php-ext-install zip RUN pecl install xdebug-2.9.0 && docker-php-ext-enable xdebug -RUN echo "xdebug.remote_enable = 1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; +RUN echo "xdebug.remote_enable = 1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \ + echo "xdebug.remote_host = ${XDEBUG_REMOTE_HOST}" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; RUN echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini; RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" diff --git a/webpack.config.js b/webpack.config.js index 367d75b..0382428 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -66,8 +66,11 @@ const config = { new GenerateSW({ navigationPreload: true, runtimeCaching: [{ - urlPattern: ({event}) => event.request.mode === 'navigate', - handler: 'NetworkFirst', + urlPattern: ({ event }) => event.request.mode === 'navigate', + handler: 'NetworkFirst', + }, { + urlPattern: /^https?:\/\/api\.maptiler\.com\//, + handler: 'CacheFirst', }], swDest: '../service-worker.js' }) From ee0cde0400745caad91796018b5874c0e87e7180 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 23 Feb 2020 17:12:23 +0100 Subject: [PATCH 10/16] Add support for multiple related objects filtering --- src/Controller/Api/v1/TracksController.php | 17 +++++++------ ...ption.php => InvalidArgumentException.php} | 4 +-- .../RelatedFilterDatabaseGenericHandler.php | 2 +- .../Database/TrackByStopDatabaseHandler.php | 4 ++- src/Modifier/IdFilter.php | 7 +++--- src/Modifier/RelatedFilter.php | 23 ++++++++++++----- src/Service/EntityReferenceFactory.php | 25 ++++++++++++++++++- 7 files changed, 60 insertions(+), 22 deletions(-) rename src/Exception/{InvalidOptionException.php => InvalidArgumentException.php} (70%) diff --git a/src/Controller/Api/v1/TracksController.php b/src/Controller/Api/v1/TracksController.php index 71e79c9..54cece5 100644 --- a/src/Controller/Api/v1/TracksController.php +++ b/src/Controller/Api/v1/TracksController.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use function App\Functions\encapsulate; +use function Kadet\Functional\ref; /** * @Route("/tracks") @@ -51,17 +52,17 @@ class TracksController extends Controller private function getModifiersFromRequest(Request $request) { if ($request->query->has('stop')) { - $stop = $request->query->get('stop'); - $stop = Stop::reference($stop); + $stop = encapsulate($request->query->get('stop')); + $stop = collect($stop)->map([Stop::class, 'reference']); - yield new RelatedFilter($stop); + yield new RelatedFilter($stop, Stop::class); } if ($request->query->has('line')) { - $line = $request->query->get('line'); - $line = Line::reference($line); + $line = encapsulate($request->query->get('line')); + $line = collect($line)->map([Line::class, 'reference']); - yield new RelatedFilter($line); + yield new RelatedFilter($line, Line::class); } if ($request->query->has('id')) { @@ -74,8 +75,8 @@ class TracksController extends Controller private function getStopsModifiersFromRequest(Request $request) { if ($request->query->has('stop')) { - $stop = $request->query->get('stop'); - $stop = Stop::reference($stop); + $stop = encapsulate($request->query->get('stop')); + $stop = collect($stop)->map(ref([Stop::class, 'reference'])); yield new RelatedFilter($stop); } diff --git a/src/Exception/InvalidOptionException.php b/src/Exception/InvalidArgumentException.php similarity index 70% rename from src/Exception/InvalidOptionException.php rename to src/Exception/InvalidArgumentException.php index b052f10..14789e4 100644 --- a/src/Exception/InvalidOptionException.php +++ b/src/Exception/InvalidArgumentException.php @@ -2,11 +2,11 @@ namespace App\Exception; -class InvalidOptionException extends \InvalidArgumentException +class InvalidArgumentException extends \InvalidArgumentException { public static function invalidType($parameter, $value, array $expected = []) { - return new \InvalidArgumentException( + return new static( sprintf('Expected %s to be of type: %s. %s given.', $parameter, implode(', ', $expected), gettype($value)) ); } diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php index 68d2fc9..315b99f 100644 --- a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -85,7 +85,7 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub $builder ->join(sprintf('%s.%s', $alias, $relationship), $relationship) - ->andWhere(sprintf("%s = %s", $relationship, $parameter)) + ->andWhere(sprintf($modifier->isMultiple() ? "%s in (%s)" : "%s = %s", $relationship, $parameter)) ->setParameter($parameter, $reference); } diff --git a/src/Handler/Database/TrackByStopDatabaseHandler.php b/src/Handler/Database/TrackByStopDatabaseHandler.php index d5476a7..fe84aa0 100644 --- a/src/Handler/Database/TrackByStopDatabaseHandler.php +++ b/src/Handler/Database/TrackByStopDatabaseHandler.php @@ -34,9 +34,11 @@ class TrackByStopDatabaseHandler implements ModifierHandler $parameter = sprintf(":%s_%s", $alias, $relationship); $reference = $this->references->create($modifier->getRelated(), $event->getMeta()['provider']); + $condition = $modifier->isMultiple() ? 'stop_in_track.stop IN (%s)' : 'stop_in_track.stop = %s'; + $builder ->join(sprintf("%s.%s", $alias, $relationship), 'stop_in_track') - ->andWhere(sprintf("stop_in_track.stop = %s", $parameter)) + ->andWhere(sprintf($condition, $parameter)) ->setParameter($parameter, $reference) ; } diff --git a/src/Modifier/IdFilter.php b/src/Modifier/IdFilter.php index f7b6a13..0e791a4 100644 --- a/src/Modifier/IdFilter.php +++ b/src/Modifier/IdFilter.php @@ -2,8 +2,9 @@ namespace App\Modifier; -use App\Exception\InvalidOptionException; +use App\Exception\InvalidArgumentException; use App\Modifier\Modifier; +use App\Service\IterableUtils; class IdFilter implements Modifier { @@ -13,10 +14,10 @@ class IdFilter implements Modifier public function __construct($id) { if (!is_iterable($id) && !is_string($id)) { - throw InvalidOptionException::invalidType('id', $id, ['string', 'array']); + throw InvalidArgumentException::invalidType('id', $id, ['string', 'array']); } - $this->id = $id instanceof \Traversable ? iterator_to_array($id) : $id; + $this->id = is_iterable($id) ? IterableUtils::toArray($id) : $id; } public function getId() diff --git a/src/Modifier/RelatedFilter.php b/src/Modifier/RelatedFilter.php index f3e9038..3375880 100644 --- a/src/Modifier/RelatedFilter.php +++ b/src/Modifier/RelatedFilter.php @@ -2,17 +2,23 @@ namespace App\Modifier; +use App\Exception\InvalidArgumentException; use App\Model\Referable; +use App\Service\IterableUtils; class RelatedFilter implements Modifier { private $relationship; - private $object; + private $reference; - public function __construct(Referable $object, ?string $relation = null) + public function __construct($reference, ?string $relation = null) { - $this->object = $object; - $this->relationship = $relation ?: get_class($object); + if (!is_iterable($reference) && !$reference instanceof Referable) { + throw InvalidArgumentException::invalidType('object', $reference, [Referable::class, 'iterable']); + } + + $this->reference = is_iterable($reference) ? IterableUtils::toArray($reference) : $reference; + $this->relationship = $relation ?: get_class($reference); } public function getRelationship(): string @@ -20,8 +26,13 @@ class RelatedFilter implements Modifier return $this->relationship; } - public function getRelated(): Referable + public function getRelated() { - return $this->object; + return $this->reference; + } + + public function isMultiple() + { + return is_array($this->reference); } } diff --git a/src/Service/EntityReferenceFactory.php b/src/Service/EntityReferenceFactory.php index d68bcc0..6a5d5fd 100644 --- a/src/Service/EntityReferenceFactory.php +++ b/src/Service/EntityReferenceFactory.php @@ -6,11 +6,16 @@ use App\Entity\LineEntity; use App\Entity\ProviderEntity; use App\Entity\StopEntity; use App\Entity\TrackEntity; +use App\Exception\InvalidArgumentException; use App\Model\Line; use App\Model\Referable; use App\Model\Stop; use App\Model\Track; use Doctrine\ORM\EntityManagerInterface; +use Tightenco\Collect\Support\Collection; +use function Kadet\Functional\partial; +use function Kadet\Functional\ref; +use const Kadet\Functional\_; final class EntityReferenceFactory { @@ -29,7 +34,25 @@ final class EntityReferenceFactory $this->id = $id; } - public function create(Referable $object, ProviderEntity $provider) + public function create($object, ProviderEntity $provider) + { + switch (true) { + case $object instanceof Referable: + return $this->createEntityReference($object, $provider); + case is_array($object): + return array_map(partial(ref([$this, 'createEntityReference']), _, $provider), $object); + case $object instanceof Collection: + return $object->map(partial(ref([$this, 'createEntityReference']), _, $provider)); + default: + throw InvalidArgumentException::invalidType( + 'object', + $object, + [Referable::class, Collection::class, 'array'] + ); + } + } + + private function createEntityReference(Referable $object, ProviderEntity $provider) { $class = get_class($object); From 87255eaf1329eed16e46ae10156c75497c77af61 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 14 Mar 2020 12:36:24 +0100 Subject: [PATCH 11/16] Fix situation when there are multiple first stops on same trip --- src/Entity/TripStopEntity.php | 17 +++++++--- src/Migrations/Version20200314112552.php | 43 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 src/Migrations/Version20200314112552.php diff --git a/src/Entity/TripStopEntity.php b/src/Entity/TripStopEntity.php index f90df10..f80b012 100644 --- a/src/Entity/TripStopEntity.php +++ b/src/Entity/TripStopEntity.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Model\Fillable; use App\Model\FillTrait; +use App\Model\Referable; use App\Model\Trip; use App\Service\IdUtils; use Carbon\Carbon; @@ -14,21 +15,28 @@ use JMS\Serializer\Tests\Fixtures\Discriminator\Car; * @ORM\Entity * @ORM\Table("trip_stop") */ -class TripStopEntity implements Fillable +class TripStopEntity implements Fillable, Referable { - use FillTrait; + use FillTrait, ReferableEntityTrait; + + /** + * Identifier for stop coming from provider + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + */ + private $id; /** * @var StopEntity * @ORM\ManyToOne(targetEntity=StopEntity::class, fetch="EAGER") - * @ORM\Id */ private $stop; /** * @var TripEntity * @ORM\ManyToOne(targetEntity=TripEntity::class, fetch="EAGER", inversedBy="stops") - * @ORM\Id */ private $trip; @@ -37,7 +45,6 @@ class TripStopEntity implements Fillable * @var int * * @ORM\Column(name="sequence", type="integer") - * @ORM\Id */ private $order; diff --git a/src/Migrations/Version20200314112552.php b/src/Migrations/Version20200314112552.php new file mode 100644 index 0000000..a16a743 --- /dev/null +++ b/src/Migrations/Version20200314112552.php @@ -0,0 +1,43 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); + + $this->addSql('CREATE TEMPORARY TABLE __temp__trip_stop AS SELECT stop_id, trip_id, sequence, arrival, departure FROM trip_stop'); + $this->addSql('DROP TABLE trip_stop'); + $this->addSql('CREATE TABLE trip_stop (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, sequence INTEGER NOT NULL, arrival DATETIME NOT NULL, departure DATETIME NOT NULL, stop_id VARCHAR(255) DEFAULT NULL, trip_id VARCHAR(255) DEFAULT NULL)'); + $this->addSql('INSERT INTO trip_stop (stop_id, trip_id, sequence, arrival, departure) SELECT stop_id, trip_id, sequence, arrival, departure FROM __temp__trip_stop'); + $this->addSql('DROP TABLE __temp__trip_stop'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); + + $this->addSql('CREATE TEMPORARY TABLE __temp__trip_stop AS SELECT sequence, arrival, departure, stop_id, trip_id FROM trip_stop'); + $this->addSql('DROP TABLE trip_stop'); + $this->addSql('CREATE TABLE trip_stop (sequence INTEGER NOT NULL, stop_id VARCHAR(255) NOT NULL COLLATE BINARY, trip_id VARCHAR(255) NOT NULL COLLATE BINARY, arrival DATETIME NOT NULL, departure DATETIME NOT NULL, PRIMARY KEY(stop_id, trip_id, sequence))'); + $this->addSql('INSERT INTO trip_stop (sequence, arrival, departure, stop_id, trip_id) SELECT sequence, arrival, departure, stop_id, trip_id FROM __temp__trip_stop'); + $this->addSql('DROP TABLE __temp__trip_stop'); + } +} From 7ffd3c02cd7ebbe0f74dbee55f42d5fc7fb66832 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 14 Mar 2020 17:19:43 +0100 Subject: [PATCH 12/16] Extract HandlerProvider to own class --- config/services.yaml | 10 +++- src/Controller/Api/v1/StopsController.php | 10 ++-- .../UnsupportedModifierException.php | 5 +- ...hp => WithDestinationsDatabaseHandler.php} | 4 +- src/Modifier/IncludeDestinations.php | 7 --- src/Modifier/With.php | 18 +++++++ src/Provider/Database/DatabaseRepository.php | 50 ++++++------------- .../Database/GenericStopRepository.php | 10 ++-- src/Service/HandlerProvider.php | 44 ++++++++++++++++ 9 files changed, 103 insertions(+), 55 deletions(-) rename src/Handler/Database/{IncludeDestinationsDatabaseHandler.php => WithDestinationsDatabaseHandler.php} (97%) delete mode 100644 src/Modifier/IncludeDestinations.php create mode 100644 src/Modifier/With.php create mode 100644 src/Service/HandlerProvider.php diff --git a/config/services.yaml b/config/services.yaml index 4337891..9fe3e71 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,7 +23,7 @@ services: # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' - exclude: '../src/{DependencyInjection,Exception,Modifiers,Entity,Model,Migrations,Tests,Functions,Kernel.php}' + exclude: '../src/{DependencyInjection,Exception,Modifie,Entity,Model,Migrations,Tests,Functions,Handler,Kernel.php}' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class @@ -35,6 +35,10 @@ services: resource: '../src/Provider' public: true + App\Handler\: + resource: '../src/Handler' + tags: [ app.handler ] + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones @@ -90,3 +94,7 @@ services: # other servces App\Service\ProviderResolver: arguments: [!tagged app.provider, '%kernel.debug%'] + + App\Service\HandlerProvider: + arguments: [!tagged_locator app.handler] + shared: false diff --git a/src/Controller/Api/v1/StopsController.php b/src/Controller/Api/v1/StopsController.php index dc6ac45..2b83043 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -4,12 +4,12 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; use App\Model\Stop; -use App\Model\TrackStop; use App\Model\StopGroup; -use App\Modifier\IdFilter; +use App\Model\TrackStop; use App\Modifier\FieldFilter; -use App\Modifier\IncludeDestinations; +use App\Modifier\IdFilter; use App\Modifier\RelatedFilter; +use App\Modifier\With; use App\Provider\StopRepository; use App\Provider\TrackRepository; use Nelmio\ApiDocBundle\Annotation\Model; @@ -95,7 +95,7 @@ class StopsController extends Controller */ public function one(Request $request, StopRepository $stops, $id) { - return $this->json($stops->first(new IdFilter($id), new IncludeDestinations())); + return $this->json($stops->first(new IdFilter($id), new With("destinations"))); } /** @@ -137,7 +137,7 @@ class StopsController extends Controller } if ($request->query->has('include-destinations')) { - yield new IncludeDestinations(); + yield new With("destinations"); } } } diff --git a/src/Exception/UnsupportedModifierException.php b/src/Exception/UnsupportedModifierException.php index f1e7145..1a03b80 100644 --- a/src/Exception/UnsupportedModifierException.php +++ b/src/Exception/UnsupportedModifierException.php @@ -3,12 +3,11 @@ namespace App\Exception; use App\Modifier\Modifier; -use App\Provider\Repository; class UnsupportedModifierException extends \LogicException { - public static function createFromModifier(Modifier $modifier, Repository $repository) + public static function createFromModifier(Modifier $modifier) { - return new static(sprintf("Modifier %s is not supported by %s.", get_class($modifier), get_class($repository))); + return new static(sprintf("Modifier %s is not supported.", get_class($modifier))); } } diff --git a/src/Handler/Database/IncludeDestinationsDatabaseHandler.php b/src/Handler/Database/WithDestinationsDatabaseHandler.php similarity index 97% rename from src/Handler/Database/IncludeDestinationsDatabaseHandler.php rename to src/Handler/Database/WithDestinationsDatabaseHandler.php index b58287f..f3f7879 100644 --- a/src/Handler/Database/IncludeDestinationsDatabaseHandler.php +++ b/src/Handler/Database/WithDestinationsDatabaseHandler.php @@ -10,11 +10,11 @@ use App\Model\Stop; use App\Service\Converter; use App\Service\IdUtils; use Doctrine\ORM\EntityManagerInterface; -use Tightenco\Collect\Support\Collection; use Kadet\Functional as f; use Kadet\Functional\Transforms as t; +use Tightenco\Collect\Support\Collection; -class IncludeDestinationsDatabaseHandler implements PostProcessingHandler +class WithDestinationsDatabaseHandler implements PostProcessingHandler { private $em; private $converter; diff --git a/src/Modifier/IncludeDestinations.php b/src/Modifier/IncludeDestinations.php deleted file mode 100644 index 01d74b2..0000000 --- a/src/Modifier/IncludeDestinations.php +++ /dev/null @@ -1,7 +0,0 @@ -relationship = $relationship; + } + + public function getRelationship(): string + { + return $this->relationship; + } +} diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index 8156253..e4021ec 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -5,28 +5,26 @@ namespace App\Provider\Database; use App\Entity\ProviderEntity; use App\Event\HandleDatabaseModifierEvent; use App\Event\PostProcessEvent; -use App\Exception\UnsupportedModifierException; +use App\Handler\Database\FieldFilterDatabaseHandler; use App\Handler\Database\IdFilterDatabaseHandler; use App\Handler\Database\LimitDatabaseHandler; -use App\Handler\Database\FieldFilterDatabaseHandler; use App\Handler\Database\RelatedFilterDatabaseGenericHandler; use App\Handler\ModifierHandler; use App\Handler\PostProcessingHandler; use App\Model\Referable; +use App\Modifier\FieldFilter; use App\Modifier\IdFilter; use App\Modifier\Limit; use App\Modifier\Modifier; -use App\Modifier\FieldFilter; use App\Modifier\RelatedFilter; use App\Provider\Repository; use App\Service\Converter; +use App\Service\HandlerProvider; use App\Service\IdUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; -use Psr\Container\ContainerInterface; -use Symfony\Contracts\Service\ServiceSubscriberInterface; -abstract class DatabaseRepository implements ServiceSubscriberInterface, Repository +abstract class DatabaseRepository implements Repository { const DEFAULT_LIMIT = 100; @@ -42,7 +40,7 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit /** @var Converter */ protected $converter; - /** @var ContainerInterface */ + /** @var HandlerProvider */ protected $handlers; /** @@ -54,12 +52,19 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit EntityManagerInterface $em, IdUtils $id, Converter $converter, - ContainerInterface $handlers + HandlerProvider $handlers ) { $this->em = $em; $this->id = $id; $this->converter = $converter; $this->handlers = $handlers; + + $this->handlers->loadConfiguration(array_merge([ + IdFilter::class => IdFilterDatabaseHandler::class, + Limit::class => LimitDatabaseHandler::class, + FieldFilter::class => FieldFilterDatabaseHandler::class, + RelatedFilter::class => RelatedFilterDatabaseGenericHandler::class, + ], static::getHandlers())); } /** @return static */ @@ -88,7 +93,7 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit $reducers = []; foreach ($modifiers as $modifier) { - $handler = $this->getHandler($modifier); + $handler = $this->handlers->get($modifier); if ($handler instanceof ModifierHandler) { $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([ @@ -120,10 +125,11 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit $builder->setMaxResults(self::DEFAULT_LIMIT); $reducers = $this->processQueryBuilder($builder, $modifiers, $meta); + $result = collect($builder->getQuery()->execute())->map(\Closure::fromCallable([$this, 'convert'])); return $reducers->reduce(function ($result, $reducer) { return $reducer($result); - }, collect($builder->getQuery()->execute())->map(\Closure::fromCallable([$this, 'convert']))); + }, $result); } public function first(Modifier ...$modifiers) @@ -131,17 +137,6 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit return $this->all(Limit::count(1), ...$modifiers)->first(); } - protected function getHandler(Modifier $modifier) - { - $class = get_class($modifier); - - if (!$this->handlers->has($class)) { - throw UnsupportedModifierException::createFromModifier($modifier, $this); - } - - return $this->handlers->get($class); - } - /** * Returns array describing handlers for each modifier type. Syntax is as follows: * [ IdFilter::class => IdFilterDatabaseHandler::class ] @@ -154,17 +149,4 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit { return []; } - - /** - * @inheritDoc - */ - public static function getSubscribedServices() - { - return array_merge([ - IdFilter::class => IdFilterDatabaseHandler::class, - Limit::class => LimitDatabaseHandler::class, - FieldFilter::class => FieldFilterDatabaseHandler::class, - RelatedFilter::class => RelatedFilterDatabaseGenericHandler::class, - ], static::getHandlers()); - } } diff --git a/src/Provider/Database/GenericStopRepository.php b/src/Provider/Database/GenericStopRepository.php index 62a7a90..82e41e6 100644 --- a/src/Provider/Database/GenericStopRepository.php +++ b/src/Provider/Database/GenericStopRepository.php @@ -3,10 +3,10 @@ namespace App\Provider\Database; use App\Entity\StopEntity; -use App\Handler\Database\IncludeDestinationsDatabaseHandler; +use App\Handler\Database\WithDestinationsDatabaseHandler; use App\Model\Stop; use App\Modifier\Modifier; -use App\Modifier\IncludeDestinations; +use App\Modifier\With; use App\Provider\StopRepository; use Tightenco\Collect\Support\Collection; @@ -30,7 +30,11 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository protected static function getHandlers() { return array_merge(parent::getHandlers(), [ - IncludeDestinations::class => IncludeDestinationsDatabaseHandler::class, + With::class => function (With $modifier) { + return $modifier->getRelationship() === 'destinations' + ? WithDestinationsDatabaseHandler::class + : GenericWithHandler::class; + }, ]); } } diff --git a/src/Service/HandlerProvider.php b/src/Service/HandlerProvider.php new file mode 100644 index 0000000..59d5edb --- /dev/null +++ b/src/Service/HandlerProvider.php @@ -0,0 +1,44 @@ +handlerLocator = $handlerLocator; + } + + public function loadConfiguration(array $providers) + { + $this->configuration = $providers; + } + + public function get(Modifier $modifier) + { + $class = get_class($modifier); + + if (!array_key_exists($class, $this->configuration)) { + throw UnsupportedModifierException::createFromModifier($modifier); + } + + $handler = $this->configuration[$class]; + + if (is_callable($handler)) { + $handler = $handler($modifier); + } + + if (is_string($handler)) { + return $this->handlerLocator->get($handler); + } + + return $handler; + } +} From 50a79470e72f7875caa6593d7f24d1022f505a09 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 15 Mar 2020 12:50:38 +0100 Subject: [PATCH 13/16] WIP - Add scheduled stop repository --- .../Database/FieldFilterDatabaseHandler.php | 5 + .../Database/GenericWithDatabaseHandler.php | 99 +++++++++++++++++++ .../RelatedFilterDatabaseGenericHandler.php | 6 ++ src/Model/ScheduledStop.php | 20 +++- src/Model/Track.php | 20 +++- src/Model/Trip.php | 16 +++ src/Provider/Database/DatabaseRepository.php | 3 + .../Database/GenericScheduleRepository.php | 17 +++- .../Database/GenericStopRepository.php | 3 +- src/Provider/ScheduleRepository.php | 2 +- .../ZtmGdanskDepartureRepository.php | 65 ++++++++++-- src/Service/EntityConverter.php | 7 +- src/Service/ScheduledStopConverter.php | 6 +- 13 files changed, 247 insertions(+), 22 deletions(-) create mode 100644 src/Handler/Database/GenericWithDatabaseHandler.php diff --git a/src/Handler/Database/FieldFilterDatabaseHandler.php b/src/Handler/Database/FieldFilterDatabaseHandler.php index 0bfc83c..e7e8b1a 100644 --- a/src/Handler/Database/FieldFilterDatabaseHandler.php +++ b/src/Handler/Database/FieldFilterDatabaseHandler.php @@ -5,6 +5,7 @@ namespace App\Handler\Database; use App\Event\HandleDatabaseModifierEvent; use App\Event\HandleModifierEvent; use App\Handler\ModifierHandler; +use App\Model\ScheduledStop; use App\Model\Stop; use App\Modifier\FieldFilter; use function App\Functions\encapsulate; @@ -15,6 +16,10 @@ class FieldFilterDatabaseHandler implements ModifierHandler Stop::class => [ 'name' => 'name', ], + ScheduledStop::class => [ + 'departure' => 'departure', + 'arrival' => 'arrival', + ] ]; public function process(HandleModifierEvent $event) diff --git a/src/Handler/Database/GenericWithDatabaseHandler.php b/src/Handler/Database/GenericWithDatabaseHandler.php new file mode 100644 index 0000000..35dc3b9 --- /dev/null +++ b/src/Handler/Database/GenericWithDatabaseHandler.php @@ -0,0 +1,99 @@ + [ + 'line' => 'line', + 'stops' => 'stopsInTrack', + ], + TrackStop::class => [ + 'track' => 'track', + ], + ScheduledStop::class => [ + 'trip' => 'trip', + 'track' => 'trip.track', + 'destination' => 'trip.track.final', + ], + ]; + + private $em; + private $id; + private $references; + + public function __construct( + EntityManagerInterface $em, + IdUtils $idUtils, + EntityReferenceFactory $references + ) { + $this->em = $em; + $this->id = $idUtils; + $this->references = $references; + } + + public function process(HandleModifierEvent $event) + { + if (!$event instanceof HandleDatabaseModifierEvent) { + return; + } + + /** @var RelatedFilter $modifier */ + $modifier = $event->getModifier(); + $builder = $event->getBuilder(); + $alias = $event->getMeta()['alias']; + $type = $event->getMeta()['type']; + + if (!array_key_exists($modifier->getRelationship(), $this->mapping[$type])) { + throw new \InvalidArgumentException( + sprintf("Relationship %s is not supported for .", $type) + ); + } + + $relationship = $this->mapping[$type][$modifier->getRelationship()]; + + foreach ($this->getRelationships($relationship, $alias) as [$relationshipPath, $relationshipAlias]) { + $selected = collect($builder->getDQLPart('select'))->flatMap(property('parts')); + + if ($selected->contains($relationshipAlias)) { + continue; + } + + $builder + ->join($relationshipPath, $relationshipAlias) + ->addSelect($relationshipAlias); + } + } + + /** + * @inheritDoc + */ + public static function getSubscribedServices() + { + return [ + TrackByStopDatabaseHandler::class, + ]; + } + + private function getRelationships($relationship, $alias) + { + $relationships = explode('.', $relationship); + + foreach ($relationships as $current) { + yield [sprintf("%s.%s", $alias, $current), $alias = sprintf('%s_%s', $alias, $current)]; + } + } +} diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php index 315b99f..dbaa48d 100644 --- a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -6,9 +6,11 @@ use App\Event\HandleDatabaseModifierEvent; use App\Event\HandleModifierEvent; use App\Handler\ModifierHandler; use App\Model\Line; +use App\Model\ScheduledStop; use App\Model\Stop; use App\Model\Track; use App\Model\TrackStop; +use App\Model\Trip; use App\Modifier\RelatedFilter; use App\Service\IdUtils; use App\Service\EntityReferenceFactory; @@ -27,6 +29,10 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub Stop::class => 'stop', Track::class => 'track', ], + ScheduledStop::class => [ + Stop::class => 'stop', + Trip::class => 'trip', + ], ]; private $em; diff --git a/src/Model/ScheduledStop.php b/src/Model/ScheduledStop.php index 2ae78f2..5042a49 100644 --- a/src/Model/ScheduledStop.php +++ b/src/Model/ScheduledStop.php @@ -7,17 +7,23 @@ use Carbon\Carbon; class ScheduledStop extends TrackStop { /** - * Arrival time + * Arrival time. * @var Carbon */ private $arrival; /** - * Departure time + * Departure time. * @var Carbon */ private $departure; + /** + * Exact trip that this scheduled stop is part of. + * @var Trip|null + */ + private $trip; + public function getArrival(): Carbon { return $this->arrival; @@ -37,4 +43,14 @@ class ScheduledStop extends TrackStop { $this->departure = $departure; } + + public function getTrip(): ?Trip + { + return $this->trip; + } + + public function setTrip(?Trip $trip): void + { + $this->trip = $trip; + } } diff --git a/src/Model/Track.php b/src/Model/Track.php index bd62451..acb26f0 100644 --- a/src/Model/Track.php +++ b/src/Model/Track.php @@ -42,6 +42,14 @@ class Track implements Referable, Fillable */ private $stops; + /** + * Destination stop of this track + * @var Stop|null + * @Serializer\Type(Stop::class) + * @SWG\Property(ref=@Model(type=Stop::class)) + */ + private $destination; + /** * Track constructor. */ @@ -89,4 +97,14 @@ class Track implements Referable, Fillable { return $this->stops = collect($stops); } -} \ No newline at end of file + + public function getDestination(): ?Stop + { + return $this->destination; + } + + public function setDestination(?Stop $destination): void + { + $this->destination = $destination; + } +} diff --git a/src/Model/Trip.php b/src/Model/Trip.php index eaea219..f9da46f 100644 --- a/src/Model/Trip.php +++ b/src/Model/Trip.php @@ -40,6 +40,12 @@ class Trip implements Referable, Fillable */ private $schedule; + /** + * Destination stop of this trip + * @var Stop|null + */ + private $destination; + /** * Track constructor. */ @@ -87,4 +93,14 @@ class Trip implements Referable, Fillable { return $this->schedule = collect($schedule); } + + public function getDestination(): ?Stop + { + return $this->destination; + } + + public function setDestination(?Stop $destination): void + { + $this->destination = $destination; + } } diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index e4021ec..898a431 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -9,6 +9,7 @@ use App\Handler\Database\FieldFilterDatabaseHandler; use App\Handler\Database\IdFilterDatabaseHandler; use App\Handler\Database\LimitDatabaseHandler; use App\Handler\Database\RelatedFilterDatabaseGenericHandler; +use App\Handler\Database\GenericWithDatabaseHandler; use App\Handler\ModifierHandler; use App\Handler\PostProcessingHandler; use App\Model\Referable; @@ -17,6 +18,7 @@ use App\Modifier\IdFilter; use App\Modifier\Limit; use App\Modifier\Modifier; use App\Modifier\RelatedFilter; +use App\Modifier\With; use App\Provider\Repository; use App\Service\Converter; use App\Service\HandlerProvider; @@ -64,6 +66,7 @@ abstract class DatabaseRepository implements Repository Limit::class => LimitDatabaseHandler::class, FieldFilter::class => FieldFilterDatabaseHandler::class, RelatedFilter::class => RelatedFilterDatabaseGenericHandler::class, + With::class => GenericWithDatabaseHandler::class, ], static::getHandlers())); } diff --git a/src/Provider/Database/GenericScheduleRepository.php b/src/Provider/Database/GenericScheduleRepository.php index ed7adbe..10c4bdd 100644 --- a/src/Provider/Database/GenericScheduleRepository.php +++ b/src/Provider/Database/GenericScheduleRepository.php @@ -9,8 +9,10 @@ use App\Entity\TripEntity; use App\Entity\TripStopEntity; use App\Model\Departure; use App\Model\Line; +use App\Model\ScheduledStop; use App\Model\Stop; use App\Model\Vehicle; +use App\Modifier\Modifier; use App\Provider\ScheduleRepository; use Carbon\Carbon; use Tightenco\Collect\Support\Collection; @@ -71,8 +73,19 @@ class GenericScheduleRepository extends DatabaseRepository implements ScheduleRe }); } - protected static function getHandlers() + public function all(Modifier ...$modifiers): Collection { - return []; + $builder = $this->em + ->createQueryBuilder() + ->select('trip_stop') + ->from(TripStopEntity::class, 'trip_stop') + ->orderBy('trip_stop.departure', 'ASC') + ; + + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'trip_stop', + 'type' => ScheduledStop::class, + 'entity' => TripStopEntity::class, + ]); } } diff --git a/src/Provider/Database/GenericStopRepository.php b/src/Provider/Database/GenericStopRepository.php index 82e41e6..7610402 100644 --- a/src/Provider/Database/GenericStopRepository.php +++ b/src/Provider/Database/GenericStopRepository.php @@ -3,6 +3,7 @@ namespace App\Provider\Database; use App\Entity\StopEntity; +use App\Handler\Database\GenericWithDatabaseHandler; use App\Handler\Database\WithDestinationsDatabaseHandler; use App\Model\Stop; use App\Modifier\Modifier; @@ -33,7 +34,7 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository With::class => function (With $modifier) { return $modifier->getRelationship() === 'destinations' ? WithDestinationsDatabaseHandler::class - : GenericWithHandler::class; + : GenericWithDatabaseHandler::class; }, ]); } diff --git a/src/Provider/ScheduleRepository.php b/src/Provider/ScheduleRepository.php index 951a20f..e650d22 100644 --- a/src/Provider/ScheduleRepository.php +++ b/src/Provider/ScheduleRepository.php @@ -6,7 +6,7 @@ use App\Model\Stop; use Carbon\Carbon; use Tightenco\Collect\Support\Collection; -interface ScheduleRepository +interface ScheduleRepository extends FluentRepository { const DEFAULT_DEPARTURES_COUNT = 16; diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index e7f7dc6..48fefc2 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -4,9 +4,14 @@ namespace App\Provider\ZtmGdansk; use App\Model\Departure; use App\Model\Line; +use App\Model\ScheduledStop; use App\Model\Stop; use App\Model\Vehicle; +use App\Modifier\FieldFilter; use App\Modifier\IdFilter; +use App\Modifier\Limit; +use App\Modifier\RelatedFilter; +use App\Modifier\With; use App\Provider\Database\GenericScheduleRepository; use App\Provider\DepartureRepository; use App\Provider\LineRepository; @@ -90,13 +95,33 @@ class ZtmGdanskDepartureRepository implements DepartureRepository private function getScheduledDepartures(Stop $stop, Carbon $time) { - return $this->schedule->getDeparturesForStop($stop, $time); + return $this->schedule->all( + new RelatedFilter($stop), + new FieldFilter('departure', $time, '>='), + new With('track'), + new With('destination'), + Limit::count(16) + ); } private function pair(Collection $schedule, Collection $real) { - $key = function (Departure $departure) { - return sprintf("%s::%s", $departure->getLine()->getSymbol(), $departure->getScheduled()->format("H:i")); + $key = function ($departure) { + if ($departure instanceof Departure) { + return sprintf( + "%s::%s", + $departure->getLine()->getId(), + $departure->getScheduled()->format("H:i") + ); + } elseif ($departure instanceof ScheduledStop) { + return sprintf( + "%s::%s", + $departure->getTrack()->getLine()->getId(), + $departure->getDeparture()->format("H:i") + ); + } else { + throw new \Exception(); + } }; $schedule = $schedule->keyBy($key)->all(); @@ -110,21 +135,27 @@ class ZtmGdanskDepartureRepository implements DepartureRepository unset($schedule[$key]); } - return [ $real, $scheduled ]; - })->merge(collect($schedule)->map(function (Departure $scheduled) { - return [ null, $scheduled ]; + return [ + 'estimated' => $real, + 'scheduled' => $scheduled, + ]; + })->merge(collect($schedule)->map(function (ScheduledStop $scheduled) { + return [ + 'estimated' => null, + 'scheduled' => $scheduled, + ]; }))->map(function ($pair) { - return $this->merge(...$pair); + return $this->merge($pair['estimated'], $pair['scheduled']); })->sortBy(function (Departure $departure) { $time = $departure->getEstimated() ?? $departure->getScheduled(); return $time->getTimestamp(); }); } - private function merge(?Departure $real, ?Departure $scheduled) + private function merge(?Departure $real, ?ScheduledStop $scheduled) { if (!$real) { - return $scheduled; + return $this->convertScheduledStopToDeparture($scheduled); } if (!$scheduled) { @@ -132,10 +163,24 @@ class ZtmGdanskDepartureRepository implements DepartureRepository } $departure = clone $real; - $departure->setDisplay($scheduled->getDisplay()); + $departure->setDisplay($real->getDisplay()); $departure->setTrack($scheduled->getTrack()); $departure->setTrip($scheduled->getTrip()); return $departure; } + + private function convertScheduledStopToDeparture(ScheduledStop $stop): Departure + { + $converted = new Departure(); + + $converted->setDisplay($stop->getTrack()->getDestination()->getName()); + $converted->setLine($stop->getTrack()->getLine()); + $converted->setTrack($stop->getTrack()); + $converted->setTrip($stop->getTrip()); + $converted->setScheduled($stop->getDeparture()); + $converted->setStop($stop->getStop()); + + return $converted; + } } diff --git a/src/Service/EntityConverter.php b/src/Service/EntityConverter.php index 222a7c8..3ec6d4b 100644 --- a/src/Service/EntityConverter.php +++ b/src/Service/EntityConverter.php @@ -32,7 +32,7 @@ final class EntityConverter implements Converter, RecursiveConverter */ public function convert($entity, array $cache = []) { - if (array_key_exists($key = get_class($entity).':'.$this->getId($entity), $cache)) { + if (array_key_exists($key = get_class($entity) . ':' . $this->getId($entity), $cache)) { return $cache[$key]; } @@ -78,6 +78,7 @@ final class EntityConverter implements Converter, RecursiveConverter ->map(t\property('stop')) ->map($convert), 'line' => $convert($entity->getLine()), + 'destination' => $convert($entity->getFinal()->getStop()), ]); break; @@ -154,7 +155,7 @@ final class EntityConverter implements Converter, RecursiveConverter private function create(Entity $entity) { - $id = $this->id->of($entity); + $id = $this->id->of($entity); $class = $this->getModelClassForEntity($entity); return $class::createFromArray(['id' => $id]); @@ -162,7 +163,7 @@ final class EntityConverter implements Converter, RecursiveConverter private function reference(Entity $entity) { - $id = $this->id->strip($this->getId($entity)); + $id = $this->id->strip($this->getId($entity)); $class = $this->getModelClassForEntity($entity); return $this->reference->get($class, ['id' => $id]); diff --git a/src/Service/ScheduledStopConverter.php b/src/Service/ScheduledStopConverter.php index c00faae..d4cfc97 100644 --- a/src/Service/ScheduledStopConverter.php +++ b/src/Service/ScheduledStopConverter.php @@ -5,6 +5,7 @@ namespace App\Service; use App\Entity\TrackStopEntity; use App\Entity\TripStopEntity; use App\Model\ScheduledStop; +use App\Model\TrackStop; class ScheduledStopConverter implements Converter, RecursiveConverter { @@ -12,9 +13,8 @@ class ScheduledStopConverter implements Converter, RecursiveConverter public function convert($entity) { - if ($entity instanceof TrackStopEntity) { - return ScheduledStop::createFromArray([ + return TrackStop::createFromArray([ 'stop' => $this->parent->convert($entity->getStop()), 'track' => $this->parent->convert($entity->getTrack()), 'order' => $entity->getOrder(), @@ -27,6 +27,8 @@ class ScheduledStopConverter implements Converter, RecursiveConverter 'departure' => $entity->getDeparture(), 'stop' => $this->parent->convert($entity->getStop()), 'order' => $entity->getOrder(), + 'track' => $this->parent->convert($entity->getTrip()->getTrack()), + 'trip' => $this->parent->convert($entity->getTrip()), ]); } } From 1a0515742e2b4166bb2f31460415c8d5189f8281 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 15 Mar 2020 17:30:08 +0100 Subject: [PATCH 14/16] Add blackfire service for profiling --- docker-compose.yml | 10 +++++-- docker/php/Dockerfile | 25 +++++++++++----- src/Functions/helpers.php | 8 ++++- .../ZtmGdanskDepartureRepository.php | 30 +++++++++---------- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 24c33b1..065e519 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3.4' services: nginx: @@ -11,9 +11,15 @@ services: php: build: docker/php - mem_limit: 2g env_file: - ./docker/php/.env volumes: - ./:/var/www:cached - ./docker/php/log.conf:/usr/local/etc/php-fpm.d/zz-log.conf + + blackfire: + image: blackfire/blackfire + ports: ["8707"] + environment: + - BLACKFIRE_SERVER_ID + - BLACKFIRE_SERVER_TOKEN diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 08a914e..27e1b3a 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -7,19 +7,30 @@ RUN apt-get update && \ RUN docker-php-ext-install zip +# XDebug RUN pecl install xdebug-2.9.0 && docker-php-ext-enable xdebug - RUN echo "xdebug.remote_enable = 1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \ echo "xdebug.remote_host = ${XDEBUG_REMOTE_HOST}" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; -RUN echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini; -RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -RUN php composer-setup.php -RUN php -r "unlink('composer-setup.php');" +# Blackfire +RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \ + && curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/linux/amd64/$version \ + && mkdir -p /tmp/blackfire \ + && tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \ + && mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \ + && printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" > $PHP_INI_DIR/conf.d/blackfire.ini \ + && rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz -RUN mv composer.phar /usr/local/bin/composer -RUN chmod +x /usr/local/bin/composer +#Composer +RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ + && php composer-setup.php \ + && php -r "unlink('composer-setup.php');" \ + && mv composer.phar /usr/local/bin/composer \ + && chmod +x /usr/local/bin/composer + +# Timezone RUN ln -snf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime +RUN echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini; WORKDIR /var/www diff --git a/src/Functions/helpers.php b/src/Functions/helpers.php index 46f1c7a..57e138f 100644 --- a/src/Functions/helpers.php +++ b/src/Functions/helpers.php @@ -12,4 +12,10 @@ function encapsulate($value) default: return [ $value ]; } -} \ No newline at end of file +} + +function setup($value, $callback) +{ + $callback($value); + return $value; +} diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index 48fefc2..6053ef5 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -21,6 +21,7 @@ use Carbon\Carbon; use JMS\Serializer\Tests\Fixtures\Discriminator\Car; use Tightenco\Collect\Support\Collection; use Kadet\Functional\Transforms as t; +use function App\Functions\setup; class ZtmGdanskDepartureRepository implements DepartureRepository { @@ -162,25 +163,22 @@ class ZtmGdanskDepartureRepository implements DepartureRepository return $real; } - $departure = clone $real; - $departure->setDisplay($real->getDisplay()); - $departure->setTrack($scheduled->getTrack()); - $departure->setTrip($scheduled->getTrip()); - - return $departure; + return setup(clone $real, function (Departure $departure) use ($scheduled, $real) { + $departure->setDisplay($real->getDisplay()); + $departure->setTrack($scheduled->getTrack()); + $departure->setTrip($scheduled->getTrip()); + }); } private function convertScheduledStopToDeparture(ScheduledStop $stop): Departure { - $converted = new Departure(); - - $converted->setDisplay($stop->getTrack()->getDestination()->getName()); - $converted->setLine($stop->getTrack()->getLine()); - $converted->setTrack($stop->getTrack()); - $converted->setTrip($stop->getTrip()); - $converted->setScheduled($stop->getDeparture()); - $converted->setStop($stop->getStop()); - - return $converted; + return setup(new Departure(), function (Departure $converted) use ($stop) { + $converted->setDisplay($stop->getTrack()->getDestination()->getName()); + $converted->setLine($stop->getTrack()->getLine()); + $converted->setTrack($stop->getTrack()); + $converted->setTrip($stop->getTrip()); + $converted->setScheduled($stop->getDeparture()); + $converted->setStop($stop->getStop()); + }); } } From 3e695bfef7d28dec81a04c5ecb8d929c2fb4b45c Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 15 Mar 2020 22:35:12 +0100 Subject: [PATCH 15/16] Add Limit support for departure repository --- .../Api/v1/DeparturesController.php | 22 +++++--- .../RelatedFilterDatabaseGenericHandler.php | 2 +- src/Provider/DepartureRepository.php | 5 +- .../Dummy/DummyDepartureRepository.php | 27 +++++----- .../ZtmGdanskDepartureRepository.php | 51 ++++++++++++++++--- src/Service/ModifierUtils.php | 29 +++++++++++ 6 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 src/Service/ModifierUtils.php diff --git a/src/Controller/Api/v1/DeparturesController.php b/src/Controller/Api/v1/DeparturesController.php index da07d63..afa0798 100644 --- a/src/Controller/Api/v1/DeparturesController.php +++ b/src/Controller/Api/v1/DeparturesController.php @@ -5,7 +5,10 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; use App\Model\Departure; +use App\Modifier\FieldFilter; use App\Modifier\IdFilter; +use App\Modifier\Limit; +use App\Modifier\With; use App\Provider\DepartureRepository; use App\Provider\StopRepository; use App\Service\SerializerContextFactory; @@ -33,11 +36,11 @@ class DeparturesController extends Controller * @SWG\Schema(type="array", @SWG\Items(ref=@Model(type=Departure::class))) * ) */ - public function stop(DepartureRepository $departures, StopRepository $stops, $stop) + public function stop(DepartureRepository $departures, StopRepository $stops, $stop, Request $request) { $stop = $stops->first(new IdFilter($stop)); - return $this->json($departures->getForStop($stop)); + return $this->json($departures->current(collect($stop), ...$this->getModifiersFromRequest($request))); } /** @@ -65,16 +68,21 @@ class DeparturesController extends Controller */ public function stops(DepartureRepository $departures, StopRepository $stops, Request $request) { - $stops = $stops - ->all(new IdFilter($request->query->get('stop'))) - ->flatMap(ref([ $departures, 'getForStop' ])) - ->sortBy(property('departure')); + $stops = $stops->all(new IdFilter($request->query->get('stop'))); + $result = $departures->current($stops, ...$this->getModifiersFromRequest($request)); return $this->json( - $stops->values()->slice(0, (int)$request->query->get('limit', 8)), + $result->values()->slice(0, (int)$request->query->get('limit', 8)), 200, [], $this->serializerContextFactory->create(Departure::class, ['Default']) ); } + + private function getModifiersFromRequest(Request $request) + { + if ($request->query->has('limit')) { + yield Limit::count($request->query->getInt('limit')); + } + } } diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php index dbaa48d..f366e8e 100644 --- a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -72,7 +72,7 @@ class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSub if (!array_key_exists($modifier->getRelationship(), $this->mapping[$type])) { throw new \InvalidArgumentException( - sprintf("Relationship %s is not supported for .", $type) + sprintf("Relationship %s is not supported for %s.", $modifier->getRelationship(), $type) ); } diff --git a/src/Provider/DepartureRepository.php b/src/Provider/DepartureRepository.php index 706274c..a6ea1f4 100644 --- a/src/Provider/DepartureRepository.php +++ b/src/Provider/DepartureRepository.php @@ -5,9 +5,10 @@ namespace App\Provider; use App\Model\Stop; +use App\Modifier\Modifier; use Tightenco\Collect\Support\Collection; interface DepartureRepository extends Repository { - public function getForStop(Stop $stop): Collection; -} \ No newline at end of file + public function current(iterable $stops, Modifier ...$modifiers); +} diff --git a/src/Provider/Dummy/DummyDepartureRepository.php b/src/Provider/Dummy/DummyDepartureRepository.php index 5210943..d265ce6 100644 --- a/src/Provider/Dummy/DummyDepartureRepository.php +++ b/src/Provider/Dummy/DummyDepartureRepository.php @@ -6,6 +6,7 @@ use App\Model\Departure; use App\Model\Line; use App\Model\Stop; use App\Model\Vehicle; +use App\Modifier\Modifier; use App\Provider\DepartureRepository; use App\Service\Proxy\ReferenceFactory; use Carbon\Carbon; @@ -25,21 +26,21 @@ class DummyDepartureRepository implements DepartureRepository $this->reference = $reference; } - public function getForStop(Stop $stop): Collection + public function current(iterable $stops, Modifier ...$modifiers) { return collect([ - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], - [ 1, Line::TYPE_TRAM, 'lorem ipsum', 2137 ], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], + [1, Line::TYPE_TRAM, 'lorem ipsum', 2137], ])->map(function ($departure) use ($stop) { - list($symbol, $type, $display, $vehicle) = $departure; + [$symbol, $type, $display, $vehicle] = $departure; $scheduled = new Carbon(); $estimated = (clone $scheduled)->addSeconds(40); @@ -53,4 +54,4 @@ class DummyDepartureRepository implements DepartureRepository ]); }); } -} \ No newline at end of file +} diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index 6053ef5..eb7f401 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -10,18 +10,22 @@ use App\Model\Vehicle; use App\Modifier\FieldFilter; use App\Modifier\IdFilter; use App\Modifier\Limit; +use App\Modifier\Modifier; use App\Modifier\RelatedFilter; use App\Modifier\With; use App\Provider\Database\GenericScheduleRepository; use App\Provider\DepartureRepository; use App\Provider\LineRepository; use App\Provider\ScheduleRepository; +use App\Service\IterableUtils; +use App\Service\ModifierUtils; use App\Service\Proxy\ReferenceFactory; use Carbon\Carbon; use JMS\Serializer\Tests\Fixtures\Discriminator\Car; use Tightenco\Collect\Support\Collection; use Kadet\Functional\Transforms as t; use function App\Functions\setup; +use function Kadet\Functional\ref; class ZtmGdanskDepartureRepository implements DepartureRepository { @@ -46,16 +50,22 @@ class ZtmGdanskDepartureRepository implements DepartureRepository $this->schedule = $schedule; } - public function getForStop(Stop $stop): Collection + public function current(iterable $stops, Modifier ...$modifiers) { - $real = $this->getRealDepartures($stop); + $real = IterableUtils::toCollection($stops) + ->flatMap(ref([$this, 'getRealDepartures'])) + ->sortBy(t\property('estimated')) + ; + $now = Carbon::now()->second(0); $first = $real->map(t\getter('scheduled'))->min() ?? $now; - $scheduled = $this->getScheduledDepartures($stop, $first); + $scheduled = $this->getScheduledDepartures($stops, $first, ...$this->extractModifiers($modifiers)); - return $this->pair($scheduled, $real)->filter(function (Departure $departure) use ($now) { + $result = $this->pair($scheduled, $real)->filter(function (Departure $departure) use ($now) { return $departure->getDeparture() > $now; }); + + return $this->processResultWithModifiers($result, $modifiers); } private function getRealDepartures(Stop $stop) @@ -94,14 +104,14 @@ class ZtmGdanskDepartureRepository implements DepartureRepository })->values(); } - private function getScheduledDepartures(Stop $stop, Carbon $time) + private function getScheduledDepartures($stop, Carbon $time, Modifier ...$modifiers) { return $this->schedule->all( - new RelatedFilter($stop), + new RelatedFilter($stop, Stop::class), new FieldFilter('departure', $time, '>='), new With('track'), new With('destination'), - Limit::count(16) + ...$modifiers ); } @@ -181,4 +191,31 @@ class ZtmGdanskDepartureRepository implements DepartureRepository $converted->setStop($stop->getStop()); }); } + + private function extractModifiers(iterable $modifiers) + { + $result = []; + + /** @var Limit $limit */ + if ($limit = ModifierUtils::getOfType($modifiers, Limit::class)) { + $result[] = new Limit($limit->getOffset(), $limit->getCount() * 2); + } else { + $result[] = Limit::count(16); + } + + return $result; + } + + private function processResultWithModifiers(Collection $result, iterable $modifiers) + { + foreach ($modifiers as $modifier) { + switch (true) { + case $modifier instanceof Limit: + $result = $result->slice($modifier->getOffset(), $modifier->getCount()); + break; + } + } + + return $result; + } } diff --git a/src/Service/ModifierUtils.php b/src/Service/ModifierUtils.php new file mode 100644 index 0000000..69f0fb4 --- /dev/null +++ b/src/Service/ModifierUtils.php @@ -0,0 +1,29 @@ +first($predicate); + } + + public static function getOfType(iterable $modifiers, $class) + { + return self::get($modifiers, instance($class)); + } + + public static function hasAny(iterable $modifiers, Predicate $predicate) + { + return collect($modifiers)->contains($predicate); + } + + public static function hasAnyOfType(iterable $modifiers, $class) + { + return collect($modifiers)->contains(instance($class)); + } +} From ae05646888f118eaaa7b29028e8297104de6752c Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 16 Mar 2020 20:32:15 +0100 Subject: [PATCH 16/16] Add paginator to properly handle limits --- src/Controller/Api/v1/TripController.php | 3 ++- .../Database/GenericWithDatabaseHandler.php | 4 ++++ src/Provider/Database/DatabaseRepository.php | 6 ++++- .../Database/GenericTripRepository.php | 4 +--- .../ZtmGdanskDepartureRepository.php | 9 +++++-- src/Service/AggregateConverter.php | 10 ++++---- src/Service/CacheableConverter.php | 8 +++++++ src/Service/EntityConverter.php | 24 ++++++++++++------- 8 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 src/Service/CacheableConverter.php diff --git a/src/Controller/Api/v1/TripController.php b/src/Controller/Api/v1/TripController.php index 031f39a..b91c8cb 100644 --- a/src/Controller/Api/v1/TripController.php +++ b/src/Controller/Api/v1/TripController.php @@ -5,6 +5,7 @@ namespace App\Controller\Api\v1; use App\Controller\Controller; use App\Model\Trip; use App\Modifier\IdFilter; +use App\Modifier\With; use App\Provider\TripRepository; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -19,7 +20,7 @@ class TripController extends Controller */ public function one($id, TripRepository $repository) { - $trip = $repository->all(new IdFilter($id)); + $trip = $repository->first(new IdFilter($id), new With('schedule')); return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class)); } diff --git a/src/Handler/Database/GenericWithDatabaseHandler.php b/src/Handler/Database/GenericWithDatabaseHandler.php index 35dc3b9..26445c7 100644 --- a/src/Handler/Database/GenericWithDatabaseHandler.php +++ b/src/Handler/Database/GenericWithDatabaseHandler.php @@ -8,6 +8,7 @@ use App\Handler\ModifierHandler; use App\Model\ScheduledStop; use App\Model\Track; use App\Model\TrackStop; +use App\Model\Trip; use App\Modifier\RelatedFilter; use App\Service\EntityReferenceFactory; use App\Service\IdUtils; @@ -21,6 +22,9 @@ class GenericWithDatabaseHandler implements ModifierHandler 'line' => 'line', 'stops' => 'stopsInTrack', ], + Trip::class => [ + 'schedule' => 'stops.stop', + ], TrackStop::class => [ 'track' => 'track', ], diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index 898a431..7e7f86b 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -25,6 +25,7 @@ use App\Service\HandlerProvider; use App\Service\IdUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Tools\Pagination\Paginator; abstract class DatabaseRepository implements Repository { @@ -128,7 +129,10 @@ abstract class DatabaseRepository implements Repository $builder->setMaxResults(self::DEFAULT_LIMIT); $reducers = $this->processQueryBuilder($builder, $modifiers, $meta); - $result = collect($builder->getQuery()->execute())->map(\Closure::fromCallable([$this, 'convert'])); + $query = $builder->getQuery(); + + $paginator = new Paginator($query); + $result = collect($paginator)->map(\Closure::fromCallable([$this, 'convert'])); return $reducers->reduce(function ($result, $reducer) { return $reducer($result); diff --git a/src/Provider/Database/GenericTripRepository.php b/src/Provider/Database/GenericTripRepository.php index 6ffd652..2a10b33 100644 --- a/src/Provider/Database/GenericTripRepository.php +++ b/src/Provider/Database/GenericTripRepository.php @@ -15,9 +15,7 @@ class GenericTripRepository extends DatabaseRepository implements TripRepository $builder = $this->em ->createQueryBuilder() ->from(TripEntity::class, 'trip') - ->join('trip.stops', 'ts') - ->join('ts.stop', 's') - ->select('t', 'ts'); + ->select('trip'); return $this->allFromQueryBuilder($builder, $modifiers, [ 'alias' => 'trip', diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index eb7f401..8e8dbc4 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -174,7 +174,7 @@ class ZtmGdanskDepartureRepository implements DepartureRepository } return setup(clone $real, function (Departure $departure) use ($scheduled, $real) { - $departure->setDisplay($real->getDisplay()); + $departure->setDisplay($this->extractDisplayFromScheduledStop($scheduled)); $departure->setTrack($scheduled->getTrack()); $departure->setTrip($scheduled->getTrip()); }); @@ -183,7 +183,7 @@ class ZtmGdanskDepartureRepository implements DepartureRepository private function convertScheduledStopToDeparture(ScheduledStop $stop): Departure { return setup(new Departure(), function (Departure $converted) use ($stop) { - $converted->setDisplay($stop->getTrack()->getDestination()->getName()); + $converted->setDisplay($this->extractDisplayFromScheduledStop($stop)); $converted->setLine($stop->getTrack()->getLine()); $converted->setTrack($stop->getTrack()); $converted->setTrip($stop->getTrip()); @@ -192,6 +192,11 @@ class ZtmGdanskDepartureRepository implements DepartureRepository }); } + private function extractDisplayFromScheduledStop(ScheduledStop $stop) + { + return $stop->getTrack()->getDestination()->getName(); + } + private function extractModifiers(iterable $modifiers) { $result = []; diff --git a/src/Service/AggregateConverter.php b/src/Service/AggregateConverter.php index 45cc308..ef180c7 100644 --- a/src/Service/AggregateConverter.php +++ b/src/Service/AggregateConverter.php @@ -2,10 +2,7 @@ namespace App\Service; -use Hoa\Iterator\Recursive\Recursive; -use Symfony\Component\DependencyInjection\ServiceLocator; -use function Kadet\Functional\Predicates\equals; -use function Kadet\Functional\Predicates\method; +use Tightenco\Collect\Support\Collection; class AggregateConverter implements Converter { @@ -40,4 +37,9 @@ class AggregateConverter implements Converter return $converter->supports($entity); }); } + + public function getConverters(): Collection + { + return clone $this->converters; + } } diff --git a/src/Service/CacheableConverter.php b/src/Service/CacheableConverter.php new file mode 100644 index 0000000..cf590df --- /dev/null +++ b/src/Service/CacheableConverter.php @@ -0,0 +1,8 @@ +id = $id; $this->reference = $reference; + $this->cache = []; } /** @@ -30,21 +32,22 @@ final class EntityConverter implements Converter, RecursiveConverter * * @return Line|Track|Stop|Operator|Trip|ScheduledStop */ - public function convert($entity, array $cache = []) + public function convert($entity) { - if (array_key_exists($key = get_class($entity) . ':' . $this->getId($entity), $cache)) { - return $cache[$key]; + if (array_key_exists($key = get_class($entity) . ':' . $this->getId($entity), $this->cache)) { + return $this->cache[$key]; } if ($entity instanceof Proxy && !$entity->__isInitialized()) { return $this->reference($entity); } - $result = $this->create($entity); - $cache = $cache + [$key => $result]; - $convert = function ($entity) use ($cache) { + $result = $this->create($entity); + $this->cache[$key] = $result; + + $convert = function ($entity) { return $this->supports($entity) - ? $this->convert($entity, $cache) + ? $this->convert($entity) : $this->parent->convert($entity); }; @@ -173,4 +176,9 @@ final class EntityConverter implements Converter, RecursiveConverter { return $entity instanceof Entity; } + + public function flushCache() + { + $this->cache = []; + } }