database entity conversion

This commit is contained in:
Kacper Donat 2018-09-08 21:59:58 +02:00
parent 7c5ee55612
commit 432d90fa1b
16 changed files with 327 additions and 53 deletions

View File

@ -8,6 +8,7 @@
"ext-iconv": "*",
"ext-json": "*",
"nesbot/carbon": "^1.33",
"ocramius/proxy-manager": "^2.2",
"sensio/framework-extra-bundle": "^5.2",
"symfony/console": "*",
"symfony/flex": "^1.1",

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0af7567013958485d4223a33f8ba099a",
"content-hash": "23c4b2debfec2fc7da526a8bd2007ec3",
"packages": [
{
"name": "doctrine/annotations",

View File

@ -31,10 +31,32 @@ services:
resource: '../src/Provider'
public: true
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
#proxy configuration
proxy.locator:
class: 'ProxyManager\FileLocator\FileLocator'
arguments: ['%kernel.cache_dir%/proxy']
proxy.strategy:
class: 'ProxyManager\GeneratorStrategy\FileWriterGeneratorStrategy'
arguments: ['@proxy.locator']
proxy.config:
class: 'ProxyManager\Configuration'
calls:
- ['setGeneratorStrategy', ['@proxy.strategy']]
- ['setProxiesTargetDir', ['%kernel.cache_dir%/proxy']]
ProxyManager\Configuration: '@proxy.config'
# serializer configuration
serializer.datetime_normalizer:
class: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
arguments: [!php/const DateTime::ATOM]
tags: [serializer.normalizer]
tags: [serializer.normalizer]
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Service\Normalizer\:
resource: '../src/Service/Normalizer'
tags: [serializer.normalizer]

View File

@ -46,7 +46,7 @@ class TrackEntity implements Entity, Fillable
* Stops in track
* @var Collection
* @ORM\OneToMany(targetEntity=StopInTrack::class, mappedBy="track", cascade={"persist"})
* @ORM\OrderBy({"order"})
* @ORM\OrderBy({"order": "ASC"})
*/
private $stopsInTrack;

View File

@ -48,6 +48,10 @@ class Kernel extends BaseKernel
$loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
if (!file_exists($this->getCacheDir().'/proxy')) {
mkdir($this->getCacheDir().'/proxy');
}
}
protected function configureRoutes(RouteCollectionBuilder $routes)

View File

@ -2,12 +2,9 @@
namespace App\Model;
use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Tightenco\Collect\Support\Collection;
class Line implements Fillable, Referable, NormalizableInterface
class Line implements Fillable, Referable
{
const TYPE_TRAM = 'tram';
const TYPE_BUS = 'bus';
@ -120,12 +117,4 @@ class Line implements Fillable, Referable, NormalizableInterface
{
$this->operator = $operator;
}
public function normalize(NormalizerInterface $normalizer, $format = null, array $context = [])
{
$normalizer = new ObjectNormalizer();
$normalizer->setIgnoredAttributes(['tracks']);
return $normalizer->normalize($this);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Model;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Tightenco\Collect\Support\Arr;
@ -76,6 +77,7 @@ class Stop implements Referable, Fillable, NormalizableInterface
$this->variant = $variant;
}
/** @Groups({"hidden"}) */
public function getLatitude(): ?float
{
return $this->latitude;
@ -86,6 +88,7 @@ class Stop implements Referable, Fillable, NormalizableInterface
$this->latitude = $latitude;
}
/** @Groups({"hidden"}) */
public function getLongitude(): ?float
{
return $this->longitude;

View File

@ -2,9 +2,12 @@
namespace App\Provider\Database;
use App\Entity\Entity;
use App\Entity\ProviderEntity;
use App\Service\EntityConverter;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use Kadet\Functional as f;
class DatabaseRepository
{
@ -17,15 +20,19 @@ class DatabaseRepository
/** @var IdUtils */
protected $id;
/** @var EntityConverter */
protected $converter;
/**
* DatabaseRepository constructor.
*
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em, IdUtils $id)
public function __construct(EntityManagerInterface $em, IdUtils $id, EntityConverter $converter)
{
$this->em = $em;
$this->id = $id;
$this->em = $em;
$this->id = $id;
$this->converter = $converter;
}
/** @return static */
@ -36,4 +43,9 @@ class DatabaseRepository
return $result;
}
protected function convert(Entity $entity)
{
return $this->converter->convert($entity);
}
}

View File

@ -33,15 +33,4 @@ class GenericLineRepository extends DatabaseRepository implements LineRepository
return collect($lines)->map(f\ref([$this, 'convert']));
}
private function convert(LineEntity $line): Line
{
return Line::createFromArray([
'id' => $this->id->of($line),
'symbol' => $line->getSymbol(),
'night' => $line->isNight(),
'fast' => $line->isFast(),
'type' => $line->getType()
]);
}
}

