35_new_repository_pattern_with_filters_and_modifiers #1

Manually merged
kadet merged 7 commits from 35_new_repository_pattern_with_filters_and_modifiers into master 2020-03-16 21:55:59 +01:00
46 changed files with 972 additions and 264 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,Modifiers,Entity,Model,Migrations,Tests,Functions,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class

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

View File

@ -5,6 +5,7 @@ namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Departure;
use App\Modifier\IdFilter;
use App\Provider\DepartureRepository;
use App\Provider\StopRepository;
use App\Service\SerializerContextFactory;
@ -34,7 +35,7 @@ class DeparturesController extends Controller
*/
public function stop(DepartureRepository $departures, StopRepository $stops, $stop)
{
$stop = $stops->getById($stop);
$stop = $stops->first(new IdFilter($stop));
return $this->json($departures->getForStop($stop));
}
@ -65,7 +66,7 @@ class DeparturesController extends Controller
public function stops(DepartureRepository $departures, StopRepository $stops, Request $request)
{
$stops = $stops
->getManyById($request->query->get('stop'))
->all(new IdFilter($request->query->get('stop')))
->flatMap(ref([ $departures, 'getForStop' ]))
->sortBy(property('departure'));

View File

@ -1,12 +1,15 @@
<?php
namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Stop;
use App\Model\Track;
use App\Model\StopGroup;
use App\Modifier\IdFilter;
use App\Modifier\FieldFilter;
use App\Modifier\IncludeDestinations;
use App\Modifier\RelatedFilter;
use App\Provider\StopRepository;
use App\Provider\TrackRepository;
use App\Service\Proxy\ReferenceFactory;
@ -38,7 +41,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 +50,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 +73,9 @@ class StopsController extends Controller
*/
public function groups(Request $request, StopRepository $stops)
{
switch (true) {
case $request->query->has('name'):
$result = $stops->findByName($request->query->get('name'));
break;
$modifiers = $this->getModifiersFromRequest($request);
default:
$result = $stops->getAll();
}
return $this->json(static::group($result)->all());
return $this->json(static::group($stops->all(...$modifiers))->toArray());
}
/**
@ -106,7 +96,7 @@ class StopsController extends Controller
*/
public function one(Request $request, StopRepository $stops, $id)
{
return $this->json($stops->getById($id));
return $this->json($stops->first(new IdFilter($id), new IncludeDestinations()));
}
/**
@ -145,4 +135,19 @@ class StopsController extends Controller
return $group;
})->values();
}
private function getModifiersFromRequest(Request $request)
{
if ($request->query->has('name')) {
yield FieldFilter::contains('name', $request->query->get('name'));
}
if ($request->query->has('id')) {
yield new IdFilter($request->query->get('id'));
}
if ($request->query->has('include-destinations')) {
yield new IncludeDestinations();
}
}
}

View File

@ -3,9 +3,13 @@
namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Line;
use App\Model\Stop;
use App\Model\Track;
use App\Modifier\IdFilter;
use App\Modifier\RelatedFilter;
use App\Provider\TrackRepository;
use App\Service\IterableUtils;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request;
@ -28,43 +32,31 @@ class TracksController extends Controller
*/
public function index(Request $request, TrackRepository $repository)
{
switch (true) {
case $request->query->has('stop'):
return $this->byStop($request, $repository);
case $request->query->has('line'):
return $this->byLine($request, $repository);
case $request->query->has('id'):
return $this->byId($request, $repository);
default:
throw new BadRequestHttpException(
sprintf(
'At least one parameter of %s must be set.',
implode(', ', ['stop', 'line', 'id'])
)
);
$modifiers = $this->getModifiersFromRequest($request);
return $this->json($repository->all(...$modifiers));
}
private function getModifiersFromRequest(Request $request)
{
if ($request->query->has('stop')) {
$stop = $request->query->get('stop');
$stop = Stop::reference($stop);
yield new RelatedFilter($stop);
}
if ($request->query->has('line')) {
$line = $request->query->get('line');
$line = Line::reference($line);
yield new RelatedFilter($line);
}
if ($request->query->has('id')) {
$id = encapsulate($request->query->get('id'));
yield new IdFilter($id);
}
}
private function byId(Request $request, TrackRepository $repository)
{
$id = encapsulate($request->query->get('id'));
return $this->json($repository->getManyById($id));
}
private function byStop(Request $request, TrackRepository $repository)
{
$stop = $request->query->get('stop');
$stop = array_map([Stop::class, 'reference'], encapsulate($stop));
return $this->json($repository->getByStop($stop));
}
private function byLine(Request $request, TrackRepository $repository)
{
$line = $request->query->get('line');
$line = array_map([Stop::class, 'reference'], encapsulate($line));
return $this->json($repository->getByLine($line));
}
}
}

