diff --git a/config/services.yaml b/config/services.yaml index 853ef0a..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,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/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/.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..27e1b3a 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,22 +1,36 @@ 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 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 "xdebug.remote_enable = 1" >> /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; +# 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 php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -RUN php composer-setup.php -RUN php -r "unlink('composer-setup.php');" +#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 -RUN mv composer.phar /usr/local/bin/composer -RUN 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/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..afa0798 100644 --- a/src/Controller/Api/v1/DeparturesController.php +++ b/src/Controller/Api/v1/DeparturesController.php @@ -5,6 +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; @@ -32,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->getById($stop); + $stop = $stops->first(new IdFilter($stop)); - return $this->json($departures->getForStop($stop)); + return $this->json($departures->current(collect($stop), ...$this->getModifiersFromRequest($request))); } /** @@ -64,16 +68,21 @@ class DeparturesController extends Controller */ public function stops(DepartureRepository $departures, StopRepository $stops, Request $request) { - $stops = $stops - ->getManyById($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/Controller/Api/v1/StopsController.php b/src/Controller/Api/v1/StopsController.php index 76c19a1..2b83043 100644 --- a/src/Controller/Api/v1/StopsController.php +++ b/src/Controller/Api/v1/StopsController.php @@ -1,15 +1,17 @@ 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 With("destinations"))); } /** @@ -115,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) @@ -145,4 +125,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 With("destinations"); + } + } } diff --git a/src/Controller/Api/v1/TracksController.php b/src/Controller/Api/v1/TracksController.php index 069a338..54cece5 100644 --- a/src/Controller/Api/v1/TracksController.php +++ b/src/Controller/Api/v1/TracksController.php @@ -3,18 +3,24 @@ 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; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use function App\Functions\encapsulate; +use function Kadet\Functional\ref; /** * @Route("/tracks") + * @SWG\Tag(name="Tracks") */ class TracksController extends Controller { @@ -23,48 +29,69 @@ 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) { - 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)); + } + + /** + * @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')) { + $stop = encapsulate($request->query->get('stop')); + $stop = collect($stop)->map([Stop::class, 'reference']); + + yield new RelatedFilter($stop, Stop::class); + } + + if ($request->query->has('line')) { + $line = encapsulate($request->query->get('line')); + $line = collect($line)->map([Line::class, 'reference']); + + yield new RelatedFilter($line, Line::class); + } + + if ($request->query->has('id')) { + $id = encapsulate($request->query->get('id')); + + yield new IdFilter($id); } } - private function byId(Request $request, TrackRepository $repository) + private function getStopsModifiersFromRequest(Request $request) { - $id = encapsulate($request->query->get('id')); + if ($request->query->has('stop')) { + $stop = encapsulate($request->query->get('stop')); + $stop = collect($stop)->map(ref([Stop::class, 'reference'])); - return $this->json($repository->getManyById($id)); + 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); + } } - - 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..b91c8cb 100644 --- a/src/Controller/Api/v1/TripController.php +++ b/src/Controller/Api/v1/TripController.php @@ -4,6 +4,8 @@ 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; @@ -18,7 +20,7 @@ class TripController extends Controller */ public function one($id, TripRepository $repository) { - $trip = $repository->getById($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/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/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/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..14789e4 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,13 @@ + [ + 'name' => 'name', + ], + ScheduledStop::class => [ + 'departure' => 'departure', + 'arrival' => 'arrival', + ] + ]; + + 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/GenericWithDatabaseHandler.php b/src/Handler/Database/GenericWithDatabaseHandler.php new file mode 100644 index 0000000..26445c7 --- /dev/null +++ b/src/Handler/Database/GenericWithDatabaseHandler.php @@ -0,0 +1,103 @@ + [ + 'line' => 'line', + 'stops' => 'stopsInTrack', + ], + Trip::class => [ + 'schedule' => 'stops.stop', + ], + 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/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/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..f366e8e --- /dev/null +++ b/src/Handler/Database/RelatedFilterDatabaseGenericHandler.php @@ -0,0 +1,107 @@ + [ + Line::class => 'line', + Stop::class => TrackByStopDatabaseHandler::class, + ], + TrackStop::class => [ + Stop::class => 'stop', + Track::class => 'track', + ], + ScheduledStop::class => [ + Stop::class => 'stop', + Trip::class => 'trip', + ], + ]; + + private $em; + private $inner; + private $id; + private $references; + + public function __construct( + ContainerInterface $inner, + EntityManagerInterface $em, + IdUtils $idUtils, + EntityReferenceFactory $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 %s.", $modifier->getRelationship(), $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($modifier->isMultiple() ? "%s in (%s)" : "%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..fe84aa0 --- /dev/null +++ b/src/Handler/Database/TrackByStopDatabaseHandler.php @@ -0,0 +1,45 @@ +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']); + + $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($condition, $parameter)) + ->setParameter($parameter, $reference) + ; + } +} diff --git a/src/Handler/Database/WithDestinationsDatabaseHandler.php b/src/Handler/Database/WithDestinationsDatabaseHandler.php new file mode 100644 index 0000000..f3f7879 --- /dev/null +++ b/src/Handler/Database/WithDestinationsDatabaseHandler.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/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 @@ +operator = $operator; } -} \ No newline at end of file +} diff --git a/src/Model/ScheduledStop.php b/src/Model/ScheduledStop.php index abfda2d..5042a49 100644 --- a/src/Model/ScheduledStop.php +++ b/src/Model/ScheduledStop.php @@ -4,53 +4,25 @@ 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 + * Arrival time. * @var Carbon */ private $arrival; /** - * Departure time + * Departure time. * @var Carbon */ 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; - } + /** + * Exact trip that this scheduled stop is part of. + * @var Trip|null + */ + private $trip; public function getArrival(): Carbon { @@ -71,4 +43,14 @@ class ScheduledStop implements Fillable { $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/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/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/Modifier/FieldFilter.php b/src/Modifier/FieldFilter.php new file mode 100644 index 0000000..e813949 --- /dev/null +++ b/src/Modifier/FieldFilter.php @@ -0,0 +1,37 @@ +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..0e791a4 --- /dev/null +++ b/src/Modifier/IdFilter.php @@ -0,0 +1,32 @@ +id = is_iterable($id) ? IterableUtils::toArray($id) : $id; + } + + public function getId() + { + return $this->id; + } + + public function isMultiple() + { + return is_array($this->id); + } +} diff --git a/src/Modifier/Limit.php b/src/Modifier/Limit.php new file mode 100644 index 0000000..7b73f8d --- /dev/null +++ b/src/Modifier/Limit.php @@ -0,0 +1,30 @@ +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 @@ +reference = is_iterable($reference) ? IterableUtils::toArray($reference) : $reference; + $this->relationship = $relation ?: get_class($reference); + } + + public function getRelationship(): string + { + return $this->relationship; + } + + public function getRelated() + { + return $this->reference; + } + + public function isMultiple() + { + return is_array($this->reference); + } +} diff --git a/src/Modifier/With.php b/src/Modifier/With.php new file mode 100644 index 0000000..bbbbd67 --- /dev/null +++ b/src/Modifier/With.php @@ -0,0 +1,18 @@ +relationship = $relationship; + } + + public function getRelationship(): string + { + return $this->relationship; + } +} diff --git a/src/Provider/Database/DatabaseRepository.php b/src/Provider/Database/DatabaseRepository.php index 3a97eff..7e7f86b 100644 --- a/src/Provider/Database/DatabaseRepository.php +++ b/src/Provider/Database/DatabaseRepository.php @@ -2,16 +2,35 @@ namespace App\Provider\Database; -use App\Entity\Entity; use App\Entity\ProviderEntity; +use App\Event\HandleDatabaseModifierEvent; +use App\Event\PostProcessEvent; +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; +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\Repository; use App\Service\Converter; +use App\Service\HandlerProvider; use App\Service\IdUtils; use Doctrine\ORM\EntityManagerInterface; -use Kadet\Functional as f; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Tools\Pagination\Paginator; -class DatabaseRepository +abstract class DatabaseRepository implements Repository { + const DEFAULT_LIMIT = 100; + /** @var EntityManagerInterface */ protected $em; @@ -24,22 +43,38 @@ class DatabaseRepository /** @var Converter */ protected $converter; + /** @var HandlerProvider */ + 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, + 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, + With::class => GenericWithDatabaseHandler::class, + ], static::getHandlers())); } /** @return static */ public function withProvider(ProviderEntity $provider) { - $result = clone $this; + $result = clone $this; $result->provider = $provider; return $result; @@ -56,4 +91,69 @@ class DatabaseRepository return $this->em->getReference($class, $id); } + + protected function processQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = []) + { + $reducers = []; + + foreach ($modifiers as $modifier) { + $handler = $this->handlers->get($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 = []) + { + $builder->setMaxResults(self::DEFAULT_LIMIT); + + $reducers = $this->processQueryBuilder($builder, $modifiers, $meta); + $query = $builder->getQuery(); + + $paginator = new Paginator($query); + $result = collect($paginator)->map(\Closure::fromCallable([$this, 'convert'])); + + return $reducers->reduce(function ($result, $reducer) { + return $reducer($result); + }, $result); + } + + public function first(Modifier ...$modifiers) + { + return $this->all(Limit::count(1), ...$modifiers)->first(); + } + + /** + * 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 []; + } } 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..10c4bdd 100644 --- a/src/Provider/Database/GenericScheduleRepository.php +++ b/src/Provider/Database/GenericScheduleRepository.php @@ -3,14 +3,16 @@ 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; 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; @@ -70,4 +72,20 @@ class GenericScheduleRepository extends DatabaseRepository implements ScheduleRe ]); }); } + + public function all(Modifier ...$modifiers): Collection + { + $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 8dc1972..7610402 100644 --- a/src/Provider/Database/GenericStopRepository.php +++ b/src/Provider/Database/GenericStopRepository.php @@ -3,84 +3,39 @@ namespace App\Provider\Database; use App\Entity\StopEntity; -use App\Entity\TrackEntity; -use App\Model\Destination; +use App\Handler\Database\GenericWithDatabaseHandler; +use App\Handler\Database\WithDestinationsDatabaseHandler; use App\Model\Stop; +use App\Modifier\Modifier; +use App\Modifier\With; 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(), [ + With::class => function (With $modifier) { + return $modifier->getRelationship() === 'destinations' + ? WithDestinationsDatabaseHandler::class + : GenericWithDatabaseHandler::class; + }, + ]); } } diff --git a/src/Provider/Database/GenericTrackRepository.php b/src/Provider/Database/GenericTrackRepository.php index 9a25d44..f25d7f2 100644 --- a/src/Provider/Database/GenericTrackRepository.php +++ b/src/Provider/Database/GenericTrackRepository.php @@ -2,66 +2,41 @@ namespace App\Provider\Database; -use App\Entity\LineEntity; -use App\Entity\StopEntity; -use App\Entity\StopInTrack; +use App\Entity\TrackStopEntity; use App\Entity\TrackEntity; -use function App\Functions\encapsulate; -use App\Model\Stop; +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; class GenericTrackRepository extends DatabaseRepository implements TrackRepository { - public function getAll(): Collection + public function stops(Modifier ...$modifiers): Collection { - $tracks = $this->em->getRepository(TrackEntity::class)->findAll(); + $builder = $this->em + ->createQueryBuilder() + ->from(TrackStopEntity::class, 'track_stop') + ->select(['track_stop']); - return collect($tracks)->map(f\ref([$this, 'convert'])); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'track_stop', + 'entity' => TrackStopEntity::class, + 'type' => TrackStop::class, + ]); } - public function getById($id): Track + public function all(Modifier ...$modifiers): Collection { - // TODO: Implement getById() method. + $builder = $this->em + ->createQueryBuilder() + ->from(TrackEntity::class, 'track') + ->select('track'); + + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'track', + 'entity' => TrackEntity::class, + 'type' => Track::class, + ]); } - - public function getManyById($ids): Collection - { - // TODO: Implement getManyById() method. - } - - public function getByStop($stop): Collection - { - $reference = f\apply(f\ref([$this, 'reference']), StopEntity::class); - - $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() ]; - }); - } - - public function getByLine($line): Collection - { - $reference = f\apply(f\ref([$this, 'reference']), LineEntity::class); - - $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'])); - } -} \ No newline at end of file +} diff --git a/src/Provider/Database/GenericTripRepository.php b/src/Provider/Database/GenericTripRepository.php index e85f178..2a10b33 100644 --- a/src/Provider/Database/GenericTripRepository.php +++ b/src/Provider/Database/GenericTripRepository.php @@ -4,25 +4,23 @@ 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') - ->join('ts.stop', 's') - ->select('t', 'ts') - ->where('t.id = :id') - ->getQuery() - ->setParameter('id', $id) - ->getOneOrNullResult(); + ->from(TripEntity::class, 'trip') + ->select('trip'); - return $this->convert($trip); + return $this->allFromQueryBuilder($builder, $modifiers, [ + 'alias' => 'trip', + 'entity' => TripEntity::class, + 'type' => Trip::class, + ]); } } 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/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 @@ +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/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index 0e956bd..8e8dbc4 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -4,17 +4,28 @@ 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\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 { @@ -39,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) @@ -65,7 +82,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'); @@ -86,15 +104,35 @@ class ZtmGdanskDepartureRepository implements DepartureRepository })->values(); } - private function getScheduledDepartures(Stop $stop, Carbon $time) + private function getScheduledDepartures($stop, Carbon $time, Modifier ...$modifiers) { - return $this->schedule->getDeparturesForStop($stop, $time); + return $this->schedule->all( + new RelatedFilter($stop, Stop::class), + new FieldFilter('departure', $time, '>='), + new With('track'), + new With('destination'), + ...$modifiers + ); } 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(); @@ -108,32 +146,81 @@ 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) { return $real; } - $departure = clone $real; - $departure->setDisplay($scheduled->getDisplay()); - $departure->setTrack($scheduled->getTrack()); - $departure->setTrip($scheduled->getTrip()); + return setup(clone $real, function (Departure $departure) use ($scheduled, $real) { + $departure->setDisplay($this->extractDisplayFromScheduledStop($scheduled)); + $departure->setTrack($scheduled->getTrack()); + $departure->setTrip($scheduled->getTrip()); + }); + } - return $departure; + private function convertScheduledStopToDeparture(ScheduledStop $stop): Departure + { + return setup(new Departure(), function (Departure $converted) use ($stop) { + $converted->setDisplay($this->extractDisplayFromScheduledStop($stop)); + $converted->setLine($stop->getTrack()->getLine()); + $converted->setTrack($stop->getTrack()); + $converted->setTrip($stop->getTrip()); + $converted->setScheduled($stop->getDeparture()); + $converted->setStop($stop->getStop()); + }); + } + + private function extractDisplayFromScheduledStop(ScheduledStop $stop) + { + return $stop->getTrack()->getDestination()->getName(); + } + + 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/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); }; @@ -78,6 +81,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 +158,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 +166,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]); @@ -172,4 +176,9 @@ final class EntityConverter implements Converter, RecursiveConverter { return $entity instanceof Entity; } + + public function flushCache() + { + $this->cache = []; + } } diff --git a/src/Service/EntityReferenceFactory.php b/src/Service/EntityReferenceFactory.php new file mode 100644 index 0000000..6a5d5fd --- /dev/null +++ b/src/Service/EntityReferenceFactory.php @@ -0,0 +1,68 @@ + LineEntity::class, + Stop::class => StopEntity::class, + Track::class => TrackEntity::class, + ]; + + private $em; + private $id; + + public function __construct(EntityManagerInterface $em, IdUtils $id) + { + $this->em = $em; + $this->id = $id; + } + + 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); + + 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()) + ); + } +} 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; + } +} 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/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)); + } +} diff --git a/src/Service/ScheduledStopConverter.php b/src/Service/ScheduledStopConverter.php index da20f28..d4cfc97 100644 --- a/src/Service/ScheduledStopConverter.php +++ b/src/Service/ScheduledStopConverter.php @@ -2,8 +2,10 @@ namespace App\Service; +use App\Entity\TrackStopEntity; use App\Entity\TripStopEntity; use App\Model\ScheduledStop; +use App\Model\TrackStop; class ScheduledStopConverter implements Converter, RecursiveConverter { @@ -11,18 +13,29 @@ class ScheduledStopConverter implements Converter, RecursiveConverter public function convert($entity) { - /** @var ScheduledStop $entity */ + if ($entity instanceof TrackStopEntity) { + return TrackStop::createFromArray([ + 'stop' => $this->parent->convert($entity->getStop()), + 'track' => $this->parent->convert($entity->getTrack()), + 'order' => $entity->getOrder(), + ]); + } - return ScheduledStop::createFromArray([ - 'arrival' => $entity->getArrival(), - 'departure' => $entity->getDeparture(), - 'stop' => $this->parent->convert($entity->getStop()), - '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(), + 'track' => $this->parent->convert($entity->getTrip()->getTrack()), + 'trip' => $this->parent->convert($entity->getTrip()), + ]); + } } public function supports($entity) { - return $entity instanceof TripStopEntity; + return $entity instanceof TripStopEntity + || $entity instanceof TrackStopEntity; } } 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' })