View File

@ -15,7 +15,7 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
{
$stops = $this->em->getRepository(StopEntity::class)->findAll();
return collect($stops)->map(\Closure::fromCallable([$this, 'convert']));
return collect($stops)->map(f\ref([$this, 'convert']));
}
public function getAllGroups(): Collection
@ -36,7 +36,7 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
$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(\Closure::fromCallable([$this, 'convert']));
return collect($stops)->map(f\ref([$this, 'convert']));
}
public function findGroupsByName(string $name): Collection
@ -47,26 +47,11 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
->where('s.name LIKE :name')
->getQuery();
$stops = collect($query->execute([':name' => "%$name%"]))->map(\Closure::fromCallable([$this, 'convert']));
$stops = collect($query->execute([':name' => "%$name%"]))->map(f\ref([$this, 'convert']));
return $this->group($stops);
}
private function convert(StopEntity $entity): Stop
{
return Stop::createFromArray([
'id' => $this->id->of($entity),
'name' => $entity->getName(),
'description' => $entity->getDescription(),
'variant' => $entity->getVariant(),
'onDemand' => $entity->isOnDemand(),
'location' => [
$entity->getLatitude(),
$entity->getLongitude(),
],
]);
}
private function group(Collection $stops)
{
return $stops->groupBy(function (Stop $stop) {

View File

@ -0,0 +1,151 @@
<?php
namespace App\Service;
use App\Entity\Entity;
use App\Entity\LineEntity;
use App\Entity\OperatorEntity;
use App\Entity\StopEntity;
use App\Entity\TrackEntity;
use App\Model\Line;
use App\Model\Operator;
use App\Model\Stop;
use App\Model\Track;
use App\Service\Proxy\ReferenceObjectFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\Proxy;
use Kadet\Functional as f;
use const Kadet\Functional\_;
final class EntityConverter
{
private $id;
private $reference;
public function __construct(IdUtils $id, ReferenceObjectFactory $reference)
{
$this->id = $id;
$this->reference = $reference;
}
/**
* @param Entity $entity
* @param array $cache
*
* @return Line|Track|Stop|Operator
*/
public function convert(Entity $entity, array $cache = [])
{
if (array_key_exists($key = get_class($entity).':'.$this->getId($entity), $cache)) {
return $cache[$key];
}
if ($entity instanceof Proxy && !$entity->__isInitialized()) {
return $this->reference($entity);
}
$result = $this->create($entity);
$cache = $cache + [$key => $result];
$convert = f\partial([$this, 'convert'], _, $cache);
switch (true) {
case $entity instanceof OperatorEntity:
$result->fill([
'name' => $entity->getName(),
'phone' => $entity->getPhone(),
'email' => $entity->getEmail(),
'url' => $entity->getEmail(),
]);
break;
case $entity instanceof LineEntity:
$result->fill([
'id' => $this->id->of($entity),
'symbol' => $entity->getSymbol(),
'type' => $entity->getType(),
'operator' => $convert($entity->getOperator()),
'tracks' => $this->collection($entity->getTracks(), $convert),
]);
break;
case $entity instanceof TrackEntity:
$result->fill([
'variant' => $entity->getVariant(),
'description' => $entity->getDescription(),
'stops' => $this->collection($entity->getStops(), $convert),
'line' => $convert($entity->getLine()),
]);
break;
case $entity instanceof StopEntity:
$result->fill([
'name' => $entity->getName(),
'variant' => $entity->getVariant(),
'description' => $entity->getDescription(),
'location' => [
$entity->getLatitude(),
$entity->getLongitude(),
],
]);
}
return $result;
}
// HACK to not trigger doctrine stupid lazy loading.
private function getId(Entity $entity) {
if ($entity instanceof Proxy) {
$id = (new \ReflectionClass(get_parent_class($entity)))->getProperty('id');
$id->setAccessible(true);
return $id->getValue($entity);
}
return $entity->getId();
}
private function collection(PersistentCollection $collection, $converter)
{
if ($collection->isInitialized()) {
return collect($collection)->map($converter);
}
return collect();
}
private function getModelClassForEntity(Entity $entity)
{
switch (true) {
case $entity instanceof OperatorEntity:
return Operator::class;
case $entity instanceof LineEntity:
return Line::class;
case $entity instanceof TrackEntity:
return Track::class;
case $entity instanceof StopEntity:
return Stop::class;
default:
return false;
}
}
private function create(Entity $entity)
{
$id = $this->id->of($entity);
$class = $this->getModelClassForEntity($entity);
return $class::createFromArray(['id' => $id]);
}
private function reference(Entity $entity)
{
$id = $this->id->strip($this->getId($entity));
$class = $this->getModelClassForEntity($entity);
return $this->reference->get($class, ['id' => $id]);
}
}

View File

@ -7,18 +7,20 @@ use App\Entity\ProviderEntity;
class IdUtils
{
const DELIMITER = '::';
public function generate(ProviderEntity $provider, $id)
{
return sprintf('%s-%s', $provider->getId(), $id);
return sprintf('%s%s%s', $provider->getId(), self::DELIMITER, $id);
}
public function strip(ProviderEntity $provider, $id)
public function strip($id)
{
return substr($id, strlen($provider->getId()) + 1);
return explode(self::DELIMITER, $id)[1];
}
public function of(Entity $entity)
{
return $this->strip($entity->getProvider(), $entity->getId());
return $this->strip($entity->getId());
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Service\Normalizer;
use App\Model\JustReference;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class JustReferenceNormalizer implements NormalizerInterface
{
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param mixed $object Object to normalize
* @param string $format Format the normalization result will be encoded as
* @param array $context Context options for the normalizer
*
* @return array|string|int|float|bool
*
* @throws InvalidArgumentException Occurs when the object given is not an attempted type for the normalizer
* @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
* reference handler can fix it
* @throws LogicException Occurs when the normalizer is not called in an expected context
*/
public function normalize($object, $format = null, array $context = [])
{
return [ 'id' => $object->getId() ];
}
/**
* Checks whether the given class is supported for normalization by this normalizer.
*
* @param mixed $data Data to normalize
* @param string $format The format being (de-)serialized from or into
*
* @return bool
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof JustReference;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Service\Normalizer;
use App\Model\Stop;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Tightenco\Collect\Support\Arr;
class StopNormalizer implements NormalizerInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function normalize($object, $format = null, array $context = [])
{
return Arr::except($this->normalizer->normalize($object), ['latitude', 'longitude']);
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Stop;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Service\Proxy;
use ProxyManager\Factory\AbstractBaseFactory;
use ProxyManager\ProxyGenerator\ProxyGeneratorInterface;
class ReferenceObjectFactory extends AbstractBaseFactory
{
public function get($class, $id)
{
$id = is_array($id) ? $id : compact('id');
$proxy = $this->generateProxy($class);
$object = new $proxy();
$object->fill($id);
return $object;
}
protected function getGenerator(): ProxyGeneratorInterface
{
return new ReferenceObjectGenerator();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Service\Proxy;
use App\Model\JustReference;
use ProxyManager\ProxyGenerator\ProxyGeneratorInterface;
use ReflectionClass;
use Zend\Code\Generator\ClassGenerator;
class ReferenceObjectGenerator implements ProxyGeneratorInterface
{
public function generate(ReflectionClass $class, ClassGenerator $generator)
{
$interfaces = array_merge($class->getInterfaceNames(), [ JustReference::class ]);
$generator->setExtendedClass($class->getName());
$generator->setImplementedInterfaces($interfaces);
}
}