View File

@ -4,6 +4,7 @@ namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Trip;
use App\Modifier\IdFilter;
use App\Provider\TripRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -18,7 +19,7 @@ class TripController extends Controller
*/
public function one($id, TripRepository $repository)
{
$trip = $repository->getById($id);
$trip = $repository->all(new IdFilter($id));
return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class));
}

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 InvalidOptionException extends \InvalidArgumentException
{
public static function invalidType($parameter, $value, array $expected = [])
{
return new \InvalidArgumentException(
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,14 @@
<?php
namespace App\Exception;
use App\Modifier\Modifier;
use App\Provider\Repository;
class UnsupportedModifierException extends \LogicException
{
public static function createFromModifier(Modifier $modifier, Repository $repository)
{
return new static(sprintf("Modifier %s is not supported by %s.", get_class($modifier), get_class($repository)));
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\Stop;
use App\Modifier\FieldFilter;
use function App\Functions\encapsulate;
class FieldFilterDatabaseHandler implements ModifierHandler
{
protected $mapping = [
Stop::class => [
'name' => 'name',
],
];
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var FieldFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$field = $this->mapFieldName($event->getMeta()['type'], $modifier->getField());
$operator = $modifier->getOperator();
$value = $modifier->getValue();
$parameter = sprintf(":%s_%s", $alias, $field);
if ($operator === 'in' || $operator === 'not in') {
$parameter = "($parameter)";
$value = encapsulate($value);
}
$builder
->andWhere(sprintf("%s.%s %s %s", $alias, $field, $operator, $parameter))
->setParameter($parameter, $value)
;
}
protected function mapFieldName(string $class, string $field)
{
if (!isset($this->mapping[$class][$field])) {
throw new \InvalidArgumentException(
sprintf("Unable to map field %s of %s into entity field.", $field, $class)
);
}
return $this->mapping[$class][$field];
}
}

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,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 Tightenco\Collect\Support\Collection;
use Kadet\Functional as f;
use Kadet\Functional\Transforms as t;
class IncludeDestinationsDatabaseHandler 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,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,96 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\Line;
use App\Model\Stop;
use App\Model\Track;
use App\Modifier\RelatedFilter;
use App\Service\IdUtils;
use App\Service\ReferenceFactory;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSubscriberInterface
{
protected $mapping = [
Track::class => [
Line::class => 'line',
Stop::class => TrackByStopDatabaseHandler::class,
],
];
private $em;
private $inner;
private $id;
private $references;
public function __construct(
ContainerInterface $inner,
EntityManagerInterface $em,
IdUtils $idUtils,
ReferenceFactory $references
) {
$this->inner = $inner;
$this->em = $em;
$this->id = $idUtils;
$this->references = $references;
}
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var RelatedFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$type = $event->getMeta()['type'];
if (!array_key_exists($type, $this->mapping)) {
throw new \InvalidArgumentException(
sprintf("Relationship filtering for %s is not supported.", $type)
);
}
if (!array_key_exists($modifier->getRelationship(), $this->mapping[$type])) {
throw new \InvalidArgumentException(
sprintf("Relationship %s is not supported for .", $type)
);
}
$relationship = $this->mapping[$type][$modifier->getRelationship()];
if ($this->inner->has($relationship)) {
/** @var ModifierHandler $inner */
$inner = $this->inner->get($relationship);
$inner->process($event);
return;
}
$parameter = sprintf(":%s_%s", $alias, $relationship);
$reference = $this->references->create($modifier->getRelated(), $event->getMeta()['provider']);
$builder
->join(sprintf('%s.%s', $alias, $relationship), $relationship)
->andWhere(sprintf("%s = %s", $relationship, $parameter))
->setParameter($parameter, $reference)
;
}
/**
* @inheritDoc
*/
public static function getSubscribedServices()
{
return [
TrackByStopDatabaseHandler::class,
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Handler\Database;
use App\Entity\StopInTrack;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Modifier\RelatedFilter;
use App\Service\ReferenceFactory;
class TrackByStopDatabaseHandler implements ModifierHandler
{
private $references;
public function __construct(ReferenceFactory $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']);
$builder
->join(sprintf("%s.%s", $alias, $relationship), 'stop_in_track')
->andWhere(sprintf("stop_in_track.stop = %s", $parameter))
->setParameter($parameter, $reference)
;
}
}

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

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

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

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

View File

@ -0,0 +1,7 @@
<?php
namespace App\Modifier;
class IncludeDestinations implements Modifier
{
}

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,27 @@
<?php
namespace App\Modifier;
use App\Model\Referable;
class RelatedFilter implements Modifier
{
private $relationship;
private $object;
public function __construct(Referable $object, ?string $relation = null)
{
$this->object = $object;
$this->relationship = $relation ?: get_class($object);
}
public function getRelationship(): string
{
return $this->relationship;
}
public function getRelated(): Referable
{
return $this->object;
}
}

View File

@ -2,15 +2,31 @@
namespace App\Provider\Database;
use App\Entity\Entity;
use App\Entity\ProviderEntity;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\PostProcessEvent;
use App\Exception\UnsupportedModifierException;
use App\Handler\Database\IdFilterDatabaseHandler;
use App\Handler\Database\LimitDatabaseHandler;
use App\Handler\Database\FieldFilterDatabaseHandler;
use App\Handler\Database\RelatedFilterDatabaseGenericHandler;
use App\Handler\ModifierHandler;
use App\Handler\PostProcessingHandler;
use App\Model\Referable;
use App\Modifier\IdFilter;
use App\Modifier\Limit;
use App\Modifier\Modifier;
use App\Modifier\FieldFilter;
use App\Modifier\RelatedFilter;
use App\Provider\Repository;
use App\Service\Converter;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use Kadet\Functional as f;
use Doctrine\ORM\QueryBuilder;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class DatabaseRepository
abstract class DatabaseRepository implements ServiceSubscriberInterface, Repository
{
/** @var EntityManagerInterface */
protected $em;
@ -24,22 +40,30 @@ class DatabaseRepository
/** @var Converter */
protected $converter;
/** @var ContainerInterface */
protected $handlers;
/**
* DatabaseRepository constructor.
*
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em, IdUtils $id, Converter $converter)
{
public function __construct(
EntityManagerInterface $em,
IdUtils $id,
Converter $converter,
ContainerInterface $handlers
) {
$this->em = $em;
$this->id = $id;
$this->converter = $converter;
$this->handlers = $handlers;
}
/** @return static */
public function withProvider(ProviderEntity $provider)
{
$result = clone $this;
$result = clone $this;
$result->provider = $provider;
return $result;
@ -56,4 +80,87 @@ class DatabaseRepository
return $this->em->getReference($class, $id);
}
protected function processQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = [])
{
$reducers = [];
foreach ($modifiers as $modifier) {
$handler = $this->getHandler($modifier);
if ($handler instanceof ModifierHandler) {
$event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([
'provider' => $this->provider,
], $meta));
$handler->process($event);
}
if ($handler instanceof PostProcessingHandler) {
$reducers[] = function ($result) use ($meta, $modifier, $handler) {
$event = new PostProcessEvent($result, $modifier, $this, array_merge([
'provider' => $this->provider,
], $meta));
$handler->postProcess($event);
return $event->getData();
};
}
}
return collect($reducers);
}
protected function allFromQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = [])
{
$reducers = $this->processQueryBuilder($builder, $modifiers, $meta);
return $reducers->reduce(function ($result, $reducer) {
return $reducer($result);
}, collect($builder->getQuery()->execute())->map(\Closure::fromCallable([$this, 'convert'])));
}
public function first(Modifier ...$modifiers)
{
return $this->all(Limit::count(1), ...$modifiers)->first();
}
protected function getHandler(Modifier $modifier)
{
$class = get_class($modifier);
if (!$this->handlers->has($class)) {
throw UnsupportedModifierException::createFromModifier($modifier, $this);
}
return $this->handlers->get($class);
}
/**
* Returns array describing handlers for each modifier type. Syntax is as follows:
* [ IdFilter::class => IdFilterDatabaseHandler::class ]
*
* It is internally used as part of service subscriber.
*
* @return array
*/
protected static function getHandlers()
{
return [];
}
/**
* @inheritDoc
*/
public static function getSubscribedServices()
{
return array_merge([
IdFilter::class => IdFilterDatabaseHandler::class,
Limit::class => LimitDatabaseHandler::class,
FieldFilter::class => FieldFilterDatabaseHandler::class,
RelatedFilter::class => RelatedFilterDatabaseGenericHandler::class,
], static::getHandlers());
}
}

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']));
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;
use App\Entity\OperatorEntity;
use App\Model\Operator;
use App\Modifier\Modifier;
use App\Provider\OperatorRepository;
use Tightenco\Collect\Support\Collection;
class GenericOperatorRepository extends DatabaseRepository implements OperatorRepository
{
public function getAll(): Collection
public function all(Modifier ...$modifiers): Collection
{
$repository = $this->em->getRepository(Operator::class);
$operators = $repository->findAll();
$builder = $this->em
->createQueryBuilder()
->from(OperatorEntity::class, 'operator')
->select('operator')
;
return collect($operators);
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'operator',
'entity' => OperatorEntity::class,
'type' => Operator::class,
]);
}
public function getById($id): ?Operator
{
$repository = $this->em->getRepository(Operator::class);
return $repository->find($id);
}
public function getManyById($ids): Collection
{
$repository = $this->em->getRepository(Operator::class);
$operators = $repository->findBy(['id' => $ids]);
return collect($operators);
}
}
}

