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
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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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 }), {});

View File

@ -8,6 +8,10 @@ export function query(params: UrlParams = { }) {
function *simplify(name: string, param: any): IterableIterator<ParamValuePair> {
if (typeof param === 'string') {
yield [ name, param ];
} else if (typeof param === 'boolean') {
if (param) {
yield [ name, '1' ];
}
} else if (typeof param === 'number') {
yield [ name, param.toString() ];
} else if (param instanceof Array) {

View File

@ -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'));
}
}
}

View File

@ -1,15 +1,17 @@
<?php
namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Stop;
use App\Model\Track;
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\TrackRepository;
use App\Service\Proxy\ReferenceFactory;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request;
@ -38,7 +40,8 @@ class StopsController extends Controller
* name="id",
* in="query",
* 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")
* )
*
@ -46,16 +49,9 @@ class StopsController extends Controller
*/
public function index(Request $request, StopRepository $stops)
{
switch (true) {
case $request->query->has('id'):
$result = $stops->getManyById($request->query->get('id'));
break;
$modifiers = $this->getModifiersFromRequest($request);
default:
$result = $stops->getAll();
}
return $this->json($result->all());
return $this->json($stops->all(...$modifiers)->toArray());
}
/**
@ -76,16 +72,9 @@ class StopsController extends Controller
*/
public function groups(Request $request, StopRepository $stops)
{
switch (true) {
case $request->query->has('name'):
$result = $stops->findByName($request->query->get('name'));
break;
$modifiers = $this->getModifiersFromRequest($request);
default:
$result = $stops->getAll();
}
return $this->json(static::group($result)->all());
return $this->json(static::group($stops->all(...$modifiers))->toArray());
}
/**
@ -106,7 +95,7 @@ class StopsController extends Controller
*/
public function one(Request $request, StopRepository $stops, $id)
{
return $this->json($stops->getById($id));
return $this->json($stops->first(new IdFilter($id), new 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");
}
}
}

View File

@ -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));
}
private function byId(Request $request, TrackRepository $repository)
/**
* @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'));
return $this->json($repository->getManyById($id));
yield new IdFilter($id);
}
}
private function byStop(Request $request, TrackRepository $repository)
private function getStopsModifiersFromRequest(Request $request)
{
$stop = $request->query->get('stop');
$stop = array_map([Stop::class, 'reference'], encapsulate($stop));
if ($request->query->has('stop')) {
$stop = encapsulate($request->query->get('stop'));
$stop = collect($stop)->map(ref([Stop::class, 'reference']));
return $this->json($repository->getByStop($stop));
yield new RelatedFilter($stop);
}
private function byLine(Request $request, TrackRepository $repository)
{
$line = $request->query->get('line');
$line = array_map([Stop::class, 'reference'], encapsulate($line));
if ($request->query->has('track') || $request->attributes->has('track')) {
$track = $request->get('track');
$track = Track::reference($track);
return $this->json($repository->getByLine($line));
yield new RelatedFilter($track);
}
if ($request->query->has('id')) {
$id = encapsulate($request->query->get('id'));
yield new IdFilter($id);
}
}
}

View File

@ -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));
}

View File

@ -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;
}

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"})
* })
*/
class StopInTrack implements Fillable, Referable
class TrackStopEntity implements Fillable, Referable
{
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;
class NonExistentServiceException extends \Exception
class NonExistentServiceException extends \LogicException
{
}

View File

@ -2,6 +2,6 @@
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

@ -13,3 +13,9 @@ function encapsulate($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

@ -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;
}
}

View File

@ -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);
}
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;
/**
* 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;
}
}

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;
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,16 +43,32 @@ 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 */
@ -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 [];
}
}

View File

@ -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']));
}
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']));
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'line',
'entity' => LineEntity::class,
'type' => Line::class,
]);
}
}

View File

@ -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);
}
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);
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'operator',
'entity' => OperatorEntity::class,
'type' => Operator::class,
]);
}
}

