Make StopRepository more fluent

This commit is contained in:
Kacper Donat 2020-02-16 21:07:16 +01:00
parent 67f7ba2a88
commit 847e3a078f
30 changed files with 374 additions and 158 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

@ -7,6 +7,9 @@ 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\Provider\StopRepository;
use App\Provider\TrackRepository;
use App\Service\Proxy\ReferenceFactory;
@ -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 IncludeDestinations()));
}
/**
@ -145,4 +134,28 @@ class StopsController extends Controller
return $group;
})->values();
}
/**
* @param Request $request
*
* @return array
*/
private function getModifiersFromRequest(Request $request): array
{
$modifiers = [];
if ($request->query->has('name')) {
$modifiers[] = FieldFilter::contains('name', $request->query->get('name'));
}
if ($request->query->has('id')) {
$modifiers[] = new IdFilter($request->query->get('id'));
}
if ($request->query->has('include-destinations')) {
$modifiers[] = new IncludeDestinations();
}
return $modifiers;
}
}

View File

@ -3,7 +3,7 @@
namespace App\Event;
use App\Event\HandleModifierEvent;
use App\Modifiers\Modifier;
use App\Modifier\Modifier;
use App\Provider\Repository;
use Doctrine\ORM\QueryBuilder;

View File

@ -2,7 +2,7 @@
namespace App\Event;
use App\Modifiers\Modifier;
use App\Modifier\Modifier;
use App\Provider\Repository;
class HandleModifierEvent

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

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

@ -2,10 +2,10 @@
namespace App\Exception;
use App\Modifiers\Modifier;
use App\Modifier\Modifier;
use App\Provider\Repository;
class UnsupportedModifierException extends \Exception
class UnsupportedModifierException extends \LogicException
{
public static function createFromModifier(Modifier $modifier, Repository $repository)
{

View File

@ -0,0 +1,52 @@
<?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;
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);
$builder
->where(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

@ -1,9 +1,9 @@
<?php
namespace App\Handlers\Database;
namespace App\Handler\Database;
use App\Handlers\ModifierHandler;
use App\Modifiers\IdFilter;
use App\Handler\ModifierHandler;
use App\Modifier\IdFilter;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Service\IdUtils;

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

@ -1,11 +1,11 @@
<?php
namespace App\Handlers\Database;
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handlers\ModifierHandler;
use App\Modifiers\Limit;
use App\Handler\ModifierHandler;
use App\Modifier\Limit;
class LimitDatabaseHandler implements ModifierHandler
{

View File

@ -1,6 +1,6 @@
<?php
namespace App\Handlers;
namespace App\Handler;
use App\Event\HandleModifierEvent;

View File

@ -0,0 +1,11 @@
<?php
namespace App\Handler;
use App\Event\HandleModifierEvent;
use App\Event\PostProcessEvent;
interface PostProcessingHandler
{
public function process(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;
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace App\Modifiers;
namespace App\Modifier;
use App\Exception\InvalidOptionException;
use App\Modifiers\Modifier;
use App\Modifier\Modifier;
class IdFilter implements Modifier
{

View File

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

View File

@ -1,6 +1,6 @@
<?php
namespace App\Modifiers;
namespace App\Modifier;
class Limit implements Modifier
{

View File

@ -1,6 +1,6 @@
<?php
namespace App\Modifiers;
namespace App\Modifier;
interface Modifier
{

View File

@ -4,8 +4,17 @@ namespace App\Provider\Database;
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\PostProcessingHandler;
use App\Model\Referable;
use App\Modifier\IdFilter;
use App\Modifier\Limit;
use App\Modifier\Modifier;
use App\Modifier\FieldFilter;
use App\Provider\Repository;
use App\Service\Converter;
use App\Service\IdUtils;
@ -71,21 +80,60 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit
protected function processQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = [])
{
$reducers = [];
foreach ($modifiers as $modifier) {
$event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([
'provider' => $this->provider,
], $meta));
$handler = $this->getHandler($modifier);
$class = get_class($modifier);
switch (true) {
case $handler instanceof PostProcessingHandler:
$reducers[] = function ($result) use ($meta, $modifier, $handler) {
$event = new PostProcessEvent($result, $modifier, $this, array_merge([
'provider' => $this->provider,
], $meta));
if (!$this->handlers->has($class)) {
throw UnsupportedModifierException::createFromModifier($modifier, $this);
$handler->process($event);
return $event->getData();
};
break;
default:
$event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([
'provider' => $this->provider,
], $meta));
$handler->process($event);
break;
}
$handler = $this->handlers->get($class);
$handler->process($event);
}
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);
}
/**
@ -106,6 +154,10 @@ abstract class DatabaseRepository implements ServiceSubscriberInterface, Reposit
*/
public static function getSubscribedServices()
{
return static::getHandlers();
return array_merge([
IdFilter::class => IdFilterDatabaseHandler::class,
Limit::class => LimitDatabaseHandler::class,
FieldFilter::class => FieldFilterDatabaseHandler::class,
], static::getHandlers());
}
}

View File

@ -4,24 +4,19 @@ namespace App\Provider\Database;
use App\Entity\LineEntity;
use App\Event\HandleDatabaseModifierEvent;
use App\Handlers\Database\LimitDatabaseHandler;
use App\Handlers\Database\IdFilterDatabaseHandler;
use App\Handlers\ModifierHandler;
use App\Handler\Database\LimitDatabaseHandler;
use App\Handler\Database\IdFilterDatabaseHandler;
use App\Handler\ModifierHandler;
use App\Model\Line;
use App\Modifiers\Limit;
use App\Modifiers\IdFilter;
use App\Modifier\Limit;
use App\Modifier\IdFilter;
use App\Provider\LineRepository;
use App\Modifiers\Modifier;
use App\Modifier\Modifier;
use Tightenco\Collect\Support\Collection;
use Kadet\Functional as f;
class GenericLineRepository extends DatabaseRepository implements LineRepository
{
public function first(Modifier ...$modifiers)
{
return $this->all(Limit::count(1), ...$modifiers)->first();
}
public function all(Modifier ...$modifiers): Collection
{
$builder = $this->em
@ -30,21 +25,10 @@ class GenericLineRepository extends DatabaseRepository implements LineRepository
->select('line')
;
$this->processQueryBuilder($builder, $modifiers, [
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'line',
'entity' => LineEntity::class,
'type' => Line::class,
]);
return collect($builder->getQuery()->execute())->map(f\ref([$this, 'convert']));
}
/** @return ModifierHandler[] */
protected static function getHandlers()
{
return [
IdFilter::class => IdFilterDatabaseHandler::class,
Limit::class => LimitDatabaseHandler::class,
];
}
}

View File

@ -3,89 +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']));
}
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(),
]);
})->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 $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'stop',
'entity' => StopEntity::class,
'type' => Stop::class,
]);
}
protected static function getHandlers()
{
return [];
return array_merge(parent::getHandlers(), [
IncludeDestinations::class => IncludeDestinationsDatabaseHandler::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

@ -2,7 +2,7 @@
namespace App\Provider;
use App\Modifiers\Modifier;
use App\Modifier\Modifier;
use Tightenco\Collect\Support\Collection;
interface FluentRepository extends 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

@ -6,7 +6,7 @@ use App\Model\Departure;
use App\Model\Line;
use App\Model\Stop;
use App\Model\Vehicle;
use App\Modifiers\IdFilter;
use App\Modifier\IdFilter;
use App\Provider\Database\GenericScheduleRepository;
use App\Provider\DepartureRepository;
use App\Provider\LineRepository;

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