Merge branch '35_new_repository_pattern_with_filters_and_modifiers'

This commit is contained in:
Kacper Donat 2020-03-16 21:33:15 +01:00
commit ea283a86e7
69 changed files with 1637 additions and 417 deletions

View File

@ -23,7 +23,7 @@ services:
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
resource: '../src/*' 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 # controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class # as action arguments even if you don't extend any base controller class
@ -35,6 +35,10 @@ services:
resource: '../src/Provider' resource: '../src/Provider'
public: true public: true
App\Handler\:
resource: '../src/Handler'
tags: [ app.handler ]
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones
@ -90,3 +94,7 @@ services:
# other servces # other servces
App\Service\ProviderResolver: App\Service\ProviderResolver:
arguments: [!tagged app.provider, '%kernel.debug%'] arguments: [!tagged app.provider, '%kernel.debug%']
App\Service\HandlerProvider:
arguments: [!tagged_locator app.handler]
shared: false

View File

@ -1,4 +1,4 @@
version: '2' version: '3.4'
services: services:
nginx: nginx:
@ -11,9 +11,15 @@ services:
php: php:
build: docker/php build: docker/php
mem_limit: 2g
env_file: env_file:
- ./docker/php/.env - ./docker/php/.env
volumes: volumes:
- ./:/var/www:cached - ./:/var/www:cached
- ./docker/php/log.conf:/usr/local/etc/php-fpm.d/zz-log.conf - ./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

View File

@ -1,2 +1 @@
XDEBUG_CONFIG=remote_host=172.17.0.1 remote_port=9001
PHP_IDE_CONFIG=serverName=czydojade PHP_IDE_CONFIG=serverName=czydojade

View File

@ -1,22 +1,36 @@
FROM php:7.3-fpm FROM php:7.3-fpm
ARG XDEBUG_REMOTE_HOST="172.17.0.1"
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends git zip libzip-dev apt-get install -y --no-install-recommends git zip libzip-dev
RUN docker-php-ext-install zip RUN docker-php-ext-install zip
# XDebug
RUN pecl install xdebug-2.9.0 && docker-php-ext-enable 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; # Blackfire
RUN echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini; 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');" #Composer
RUN php composer-setup.php RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
RUN php -r "unlink('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 # Timezone
RUN chmod +x /usr/local/bin/composer
RUN ln -snf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime 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 WORKDIR /var/www

View File

@ -80,7 +80,7 @@ export class FinderComponent extends Vue {
this.state = 'fetching'; 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) { if (response.ok) {
this.found = (await response.json()).reduce((accumulator, { name, stops }) => Object.assign(accumulator, { [name]: stops }), {}); this.found = (await response.json()).reduce((accumulator, { name, stops }) => Object.assign(accumulator, { [name]: stops }), {});

View File

@ -8,7 +8,11 @@ export function query(params: UrlParams = { }) {
function *simplify(name: string, param: any): IterableIterator<ParamValuePair> { function *simplify(name: string, param: any): IterableIterator<ParamValuePair> {
if (typeof param === 'string') { if (typeof param === 'string') {
yield [ name, param ]; 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() ]; yield [ name, param.toString() ];
} else if (param instanceof Array) { } else if (param instanceof Array) {
for (let entry of param) { for (let entry of param) {

View File

@ -5,6 +5,10 @@ namespace App\Controller\Api\v1;
use App\Controller\Controller; use App\Controller\Controller;
use App\Model\Departure; 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\DepartureRepository;
use App\Provider\StopRepository; use App\Provider\StopRepository;
use App\Service\SerializerContextFactory; use App\Service\SerializerContextFactory;
@ -32,11 +36,11 @@ class DeparturesController extends Controller
* @SWG\Schema(type="array", @SWG\Items(ref=@Model(type=Departure::class))) * @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) public function stops(DepartureRepository $departures, StopRepository $stops, Request $request)
{ {
$stops = $stops $stops = $stops->all(new IdFilter($request->query->get('stop')));
->getManyById($request->query->get('stop')) $result = $departures->current($stops, ...$this->getModifiersFromRequest($request));
->flatMap(ref([ $departures, 'getForStop' ]))
->sortBy(property('departure'));
return $this->json( return $this->json(
$stops->values()->slice(0, (int)$request->query->get('limit', 8)), $result->values()->slice(0, (int)$request->query->get('limit', 8)),
200, 200,
[], [],
$this->serializerContextFactory->create(Departure::class, ['Default']) $this->serializerContextFactory->create(Departure::class, ['Default'])
); );
} }
private function getModifiersFromRequest(Request $request)
{
if ($request->query->has('limit')) {
yield Limit::count($request->query->getInt('limit'));
}
}
} }

View File

@ -1,15 +1,17 @@
<?php <?php
namespace App\Controller\Api\v1; namespace App\Controller\Api\v1;
use App\Controller\Controller; use App\Controller\Controller;
use App\Model\Stop; use App\Model\Stop;
use App\Model\Track;
use App\Model\StopGroup; use App\Model\StopGroup;
use App\Model\TrackStop;
use App\Modifier\FieldFilter;
use App\Modifier\IdFilter;
use App\Modifier\RelatedFilter;
use App\Modifier\With;
use App\Provider\StopRepository; use App\Provider\StopRepository;
use App\Provider\TrackRepository; use App\Provider\TrackRepository;
use App\Service\Proxy\ReferenceFactory;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -38,7 +40,8 @@ class StopsController extends Controller
* name="id", * name="id",
* in="query", * in="query",
* type="array", * type="array",
* description="Stop identificators to retrieve at once. Can be used to bulk load data. If not specified will return all data.", * description="Stop identificators to retrieve at once. Can be used to bulk load data. If not specified will
* return all data.",
* @SWG\Items(type="string") * @SWG\Items(type="string")
* ) * )
* *
@ -46,16 +49,9 @@ class StopsController extends Controller
*/ */
public function index(Request $request, StopRepository $stops) public function index(Request $request, StopRepository $stops)
{ {
switch (true) { $modifiers = $this->getModifiersFromRequest($request);
case $request->query->has('id'):
$result = $stops->getManyById($request->query->get('id'));
break;
default: return $this->json($stops->all(...$modifiers)->toArray());
$result = $stops->getAll();
}
return $this->json($result->all());
} }
/** /**
@ -76,16 +72,9 @@ class StopsController extends Controller
*/ */
public function groups(Request $request, StopRepository $stops) public function groups(Request $request, StopRepository $stops)
{ {
switch (true) { $modifiers = $this->getModifiersFromRequest($request);
case $request->query->has('name'):
$result = $stops->findByName($request->query->get('name'));
break;
default: return $this->json(static::group($stops->all(...$modifiers))->toArray());
$result = $stops->getAll();
}
return $this->json(static::group($result)->all());
} }
/** /**
@ -106,7 +95,7 @@ class StopsController extends Controller
*/ */
public function one(Request $request, StopRepository $stops, $id) 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( * @SWG\Response(
* response=200, * response=200,
* description="Returns specific stop referenced via identificator.", * description="Returns specific stop referenced via identificator.",
* @SWG\Schema(type="object", properties={ * @SWG\Schema(ref=@Model(type=TrackStop::class))
* @SWG\Property(property="track", type="object", ref=@Model(type=Track::class)),
* @SWG\Property(property="order", type="integer", minimum="0")
* })
* ) * )
*
* @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->stops(new RelatedFilter(Stop::reference($id))));
return $this->json($tracks->getByStop($stop)->map(function ($tuple) {
return array_combine(['track', 'order'], $tuple);
}));
} }
public static function group(Collection $stops) public static function group(Collection $stops)
@ -145,4 +125,19 @@ class StopsController extends Controller
return $group; return $group;
})->values(); })->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");
}
}
} }