View File

@ -70,4 +70,9 @@ class GenericScheduleRepository extends DatabaseRepository implements ScheduleRe
]);
});
}
protected static function getHandlers()
{
return [];
}
}

View File

@ -3,84 +3,34 @@
namespace App\Provider\Database;
use App\Entity\StopEntity;
use App\Entity\TrackEntity;
use App\Model\Destination;
use App\Handler\Database\IncludeDestinationsDatabaseHandler;
use App\Model\Stop;
use App\Modifier\Modifier;
use App\Modifier\IncludeDestinations;
use App\Provider\StopRepository;
use Kadet\Functional as f;
use Kadet\Functional\Transforms as t;
use Tightenco\Collect\Support\Collection;
class GenericStopRepository extends DatabaseRepository implements StopRepository
{
public function getAll(): Collection
public function all(Modifier ...$modifiers): Collection
{
$stops = $this->em->getRepository(StopEntity::class)->findAll();
$builder = $this->em
->createQueryBuilder()
->from(StopEntity::class, 'stop')
->select('stop')
;
return collect($stops)->map(f\ref([$this, 'convert']));
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'stop',
'entity' => StopEntity::class,
'type' => Stop::class,
]);
}
public function getById($id): ?Stop
protected static function getHandlers()
{
$id = $this->id->generate($this->provider, $id);
$stop = $this->em->getRepository(StopEntity::class)->find($id);
return $this->convert($stop);
}
public function getManyById($ids): Collection
{
$ids = collect($ids)->map(f\apply(f\ref([$this->id, 'generate']), $this->provider));
$stops = $this->em->getRepository(StopEntity::class)->findBy(['id' => $ids->all()]);
return collect($stops)->map(f\ref([$this, 'convert']));
}
public function findByName(string $name): Collection
{
$query = $this->em->createQueryBuilder()
->select('s')
->from(StopEntity::class, 's')
->where('s.name LIKE :name')
->getQuery();
$stops = collect($query->execute([':name' => "%$name%"]));
$destinations = collect($this->em->createQueryBuilder()
->select('t', 'tl', 'f', 'fs', 'ts')
->from(TrackEntity::class, 't')
->join('t.stopsInTrack', 'ts')
->join('t.line', 'tl')
->where('ts.stop IN (:stops)')
->join('t.final', 'f')
->join('f.stop', 'fs')
->getQuery()
->execute(['stops' => $stops->map(t\property('id'))->all()]))
->reduce(function ($grouped, TrackEntity $track) {
foreach ($track->getStopsInTrack()->map(t\property('stop'))->map(t\property('id')) as $stop) {
$grouped[$stop] = ($grouped[$stop] ?? collect())->add($track);
}
return $grouped;
}, collect())
->map(function (Collection $tracks) {
return $tracks
->groupBy(function (TrackEntity $track) {
return $track->getFinal()->getStop()->getId();
})->map(function (Collection $tracks, $id) {
return Destination::createFromArray([
'stop' => $this->convert($tracks->first()->getFinal()->getStop()),
'lines' => $tracks
->map(t\property('line'))
->unique(t\property('id'))
->map(f\ref([$this, 'convert']))
->values(),
]);
})->values();
});
return collect($stops)->map(f\ref([$this, 'convert']))->each(function (Stop $stop) use ($destinations) {
$stop->setDestinations($destinations[$this->id->generate($this->provider, $stop->getId())]);
});
return array_merge(parent::getHandlers(), [
IncludeDestinations::class => IncludeDestinationsDatabaseHandler::class,
]);
}
}

