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..669e58f 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -1,12 +1,15 @@ 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 +73,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 +96,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 +135,19 @@ class StopsController extends Controller return $group; })->values(); } + + private function getModifiersFromRequest(Request $request) + { + if ($request->query->has('name')) { + yield FieldFilter::contains('name', $request->query->get('name')); + } + + if ($request->query->has('id')) { + yield new IdFilter($request->query->get('id')); + } + + if ($request->query->has('include-destinations')) { + yield new IncludeDestinations(); + } + } } diff --git a/src/Controller/Api/v1/TracksController.php b/src/Controller/Api/v1/TracksController.php index 069a338..71b7ae8 100644 --- a/src/Controller/Api/v1/TracksController.php +++ b/src/Controller/Api/v1/TracksController.php @@ -3,9 +3,13 @@ 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 App\Service\IterableUtils; use Nelmio\ApiDocBundle\Annotation\Model; use Swagger\Annotations as SWG; use Symfony\Component\HttpFoundation\Request; @@ -28,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->getManyById($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 = array_map([Stop::class, 'reference'], encapsulate($line)); - - return $this->json($repository->getByLine($line)); - } -} \ No newline at end of file +} 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/Event/HandleDatabaseModifierEvent.php b/src/Event/HandleDatabaseModifierEvent.php new file mode 100644 index 0000000..c548d59 --- /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..5544af4 --- /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/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/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 @@ + [ + '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); + + if ($operator === 'in' || $operator === 'not in') { + $parameter = "($parameter)"; + $value = encapsulate($value); + } + + $builder + ->andWhere(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/Handler/Database/IdFilterDatabaseHandler.php b/src/Handler/Database/IdFilterDatabaseHandler.php new file mode 100644 index 0000000..9f95d94 --- /dev/null +++ b/src/Handler/Database/IdFilterDatabaseHandler.php @@ -0,0 +1,44 @@ +id = $id; + } + + public function process(HandleModifierEvent $event) + { + if (!$event instanceof HandleDatabaseModifierEvent) { + return; + } + + /** @var IdFilter $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 + ->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 new file mode 100644 index 0000000..b58287f --- /dev/null +++ b/src/Handler/Database/IncludeDestinationsDatabaseHandler.php @@ -0,0 +1,76 @@ +em = $entityManager; + $this->converter = $converter; + $this->id = $id; + } + + public function postProcess(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/Handler/Database/LimitDatabaseHandler.php b/src/Handler/Database/LimitDatabaseHandler.php new file mode 100644 index 0000000..3f445a9 --- /dev/null +++ b/src/Handler/Database/LimitDatabaseHandler.php @@ -0,0 +1,27 @@ +getModifier(); + $builder = $event->getBuilder(); + + $builder + ->setFirstResult($modifier->getOffset()) + ->setMaxResults($modifier->getCount()) + ; + } +} diff --git a/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php new file mode 100644 index 0000000..6a2683d --- /dev/null +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -0,0 +1,96 @@ + [ + Line::class => 'line', + Stop::class => TrackByStopDatabaseHandler::class, + ], + ]; + + private $em; + private $inner; + private $id; + private $references; + + 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) + { + 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()]; + + 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->references->create($modifier->getRelated(), $event->getMeta()['provider']); + + $builder + ->join(sprintf('%s.%s', $alias, $relationship), $relationship) + ->andWhere(sprintf("%s = %s", $relationship, $parameter)) + ->setParameter($parameter, $reference) + ; + } + + /** + * @inheritDoc + */ + public static function getSubscribedServices() + { + return [ + 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/Handler/ModifierHandler.php b/src/Handler/ModifierHandler.php new file mode 100644 index 0000000..237f87d --- /dev/null +++ b/src/Handler/ModifierHandler.php @@ -0,0 +1,10 @@ +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/Modifier/IdFilter.php b/src/Modifier/IdFilter.php new file mode 100644 index 0000000..f7b6a13 --- /dev/null +++ b/src/Modifier/IdFilter.php @@ -0,0 +1,31 @@ +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/Modifier/IncludeDestinations.php b/src/Modifier/IncludeDestinations.php new file mode 100644 index 0000000..01d74b2 --- /dev/null +++ b/src/Modifier/IncludeDestinations.php @@ -0,0 +1,7 @@ +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/Modifier/Modifier.php b/src/Modifier/Modifier.php new file mode 100644 index 0000000..e1b3e62 --- /dev/null +++ b/src/Modifier/Modifier.php @@ -0,0 +1,8 @@ +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 3a97eff..60ad5ca 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -2,15 +2,31 @@ namespace App\Provider\Database; -use App\Entity\Entity; use App\Entity\ProviderEntity; +use App\Event\HandleDatabaseModifierEvent; +use App\Event\PostProcessEvent; +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; 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 +40,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 +80,87 @@ class DatabaseRepository return $this->em->getReference($class, $id); } + + protected function processQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = []) + { + $reducers = []; + + foreach ($modifiers as $modifier) { + $handler = $this->getHandler($modifier); + + if ($handler instanceof ModifierHandler) { + $event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([ + 'provider' => $this->provider, + ], $meta)); + + $handler->process($event); + } + + if ($handler instanceof PostProcessingHandler) { + $reducers[] = function ($result) use ($meta, $modifier, $handler) { + $event = new PostProcessEvent($result, $modifier, $this, array_merge([ + 'provider' => $this->provider, + ], $meta)); + + $handler->postProcess($event); + + return $event->getData(); + }; + } + + } + + 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); + } + + /** + * 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 static function getHandlers() + { + 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/GenericLineRepository.php b/src/Provider/Database/GenericLineRepository.php index e9bab49..9ca147d 100644 --- a/src/Provider/Database/GenericLineRepository.php +++ b/src/Provider/Database/GenericLineRepository.php @@ -3,34 +3,32 @@ namespace App\Provider\Database; use App\Entity\LineEntity; +use App\Event\HandleDatabaseModifierEvent; +use App\Handler\Database\LimitDatabaseHandler; +use App\Handler\Database\IdFilterDatabaseHandler; +use App\Handler\ModifierHandler; use App\Model\Line; +use App\Modifier\Limit; +use App\Modifier\IdFilter; use App\Provider\LineRepository; +use App\Modifier\Modifier; use Tightenco\Collect\Support\Collection; use Kadet\Functional as f; class GenericLineRepository extends DatabaseRepository implements LineRepository { - public function getAll(): Collection + public function all(Modifier ...$modifiers): Collection { - $repository = $this->em->getRepository(LineEntity::class); - $lines = $repository->findAll(); + $builder = $this->em + ->createQueryBuilder() + ->from(LineEntity::class, 'line') + ->select('line') + ; - return collect($lines)->map(f\ref([$this, 'convert'])); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'line', + 'entity' => LineEntity::class, + 'type' => Line::class, + ]); } - - public function getById($id): ?Line - { - $repository = $this->em->getRepository(LineEntity::class); - return $this->convert($repository->find($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'])); - } -} \ No newline at end of file +} diff --git a/src/Provider/Database/GenericOperatorRepository.php b/src/Provider/Database/GenericOperatorRepository.php index 946eff6..2af85f7 100644 --- a/src/Provider/Database/GenericOperatorRepository.php +++ b/src/Provider/Database/GenericOperatorRepository.php @@ -2,32 +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); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'operator', + 'entity' => OperatorEntity::class, + 'type' => Operator::class, + ]); } - - 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); - } -} \ No newline at end of file +} 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..62a7a90 100644 --- a/src/Provider/Database/GenericStopRepository.php +++ b/src/Provider/Database/GenericStopRepository.php @@ -3,84 +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'])); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'stop', + 'entity' => StopEntity::class, + 'type' => Stop::class, + ]); } - public function getById($id): ?Stop + protected static function getHandlers() { - $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 array_merge(parent::getHandlers(), [ + IncludeDestinations::class => IncludeDestinationsDatabaseHandler::class, + ]); } } diff --git a/src/Provider/Database/GenericTrackRepository.php b/src/Provider/Database/GenericTrackRepository.php index 9a25d44..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,19 +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'])); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'track', + 'entity' => TrackEntity::class, + 'type' => Track::class, + ]); } -} \ No newline at end of file +} diff --git a/src/Provider/Database/GenericTripRepository.php b/src/Provider/Database/GenericTripRepository.php index e85f178..6ffd652 100644 --- a/src/Provider/Database/GenericTripRepository.php +++ b/src/Provider/Database/GenericTripRepository.php @@ -4,25 +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); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'trip', + 'entity' => TripEntity::class, + 'type' => Trip::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 new file mode 100644 index 0000000..a02831c --- /dev/null +++ b/src/Provider/FluentRepository.php @@ -0,0 +1,12 @@ +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'); 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 +} 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()) + ); + } +}