View File

@ -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,
]);
}
}

View File

@ -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']));
}
public function getById($id): ?Stop
{
$id = $this->id->generate($this->provider, $id);
$stop = $this->em->getRepository(StopEntity::class)->find($id);
return $this->convert($stop);
}
public function getManyById($ids): Collection
{
$ids = collect($ids)->map(f\apply(f\ref([$this->id, 'generate']), $this->provider));
$stops = $this->em->getRepository(StopEntity::class)->findBy(['id' => $ids->all()]);
return collect($stops)->map(f\ref([$this, 'convert']));
}
public function findByName(string $name): Collection
{
$query = $this->em->createQueryBuilder()
->select('s')
->from(StopEntity::class, 's')
->where('s.name LIKE :name')
->getQuery();
$stops = collect($query->execute([':name' => "%$name%"]));
$destinations = collect($this->em->createQueryBuilder()
->select('t', 'tl', 'f', 'fs', 'ts')
->from(TrackEntity::class, 't')
->join('t.stopsInTrack', 'ts')
->join('t.line', 'tl')
->where('ts.stop IN (:stops)')
->join('t.final', 'f')
->join('f.stop', 'fs')
->getQuery()
->execute(['stops' => $stops->map(t\property('id'))->all()]))
->reduce(function ($grouped, TrackEntity $track) {
foreach ($track->getStopsInTrack()->map(t\property('stop'))->map(t\property('id')) as $stop) {
$grouped[$stop] = ($grouped[$stop] ?? collect())->add($track);
}
return $grouped;
}, collect())
->map(function (Collection $tracks) {
return $tracks
->groupBy(function (TrackEntity $track) {
return $track->getFinal()->getStop()->getId();
})->map(function (Collection $tracks, $id) {
return Destination::createFromArray([
'stop' => $this->convert($tracks->first()->getFinal()->getStop()),
'lines' => $tracks
->map(t\property('line'))
->unique(t\property('id'))
->map(f\ref([$this, 'convert']))
->values(),
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'stop',
'entity' => StopEntity::class,
'type' => Stop::class,
]);
})->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())]);
});
protected static function getHandlers()
{
return array_merge(parent::getHandlers(), [
With::class => function (With $modifier) {
return $modifier->getRelationship() === 'destinations'
? WithDestinationsDatabaseHandler::class
: GenericWithDatabaseHandler::class;
},
]);
}
}

View File

@ -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');
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']));
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'track',
'entity' => TrackEntity::class,
'type' => Track::class,
]);
}
}

View File

@ -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,
]);
}
}

View File

@ -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;
public function current(iterable $stops, Modifier ...$modifiers);
}

View File

@ -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);

View File

@ -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.
}
}

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;
use App\Model\Line;
use Tightenco\Collect\Support\Collection;
interface LineRepository extends Repository
interface LineRepository extends FluentRepository
{
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 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
{
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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());
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;
}
}

View File

@ -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;
}
}

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 const Kadet\Functional\_;
final class EntityConverter implements Converter, RecursiveConverter
final class EntityConverter implements Converter, RecursiveConverter, CacheableConverter
{
use RecursiveConverterTrait;
private $id;
private $reference;
private $cache;
public function __construct(IdUtils $id, ReferenceFactory $reference)
{
$this->id = $id;
$this->reference = $reference;
$this->cache = [];
}
/**
@ -30,10 +32,10 @@ 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()) {
@ -41,10 +43,11 @@ final class EntityConverter implements Converter, RecursiveConverter
}
$result = $this->create($entity);
$cache = $cache + [$key => $result];
$convert = function ($entity) use ($cache) {
$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;
@ -172,4 +176,9 @@ final class EntityConverter implements Converter, RecursiveConverter
{
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)
{
// todo: use array cache if not fast enough
return sprintf('%s%s%s', $provider->getId(), self::DELIMITER, $id);
}

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;
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(),
]);
}
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;
}
}

View File

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