View File

@ -2,36 +2,18 @@
namespace App\Provider\Database;
use App\Entity\LineEntity;
use App\Entity\StopEntity;
use App\Entity\StopInTrack;
use App\Entity\TrackEntity;
use function App\Functions\encapsulate;
use App\Model\Stop;
use App\Modifier\Modifier;
use App\Model\Track;
use App\Provider\TrackRepository;
use Tightenco\Collect\Support\Collection;
use Kadet\Functional as f;
use function App\Functions\encapsulate;
class GenericTrackRepository extends DatabaseRepository implements TrackRepository
{
public function getAll(): Collection
{
$tracks = $this->em->getRepository(TrackEntity::class)->findAll();
return collect($tracks)->map(f\ref([$this, 'convert']));
}
public function getById($id): Track
{
// TODO: Implement getById() method.
}
public function getManyById($ids): Collection
{
// TODO: Implement getManyById() method.
}
public function getByStop($stop): Collection
{
$reference = f\apply(f\ref([$this, 'reference']), StopEntity::class);
@ -49,19 +31,17 @@ class GenericTrackRepository extends DatabaseRepository implements TrackReposito
});
}
public function getByLine($line): Collection
public function all(Modifier ...$modifiers): Collection
{
$reference = f\apply(f\ref([$this, 'reference']), LineEntity::class);
$builder = $this->em
->createQueryBuilder()
->from(TrackEntity::class, 'track')
->select('track');
$tracks = $this->em->createQueryBuilder()
->from(StopInTrack::class, 'st')
->join('st.track', 't')
->join('t.stops', 's')
->where('st.line in (:line)')
->select(['st', 't', 's'])
->getQuery()
->execute(['stop' => array_map($reference, encapsulate($line))]);
return collect($tracks)->map(f\ref([$this, 'convert']));
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'track',
'entity' => TrackEntity::class,
'type' => Track::class,
]);
}
}
}