View File

@ -3,18 +3,24 @@
namespace App\Controller\Api\v1; namespace App\Controller\Api\v1;
use App\Controller\Controller; use App\Controller\Controller;
use App\Model\Line;
use App\Model\Stop; use App\Model\Stop;
use App\Model\Track; use App\Model\Track;
use App\Modifier\IdFilter;
use App\Modifier\RelatedFilter;
use App\Provider\TrackRepository; use App\Provider\TrackRepository;
use App\Service\IterableUtils;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG; use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use function App\Functions\encapsulate; use function App\Functions\encapsulate;
use function Kadet\Functional\ref;
/** /**
* @Route("/tracks") * @Route("/tracks")
* @SWG\Tag(name="Tracks")
*/ */
class TracksController extends Controller class TracksController extends Controller
{ {
@ -23,48 +29,69 @@ class TracksController extends Controller
* response=200, * response=200,
* description="Returns all tracks for specific provider, e.g. ZTM Gdańsk.", * description="Returns all tracks for specific provider, e.g. ZTM Gdańsk.",
* ) * )
* @SWG\Tag(name="Tracks")
* @Route("/", methods={"GET"}) * @Route("/", methods={"GET"})
*/ */
public function index(Request $request, TrackRepository $repository) public function index(Request $request, TrackRepository $repository)
{ {
switch (true) { $modifiers = $this->getModifiersFromRequest($request);
case $request->query->has('stop'):
return $this->byStop($request, $repository); return $this->json($repository->all(...$modifiers));
case $request->query->has('line'): }
return $this->byLine($request, $repository);
case $request->query->has('id'): /**
return $this->byId($request, $repository); * @Route("/stops", methods={"GET"})
default: * @Route("/{track}/stops", methods={"GET"})
throw new BadRequestHttpException( */
sprintf( public function stops(Request $request, TrackRepository $repository)
'At least one parameter of %s must be set.', {
implode(', ', ['stop', 'line', 'id']) $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));
}
}

View File

@ -4,6 +4,8 @@ namespace App\Controller\Api\v1;
use App\Controller\Controller; use App\Controller\Controller;
use App\Model\Trip; use App\Model\Trip;
use App\Modifier\IdFilter;
use App\Modifier\With;
use App\Provider\TripRepository; use App\Provider\TripRepository;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@ -18,7 +20,7 @@ class TripController extends Controller
*/ */
public function one($id, TripRepository $repository) 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)); return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class));
} }

View File