View File

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

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

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

@ -5,13 +5,7 @@ namespace App\Provider;
use App\Model\Track;
use Tightenco\Collect\Support\Collection;
interface TrackRepository
interface TrackRepository extends FluentRepository
{
public function getAll(): Collection;
public function getById($id): Track;
public function getManyById($ids): Collection;
public function getByStop($stop): Collection;
public function getByLine($line): Collection;
}
}

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,6 +6,7 @@ use App\Model\Departure;
use App\Model\Line;
use App\Model\Stop;
use App\Model\Vehicle;
use App\Modifier\IdFilter;
use App\Provider\Database\GenericScheduleRepository;
use App\Provider\DepartureRepository;
use App\Provider\LineRepository;
@ -65,7 +66,8 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
$lines = $estimates->map(function ($delay) {
return $delay['routeId'];
})->unique();
$lines = $this->lines->getManyById($lines)->keyBy(t\property('id'));
$lines = $this->lines->all(new IdFilter($lines))->keyBy(t\property('id'));
return collect($estimates)->map(function ($delay) use ($stop, $lines) {
$scheduled = (new Carbon($delay['theoreticalTime'], 'Europe/Warsaw'))->tz('UTC');

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);
}
@ -23,4 +24,4 @@ class IdUtils
{
return $this->strip($entity->getId());
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Service;
use App\Entity\LineEntity;
use App\Entity\ProviderEntity;
use App\Entity\StopEntity;
use App\Model\Line;
use App\Model\Referable;
use App\Model\Stop;
use Doctrine\ORM\EntityManagerInterface;
final class ReferenceFactory
{
protected $mapping = [
Line::class => LineEntity::class,
Stop::class => StopEntity::class,
];
private $em;
private $id;
public function __construct(EntityManagerInterface $em, IdUtils $id)
{
$this->em = $em;
$this->id = $id;
}
public function create(Referable $object, ProviderEntity $provider)
{
$class = get_class($object);
if (!array_key_exists($class, $this->mapping)) {
throw new \InvalidArgumentException(sprintf("Cannot make entity reference of %s.", $class));
}
return $this->em->getReference(
$this->mapping[$class],
$this->id->generate($provider, $object->getId())
);
}
}