@ -45,16 +45,18 @@ class TrackEntity implements Entity, Fillable
/** /**
* Stops in track * 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"}) * @ORM\OrderBy({"order": "ASC"})
*/ */
private $stopsInTrack; private $stopsInTrack;
/** /**
* Final stop in this track. * 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; private $final;
@ -114,7 +116,7 @@ class TrackEntity implements Entity, Fillable
$this->final = $this->stopsInTrack->last(); $this->final = $this->stopsInTrack->last();
} }
public function getFinal(): StopInTrack public function getFinal(): TrackStopEntity
{ {
return $this->final; return $this->final;
} }

View File

@ -14,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\UniqueConstraint(name="stop_in_track_idx", columns={"stop_id", "track_id", "sequence"}) * @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; use FillTrait, ReferableEntityTrait;

View File

@ -0,0 +1,34 @@
<?php
namespace App\Event;
use App\Event\HandleModifierEvent;
use App\Modifier\Modifier;
use App\Provider\Repository;
use Doctrine\ORM\QueryBuilder;
class HandleDatabaseModifierEvent extends HandleModifierEvent
{
private $builder;
public function __construct(
Modifier $modifier,
Repository $repository,
QueryBuilder $builder,
array $meta = []
) {
parent::__construct($modifier, $repository, $meta);
$this->builder = $builder;
}
public function getBuilder(): QueryBuilder
{
return $this->builder;
}
public function replaceBuilder(QueryBuilder $builder): void
{
$this->builder = $builder;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Event;
use App\Modifier\Modifier;
use App\Provider\Repository;
class HandleModifierEvent
{
private $repository;
private $modifier;
private $meta = [];
public function __construct(Modifier $modifier, Repository $repository, array $meta = [])
{
$this->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;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Event;
use App\Modifier\Modifier;
use App\Provider\Repository;
class PostProcessEvent extends HandleModifierEvent
{
private $data;
public function __construct($data, Modifier $modifier, Repository $repository, array $meta = [])
{
parent::__construct($modifier, $repository, $meta);
$this->data = $data;
}
public function getData()
{
return $this->data;
}
public function setData($data): void
{
$this->data = $data;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Exception;
class InvalidArgumentException extends \InvalidArgumentException
{
public static function invalidType($parameter, $value, array $expected = [])
{
return new static(
sprintf('Expected %s to be of type: %s. %s given.', $parameter, implode(', ', $expected), gettype($value))
);
}
}

View File

@ -4,7 +4,6 @@
namespace App\Exception; namespace App\Exception;
class NonExistentServiceException extends \Exception class NonExistentServiceException extends \LogicException
{ {
}
}

View File

@ -2,6 +2,6 @@
namespace App\Exception; namespace App\Exception;
class NotSupportedException extends \RuntimeException class NotSupportedException extends \LogicException
{ {
} }

View File

@ -0,0 +1,13 @@
<?php
namespace App\Exception;
use App\Modifier\Modifier;
class UnsupportedModifierException extends \LogicException
{
public static function createFromModifier(Modifier $modifier)
{
return new static(sprintf("Modifier %s is not supported.", get_class($modifier)));
}
}

View File

@ -12,4 +12,10 @@ function encapsulate($value)
default: default:
return [ $value ]; return [ $value ];
} }
} }
function setup($value, $callback)
{
$callback($value);
return $value;
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\ScheduledStop;
use App\Model\Stop;
use App\Modifier\FieldFilter;
use function App\Functions\encapsulate;
class FieldFilterDatabaseHandler implements ModifierHandler
{
protected $mapping = [
Stop::class => [
'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];
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\ScheduledStop;
use App\Model\Track;
use App\Model\TrackStop;
use App\Model\Trip;
use App\Modifier\RelatedFilter;
use App\Service\EntityReferenceFactory;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use function Kadet\Functional\Transforms\property;
class GenericWithDatabaseHandler implements ModifierHandler
{
protected $mapping = [
Track::class => [
'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)];
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Handler\Database;
use App\Handler\ModifierHandler;
use App\Modifier\IdFilter;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Service\IdUtils;
use function Kadet\Functional\apply;
class IdFilterDatabaseHandler implements ModifierHandler
{
/**
* @var IdUtils
*/
private $id;
public function __construct(IdUtils $id)
{
$this->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));
;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Modifier\Limit;
class LimitDatabaseHandler implements ModifierHandler
{
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var Limit $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$builder
->setFirstResult($modifier->getOffset())
->setMaxResults($modifier->getCount())
;
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\Line;
use App\Model\ScheduledStop;
use App\Model\Stop;
use App\Model\Track;
use App\Model\TrackStop;
use App\Model\Trip;
use App\Modifier\RelatedFilter;
use App\Service\IdUtils;
use App\Service\EntityReferenceFactory;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSubscriberInterface
{
protected $mapping = [
Track::class => [
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,
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Handler\Database;
use App\Entity\TrackStopEntity;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Modifier\RelatedFilter;
use App\Service\EntityReferenceFactory;
class TrackByStopDatabaseHandler implements ModifierHandler
{
private $references;
public function __construct(EntityReferenceFactory $references)
{
$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'];
$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)
;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Handler\Database;
use App\Entity\TrackEntity;
use App\Event\PostProcessEvent;
use App\Handler\PostProcessingHandler;
use App\Model\Destination;
use App\Model\Stop;
use App\Service\Converter;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use Kadet\Functional as f;
use Kadet\Functional\Transforms as t;
use Tightenco\Collect\Support\Collection;
class WithDestinationsDatabaseHandler implements PostProcessingHandler
{
private $em;
private $converter;
private $id;
public function __construct(EntityManagerInterface $entityManager, Converter $converter, IdUtils $id)
{
$this->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())]);
});
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Handler;
use App\Event\HandleModifierEvent;
interface ModifierHandler
{
public function process(HandleModifierEvent $event);
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Handler;
use App\Event\PostProcessEvent;
interface PostProcessingHandler
{
public function postProcess(PostProcessEvent $event);
}

View File

@ -138,4 +138,4 @@ class Line implements Fillable, Referable
{ {
$this->operator = $operator; $this->operator = $operator;
} }
} }

View File

@ -4,53 +4,25 @@ namespace App\Model;
use Carbon\Carbon; use Carbon\Carbon;
class ScheduledStop implements Fillable class ScheduledStop extends TrackStop
{ {
use FillTrait;
/** /**
* Stop (as a place) related to that scheduled bus stop * Arrival time.
* @var Stop
*/
private $stop;
/**
* Order in trip
* @var int
*/
private $order;
/**
* Arrival time
* @var Carbon * @var Carbon
*/ */
private $arrival; private $arrival;
/** /**
* Departure time * Departure time.
* @var Carbon * @var Carbon
*/ */
private $departure; private $departure;
public function getStop() /**
{ * Exact trip that this scheduled stop is part of.
return $this->stop; * @var Trip|null
} */
private $trip;
public function setStop($stop): void
{
$this->stop = $stop;
}
public function getOrder(): int
{
return $this->order;
}
public function setOrder(int $order): void
{
$this->order = $order;
}
public function getArrival(): Carbon public function getArrival(): Carbon
{ {
@ -71,4 +43,14 @@ class ScheduledStop implements Fillable
{ {
$this->departure = $departure; $this->departure = $departure;
} }
public function getTrip(): ?Trip
{
return $this->trip;
}
public function setTrip(?Trip $trip): void
{
$this->trip = $trip;
}
} }

View File

@ -42,6 +42,14 @@ class Track implements Referable, Fillable
*/ */
private $stops; 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. * Track constructor.
*/ */
@ -89,4 +97,14 @@ class Track implements Referable, Fillable
{ {
return $this->stops = collect($stops); return $this->stops = collect($stops);
} }
}
public function getDestination(): ?Stop
{
return $this->destination;
}
public function setDestination(?Stop $destination): void
{
$this->destination = $destination;
}
}

58
src/Model/TrackStop.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace App\Model;
use JMS\Serializer\Annotation as Serializer;
class TrackStop implements Fillable
{
use FillTrait;
/**
* Order in trip
* @var int
*/
private $order;
/**
* Stop (as a place) related to that scheduled bus stop
* @var Stop
*/
private $stop;
/**
* Track that this stop is part of.
* @var Track|null
*/
private $track;
public function getStop()
{
return $this->stop;
}
public function setStop($stop): void
{
$this->stop = $stop;
}
public function getOrder(): int
{
return $this->order;
}
public function setOrder(int $order): void
{
$this->order = $order;
}
public function getTrack(): ?Track
{
return $this->track;
}
public function setTrack(?Track $track): void
{
$this->track = $track;
}
}

View File

@ -40,6 +40,12 @@ class Trip implements Referable, Fillable
*/ */
private $schedule; private $schedule;
/**
* Destination stop of this trip
* @var Stop|null
*/
private $destination;
/** /**
* Track constructor. * Track constructor.
*/ */
@ -87,4 +93,14 @@ class Trip implements Referable, Fillable
{ {
return $this->schedule = collect($schedule); return $this->schedule = collect($schedule);
} }
public function getDestination(): ?Stop
{
return $this->destination;
}
public function setDestination(?Stop $destination): void
{
$this->destination = $destination;
}
} }

View File

@ -0,0 +1,37 @@
<?php
namespace App\Modifier;
class FieldFilter implements Modifier
{
private $field;
private $value;
private $operator;
public function __construct(string $field, $value, string $operator = '=')
{
$this->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;
}
}

32
src/Modifier/IdFilter.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace App\Modifier;
use App\Exception\InvalidArgumentException;
use App\Modifier\Modifier;
use App\Service\IterableUtils;
class IdFilter implements Modifier
{
/** @var string|array */
private $id;
public function __construct($id)
{
if (!is_iterable($id) && !is_string($id)) {
throw InvalidArgumentException::invalidType('id', $id, ['string', 'array']);
}
$this->id = is_iterable($id) ? IterableUtils::toArray($id) : $id;
}
public function getId()
{
return $this->id;
}
public function isMultiple()
{
return is_array($this->id);
}
}

30
src/Modifier/Limit.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Modifier;
class Limit implements Modifier
{
private $offset;
private $count;
public function __construct(int $offset = 0, ?int $count = null)
{
$this->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);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Modifier;
interface Modifier
{
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Modifier;
use App\Exception\InvalidArgumentException;
use App\Model\Referable;
use App\Service\IterableUtils;
class RelatedFilter implements Modifier
{
private $relationship;
private $reference;
public function __construct($reference, ?string $relation = null)
{
if (!is_iterable($reference) && !$reference instanceof Referable) {
throw InvalidArgumentException::invalidType('object', $reference, [Referable::class, 'iterable']);
}
$this->reference = is_iterable($reference) ? IterableUtils::toArray($reference) : $reference;
$this->relationship = $relation ?: get_class($reference);
}
public function getRelationship(): string
{
return $this->relationship;
}
public function getRelated()
{
return $this->reference;
}
public function isMultiple()
{
return is_array($this->reference);
}
}

18
src/Modifier/With.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Modifier;
class With implements Modifier
{
private $relationship;
public function __construct(string $relationship)
{
$this->relationship = $relationship;
}
public function getRelationship(): string
{
return $this->relationship;
}
}

View File

@ -2,16 +2,35 @@
namespace App\Provider\Database; namespace App\Provider\Database;
use App\Entity\Entity;
use App\Entity\ProviderEntity; 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\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\Converter;
use App\Service\HandlerProvider;
use App\Service\IdUtils; use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface; 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 */ /** @var EntityManagerInterface */
protected $em; protected $em;
@ -24,22 +43,38 @@ class DatabaseRepository
/** @var Converter */ /** @var Converter */
protected $converter; protected $converter;
/** @var HandlerProvider */
protected $handlers;
/** /**
* DatabaseRepository constructor. * DatabaseRepository constructor.
* *
* @param EntityManagerInterface $em * @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->em = $em;
$this->id = $id; $this->id = $id;
$this->converter = $converter; $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 */ /** @return static */
public function withProvider(ProviderEntity $provider) public function withProvider(ProviderEntity $provider)
{ {
$result = clone $this; $result = clone $this;
$result->provider = $provider; $result->provider = $provider;
return $result; return $result;
@ -56,4 +91,69 @@ class DatabaseRepository
return $this->em->getReference($class, $id); 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 [];
}
} }

View File

@ -3,34 +3,32 @@
namespace App\Provider\Database; namespace App\Provider\Database;
use App\Entity\LineEntity; 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\Model\Line;
use App\Modifier\Limit;
use App\Modifier\IdFilter;
use App\Provider\LineRepository; use App\Provider\LineRepository;
use App\Modifier\Modifier;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
use Kadet\Functional as f; use Kadet\Functional as f;
class GenericLineRepository extends DatabaseRepository implements LineRepository class GenericLineRepository extends DatabaseRepository implements LineRepository
{ {
public function getAll(): Collection public function all(Modifier ...$modifiers): Collection
{ {
$repository = $this->em->getRepository(LineEntity::class); $builder = $this->em
$lines = $repository->findAll(); ->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']));
}
}

View File

@ -2,32 +2,26 @@
namespace App\Provider\Database; namespace App\Provider\Database;
use App\Entity\OperatorEntity;
use App\Model\Operator; use App\Model\Operator;
use App\Modifier\Modifier;
use App\Provider\OperatorRepository; use App\Provider\OperatorRepository;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
class GenericOperatorRepository extends DatabaseRepository implements OperatorRepository class GenericOperatorRepository extends DatabaseRepository implements OperatorRepository
{ {
public function getAll(): Collection public function all(Modifier ...$modifiers): Collection
{ {
$repository = $this->em->getRepository(Operator::class); $builder = $this->em
$operators = $repository->findAll(); ->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);
}
}

View File

@ -3,14 +3,16 @@
namespace App\Provider\Database; namespace App\Provider\Database;
use App\Entity\StopEntity; use App\Entity\StopEntity;
use App\Entity\StopInTrack; use App\Entity\TrackStopEntity;
use App\Entity\TrackEntity; use App\Entity\TrackEntity;
use App\Entity\TripEntity; use App\Entity\TripEntity;
use App\Entity\TripStopEntity; use App\Entity\TripStopEntity;
use App\Model\Departure; use App\Model\Departure;
use App\Model\Line; use App\Model\Line;
use App\Model\ScheduledStop;
use App\Model\Stop; use App\Model\Stop;
use App\Model\Vehicle; use App\Model\Vehicle;
use App\Modifier\Modifier;
use App\Provider\ScheduleRepository; use App\Provider\ScheduleRepository;
use Carbon\Carbon; use Carbon\Carbon;
use Tightenco\Collect\Support\Collection; 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,
]);
}
} }

View File

@ -3,84 +3,39 @@
namespace App\Provider\Database; namespace App\Provider\Database;
use App\Entity\StopEntity; use App\Entity\StopEntity;
use App\Entity\TrackEntity; use App\Handler\Database\GenericWithDatabaseHandler;
use App\Model\Destination; use App\Handler\Database\WithDestinationsDatabaseHandler;
use App\Model\Stop; use App\Model\Stop;
use App\Modifier\Modifier;
use App\Modifier\With;
use App\Provider\StopRepository; use App\Provider\StopRepository;
use Kadet\Functional as f;
use Kadet\Functional\Transforms as t;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
class GenericStopRepository extends DatabaseRepository implements StopRepository 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); return array_merge(parent::getHandlers(), [
$stop = $this->em->getRepository(StopEntity::class)->find($id); With::class => function (With $modifier) {
return $modifier->getRelationship() === 'destinations'
return $this->convert($stop); ? WithDestinationsDatabaseHandler::class
} : GenericWithDatabaseHandler::class;
},
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())]);
});
} }
} }

View File

@ -2,66 +2,41 @@
namespace App\Provider\Database; namespace App\Provider\Database;
use App\Entity\LineEntity; use App\Entity\TrackStopEntity;
use App\Entity\StopEntity;
use App\Entity\StopInTrack;
use App\Entity\TrackEntity; use App\Entity\TrackEntity;
use function App\Functions\encapsulate; use App\Model\TrackStop;
use App\Model\Stop; use App\Modifier\Modifier;
use App\Model\Track; use App\Model\Track;
use App\Provider\TrackRepository; use App\Provider\TrackRepository;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
use Kadet\Functional as f;
class GenericTrackRepository extends DatabaseRepository implements TrackRepository 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']));
}
}

View File

@ -4,25 +4,23 @@ namespace App\Provider\Database;
use App\Entity\TripEntity; use App\Entity\TripEntity;
use App\Model\Trip; use App\Model\Trip;
use App\Modifier\Modifier;
use App\Provider\TripRepository; use App\Provider\TripRepository;
use Tightenco\Collect\Support\Collection;
class GenericTripRepository extends DatabaseRepository implements TripRepository 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); $builder = $this->em
$trip = $this->em
->createQueryBuilder() ->createQueryBuilder()
->from(TripEntity::class, 't') ->from(TripEntity::class, 'trip')
->join('t.stops', 'ts') ->select('trip');
->join('ts.stop', 's')
->select('t', 'ts')
->where('t.id = :id')
->getQuery()
->setParameter('id', $id)
->getOneOrNullResult();
return $this->convert($trip); return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'trip',
'entity' => TripEntity::class,
'type' => Trip::class,
]);
} }
} }

View File

@ -5,9 +5,10 @@ namespace App\Provider;
use App\Model\Stop; use App\Model\Stop;
use App\Modifier\Modifier;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
interface DepartureRepository extends Repository interface DepartureRepository extends Repository
{ {
public function getForStop(Stop $stop): Collection; public function current(iterable $stops, Modifier ...$modifiers);
} }

View File

@ -6,6 +6,7 @@ use App\Model\Departure;
use App\Model\Line; use App\Model\Line;
use App\Model\Stop; use App\Model\Stop;
use App\Model\Vehicle; use App\Model\Vehicle;
use App\Modifier\Modifier;
use App\Provider\DepartureRepository; use App\Provider\DepartureRepository;
use App\Service\Proxy\ReferenceFactory; use App\Service\Proxy\ReferenceFactory;
use Carbon\Carbon; use Carbon\Carbon;
@ -25,21 +26,21 @@ class DummyDepartureRepository implements DepartureRepository
$this->reference = $reference; $this->reference = $reference;
} }
public function getForStop(Stop $stop): Collection public function current(iterable $stops, Modifier ...$modifiers)
{ {
return collect([ 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) { ])->map(function ($departure) use ($stop) {
list($symbol, $type, $display, $vehicle) = $departure; [$symbol, $type, $display, $vehicle] = $departure;
$scheduled = new Carbon(); $scheduled = new Carbon();
$estimated = (clone $scheduled)->addSeconds(40); $estimated = (clone $scheduled)->addSeconds(40);
@ -53,4 +54,4 @@ class DummyDepartureRepository implements DepartureRepository
]); ]);
}); });
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Provider\Dummy; namespace App\Provider\Dummy;
use App\Model\Stop; use App\Model\Stop;
use App\Modifier\Modifier;
use App\Provider\StopRepository; use App\Provider\StopRepository;
use App\Service\Proxy\ReferenceFactory; use App\Service\Proxy\ReferenceFactory;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
@ -41,4 +42,14 @@ class DummyStopRepository implements StopRepository
{ {
return collect(); return collect();
} }
public function first(Modifier ...$modifiers)
{
// TODO: Implement first() method.
}
public function all(Modifier ...$modifiers): Collection
{
// TODO: Implement all() method.
}
} }

View File

@ -0,0 +1,12 @@
<?php
namespace App\Provider;
use App\Modifier\Modifier;
use Tightenco\Collect\Support\Collection;
interface FluentRepository extends Repository
{
public function first(Modifier ...$modifiers);
public function all(Modifier ...$modifiers): Collection;
}

View File

@ -3,14 +3,6 @@
namespace App\Provider; namespace App\Provider;
interface LineRepository extends FluentRepository
use App\Model\Line;
use Tightenco\Collect\Support\Collection;
interface LineRepository extends Repository
{ {
public function getAll(): Collection; }
public function getById($id): ?Line;
public function getManyById($ids): Collection;
}

View File

@ -7,9 +7,6 @@ namespace App\Provider;
use App\Model\Operator; use App\Model\Operator;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
interface OperatorRepository interface OperatorRepository extends FluentRepository
{ {
public function getAll(): Collection; }
public function getById($id): ?Operator;
public function getManyById($ids): Collection;
}

View File

@ -6,5 +6,4 @@ namespace App\Provider;
interface Repository interface Repository
{ {
}
}

View File

@ -6,7 +6,7 @@ use App\Model\Stop;
use Carbon\Carbon; use Carbon\Carbon;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
interface ScheduleRepository interface ScheduleRepository extends FluentRepository
{ {
const DEFAULT_DEPARTURES_COUNT = 16; const DEFAULT_DEPARTURES_COUNT = 16;

View File

@ -7,10 +7,6 @@ namespace App\Provider;
use App\Model\Stop; use App\Model\Stop;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
interface StopRepository extends Repository interface StopRepository extends FluentRepository
{ {
public function getAll(): Collection;
public function getById($id): ?Stop;
public function getManyById($ids): Collection;
public function findByName(string $name): Collection;
} }

View File

@ -3,15 +3,10 @@
namespace App\Provider; namespace App\Provider;
use App\Model\Track; use App\Model\Track;
use App\Modifier\Modifier;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
interface TrackRepository interface TrackRepository extends FluentRepository
{ {
public function getAll(): Collection; public function stops(Modifier ...$modifiers): Collection;
}
public function getById($id): Track;
public function getManyById($ids): Collection;
public function getByStop($stop): Collection;
public function getByLine($line): Collection;
}

View File

@ -4,7 +4,6 @@ namespace App\Provider;
use App\Model\Trip; use App\Model\Trip;
interface TripRepository interface TripRepository extends FluentRepository
{ {
public function getById(string $id): Trip;
} }

View File

@ -6,7 +6,7 @@ use App\Entity\LineEntity;
use App\Entity\OperatorEntity; use App\Entity\OperatorEntity;
use App\Entity\ProviderEntity; use App\Entity\ProviderEntity;
use App\Entity\StopEntity; use App\Entity\StopEntity;
use App\Entity\StopInTrack; use App\Entity\TrackStopEntity;
use App\Entity\TrackEntity; use App\Entity\TrackEntity;
use App\Entity\TripEntity; use App\Entity\TripEntity;
use App\Entity\TripStopEntity; use App\Entity\TripStopEntity;
@ -216,7 +216,7 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
return !in_array($stop['stopId'], $this->stopBlacklist); return !in_array($stop['stopId'], $this->stopBlacklist);
}) })
->map(function ($stop) use ($entity, $provider) { ->map(function ($stop) use ($entity, $provider) {
return StopInTrack::createFromArray([ return TrackStopEntity::createFromArray([
'stop' => $this->em->getReference( 'stop' => $this->em->getReference(
StopEntity::class, StopEntity::class,
$this->ids->generate($provider, $stop['stopId']) $this->ids->generate($provider, $stop['stopId'])

View File

@ -4,17 +4,28 @@ namespace App\Provider\ZtmGdansk;
use App\Model\Departure; use App\Model\Departure;
use App\Model\Line; use App\Model\Line;
use App\Model\ScheduledStop;
use App\Model\Stop; use App\Model\Stop;
use App\Model\Vehicle; 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\Database\GenericScheduleRepository;
use App\Provider\DepartureRepository; use App\Provider\DepartureRepository;
use App\Provider\LineRepository; use App\Provider\LineRepository;
use App\Provider\ScheduleRepository; use App\Provider\ScheduleRepository;
use App\Service\IterableUtils;
use App\Service\ModifierUtils;
use App\Service\Proxy\ReferenceFactory; use App\Service\Proxy\ReferenceFactory;
use Carbon\Carbon; use Carbon\Carbon;
use JMS\Serializer\Tests\Fixtures\Discriminator\Car; use JMS\Serializer\Tests\Fixtures\Discriminator\Car;
use Tightenco\Collect\Support\Collection; use Tightenco\Collect\Support\Collection;
use Kadet\Functional\Transforms as t; use Kadet\Functional\Transforms as t;
use function App\Functions\setup;
use function Kadet\Functional\ref;
class ZtmGdanskDepartureRepository implements DepartureRepository class ZtmGdanskDepartureRepository implements DepartureRepository
{ {
@ -39,16 +50,22 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
$this->schedule = $schedule; $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); $now = Carbon::now()->second(0);
$first = $real->map(t\getter('scheduled'))->min() ?? $now; $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 $departure->getDeparture() > $now;
}); });
return $this->processResultWithModifiers($result, $modifiers);
} }
private function getRealDepartures(Stop $stop) private function getRealDepartures(Stop $stop)
@ -65,7 +82,8 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
$lines = $estimates->map(function ($delay) { $lines = $estimates->map(function ($delay) {
return $delay['routeId']; return $delay['routeId'];
})->unique(); })->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) { return collect($estimates)->map(function ($delay) use ($stop, $lines) {
$scheduled = (new Carbon($delay['theoreticalTime'], 'Europe/Warsaw'))->tz('UTC'); $scheduled = (new Carbon($delay['theoreticalTime'], 'Europe/Warsaw'))->tz('UTC');
@ -86,15 +104,35 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
})->values(); })->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) private function pair(Collection $schedule, Collection $real)
{ {
$key = function (Departure $departure) { $key = function ($departure) {
return sprintf("%s::%s", $departure->getLine()->getSymbol(), $departure->getScheduled()->format("H:i")); 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(); $schedule = $schedule->keyBy($key)->all();
@ -108,32 +146,81 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
unset($schedule[$key]); unset($schedule[$key]);
} }
return [ $real, $scheduled ]; return [
})->merge(collect($schedule)->map(function (Departure $scheduled) { 'estimated' => $real,
return [ null, $scheduled ]; 'scheduled' => $scheduled,
];
})->merge(collect($schedule)->map(function (ScheduledStop $scheduled) {
return [
'estimated' => null,
'scheduled' => $scheduled,
];
}))->map(function ($pair) { }))->map(function ($pair) {
return $this->merge(...$pair); return $this->merge($pair['estimated'], $pair['scheduled']);
})->sortBy(function (Departure $departure) { })->sortBy(function (Departure $departure) {
$time = $departure->getEstimated() ?? $departure->getScheduled(); $time = $departure->getEstimated() ?? $departure->getScheduled();
return $time->getTimestamp(); return $time->getTimestamp();
}); });
} }
private function merge(?Departure $real, ?Departure $scheduled) private function merge(?Departure $real, ?ScheduledStop $scheduled)
{ {
if (!$real) { if (!$real) {
return $scheduled; return $this->convertScheduledStopToDeparture($scheduled);
} }
if (!$scheduled) { if (!$scheduled) {
return $real; return $real;
} }
$departure = clone $real; return setup(clone $real, function (Departure $departure) use ($scheduled, $real) {
$departure->setDisplay($scheduled->getDisplay()); $departure->setDisplay($this->extractDisplayFromScheduledStop($scheduled));
$departure->setTrack($scheduled->getTrack()); $departure->setTrack($scheduled->getTrack());
$departure->setTrip($scheduled->getTrip()); $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;
} }
} }

View File

@ -2,10 +2,7 @@
namespace App\Service; namespace App\Service;
use Hoa\Iterator\Recursive\Recursive; use Tightenco\Collect\Support\Collection;
use Symfony\Component\DependencyInjection\ServiceLocator;
use function Kadet\Functional\Predicates\equals;
use function Kadet\Functional\Predicates\method;
class AggregateConverter implements Converter class AggregateConverter implements Converter
{ {
@ -40,4 +37,9 @@ class AggregateConverter implements Converter
return $converter->supports($entity); return $converter->supports($entity);
}); });
} }
public function getConverters(): Collection
{
return clone $this->converters;
}
} }

View File

@ -0,0 +1,8 @@
<?php
namespace App\Service;
interface CacheableConverter extends Converter
{
public function flushCache();
}

View File

@ -11,17 +11,19 @@ use Kadet\Functional as f;
use Kadet\Functional\Transforms as t; use Kadet\Functional\Transforms as t;
use const Kadet\Functional\_; use const Kadet\Functional\_;
final class EntityConverter implements Converter, RecursiveConverter final class EntityConverter implements Converter, RecursiveConverter, CacheableConverter
{ {
use RecursiveConverterTrait; use RecursiveConverterTrait;
private $id; private $id;
private $reference; private $reference;
private $cache;
public function __construct(IdUtils $id, ReferenceFactory $reference) public function __construct(IdUtils $id, ReferenceFactory $reference)
{ {
$this->id = $id; $this->id = $id;
$this->reference = $reference; $this->reference = $reference;
$this->cache = [];
} }
/** /**
@ -30,21 +32,22 @@ final class EntityConverter implements Converter, RecursiveConverter
* *
* @return Line|Track|Stop|Operator|Trip|ScheduledStop * @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)) { if (array_key_exists($key = get_class($entity) . ':' . $this->getId($entity), $this->cache)) {
return $cache[$key]; return $this->cache[$key];
} }
if ($entity instanceof Proxy && !$entity->__isInitialized()) { if ($entity instanceof Proxy && !$entity->__isInitialized()) {
return $this->reference($entity); return $this->reference($entity);
} }
$result = $this->create($entity); $result = $this->create($entity);
$cache = $cache + [$key => $result]; $this->cache[$key] = $result;
$convert = function ($entity) use ($cache) {
$convert = function ($entity) {
return $this->supports($entity) return $this->supports($entity)
? $this->convert($entity, $cache) ? $this->convert($entity)
: $this->parent->convert($entity); : $this->parent->convert($entity);
}; };
@ -78,6 +81,7 @@ final class EntityConverter implements Converter, RecursiveConverter
->map(t\property('stop')) ->map(t\property('stop'))
->map($convert), ->map($convert),
'line' => $convert($entity->getLine()), 'line' => $convert($entity->getLine()),
'destination' => $convert($entity->getFinal()->getStop()),
]); ]);
break; break;
@ -154,7 +158,7 @@ final class EntityConverter implements Converter, RecursiveConverter
private function create(Entity $entity) private function create(Entity $entity)
{ {
$id = $this->id->of($entity); $id = $this->id->of($entity);
$class = $this->getModelClassForEntity($entity); $class = $this->getModelClassForEntity($entity);
return $class::createFromArray(['id' => $id]); return $class::createFromArray(['id' => $id]);
@ -162,7 +166,7 @@ final class EntityConverter implements Converter, RecursiveConverter
private function reference(Entity $entity) private function reference(Entity $entity)
{ {
$id = $this->id->strip($this->getId($entity)); $id = $this->id->strip($this->getId($entity));
$class = $this->getModelClassForEntity($entity); $class = $this->getModelClassForEntity($entity);
return $this->reference->get($class, ['id' => $id]); return $this->reference->get($class, ['id' => $id]);
@ -172,4 +176,9 @@ final class EntityConverter implements Converter, RecursiveConverter
{ {
return $entity instanceof Entity; return $entity instanceof Entity;
} }
public function flushCache()
{
$this->cache = [];
}
} }

View File

@ -0,0 +1,68 @@
<?php
namespace App\Service;
use App\Entity\LineEntity;
use App\Entity\ProviderEntity;
use App\Entity\StopEntity;
use App\Entity\TrackEntity;
use App\Exception\InvalidArgumentException;
use App\Model\Line;
use App\Model\Referable;
use App\Model\Stop;
use App\Model\Track;
use Doctrine\ORM\EntityManagerInterface;
use Tightenco\Collect\Support\Collection;
use function Kadet\Functional\partial;
use function Kadet\Functional\ref;
use const Kadet\Functional\_;
final class EntityReferenceFactory
{
protected $mapping = [
Line::class => 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())
);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Service;
use App\Exception\UnsupportedModifierException;
use App\Modifier\Modifier;
use Symfony\Component\DependencyInjection\ServiceLocator;
class HandlerProvider
{
private $configuration = [];
private $handlerLocator;
public function __construct(ServiceLocator $handlerLocator)
{
$this->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;
}
}

View File

@ -11,6 +11,7 @@ class IdUtils
public function generate(ProviderEntity $provider, $id) public function generate(ProviderEntity $provider, $id)
{ {
// todo: use array cache if not fast enough
return sprintf('%s%s%s', $provider->getId(), self::DELIMITER, $id); return sprintf('%s%s%s', $provider->getId(), self::DELIMITER, $id);
} }
@ -23,4 +24,4 @@ class IdUtils
{ {
return $this->strip($entity->getId()); return $this->strip($entity->getId());
} }
} }

View File

@ -0,0 +1,29 @@
<?php
namespace App\Service;
use Kadet\Functional\Predicate;
use function Kadet\Functional\Predicates\instance;
final class ModifierUtils
{
public static function get(iterable $modifiers, Predicate $predicate)
{
return collect($modifiers)->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));
}
}

View File

@ -2,8 +2,10 @@
namespace App\Service; namespace App\Service;
use App\Entity\TrackStopEntity;
use App\Entity\TripStopEntity; use App\Entity\TripStopEntity;
use App\Model\ScheduledStop; use App\Model\ScheduledStop;
use App\Model\TrackStop;
class ScheduledStopConverter implements Converter, RecursiveConverter class ScheduledStopConverter implements Converter, RecursiveConverter
{ {
@ -11,18 +13,29 @@ class ScheduledStopConverter implements Converter, RecursiveConverter
public function convert($entity) 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([ if ($entity instanceof TripStopEntity) {
'arrival' => $entity->getArrival(), return ScheduledStop::createFromArray([
'departure' => $entity->getDeparture(), 'arrival' => $entity->getArrival(),
'stop' => $this->parent->convert($entity->getStop()), 'departure' => $entity->getDeparture(),
'order' => $entity->getOrder(), '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) public function supports($entity)
{ {
return $entity instanceof TripStopEntity; return $entity instanceof TripStopEntity
|| $entity instanceof TrackStopEntity;
} }
} }

View File

@ -66,8 +66,11 @@ const config = {
new GenerateSW({ new GenerateSW({
navigationPreload: true, navigationPreload: true,
runtimeCaching: [{ runtimeCaching: [{
urlPattern: ({event}) => event.request.mode === 'navigate', urlPattern: ({ event }) => event.request.mode === 'navigate',
handler: 'NetworkFirst', handler: 'NetworkFirst',
}, {
urlPattern: /^https?:\/\/api\.maptiler\.com\//,
handler: 'CacheFirst',
}], }],
swDest: '../service-worker.js' swDest: '../service-worker.js'
}) })