From 2c9a795756ee1547a797135236524a901844d7b0 Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Tue, 21 Jan 2020 21:46:27 +0100
Subject: [PATCH] SerializeAs annotation

---
 config/services.yaml                          | 10 +--
 .../Api/v1/DeparturesController.php           |  8 +-
 src/Controller/Controller.php                 | 17 ++--
 src/Model/Departure.php                       | 40 ++++++++++
 .../Database/GenericScheduleRepository.php    |  6 +-
 .../ZtmGdanskDepartureRepository.php          |  2 +
 src/Serialization/SerializeAs.php             | 15 ++++
 src/Service/SerializerContextFactory.php      | 79 +++++++++++++++++++
 8 files changed, 161 insertions(+), 16 deletions(-)
 create mode 100644 src/Serialization/SerializeAs.php
 create mode 100644 src/Service/SerializerContextFactory.php

diff --git a/config/services.yaml b/config/services.yaml
index eb381fc..4bcbebc 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -69,14 +69,10 @@ services:
     ProxyManager\Configuration: '@proxy.config'
 
     # serializer configuration
-    serializer.datetime_normalizer:
-        class: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
-        arguments: [!php/const DateTime::ATOM]
-        tags: [serializer.normalizer]
+    App\Service\SerializerContextFactory:
+        arguments:
+            $factory: '@jms_serializer.metadata_factory'
 
-    App\Service\Normalizer\:
-        resource: '../src/Service/Normalizer'
-        tags: [serializer.normalizer]
 
     # other servces
     App\Service\ProviderResolver:
diff --git a/src/Controller/Api/v1/DeparturesController.php b/src/Controller/Api/v1/DeparturesController.php
index 3141006..410a753 100644
--- a/src/Controller/Api/v1/DeparturesController.php
+++ b/src/Controller/Api/v1/DeparturesController.php
@@ -7,6 +7,7 @@ use App\Controller\Controller;
 use App\Model\Departure;
 use App\Provider\DepartureRepository;
 use App\Provider\StopRepository;
+use App\Service\SerializerContextFactory;
 use Nelmio\ApiDocBundle\Annotation\Model;
 use Swagger\Annotations as SWG;
 use Symfony\Component\HttpFoundation\Request;
@@ -68,6 +69,11 @@ class DeparturesController extends Controller
             ->flatMap(ref([ $departures, 'getForStop' ]))
             ->sortBy(property('departure'));
 
-        return $this->json($stops->values()->slice(0, (int)$request->query->get('limit', 8)));
+        return $this->json(
+            $stops->values()->slice(0, (int)$request->query->get('limit', 8)),
+            200,
+            [],
+            $this->serializerContextFactory->create(Departure::class, ['Default'])
+        );
     }
 }
diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php
index a2d04b4..1187604 100644
--- a/src/Controller/Controller.php
+++ b/src/Controller/Controller.php
@@ -4,21 +4,24 @@
 namespace App\Controller;
 
 
+use App\Service\SerializerContextFactory;
 use JMS\Serializer\SerializerInterface;
-use Symfony\Bundle\FrameworkBundle\Controller\Controller as SymfonyController;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\JsonResponse;
 
-abstract class Controller extends SymfonyController
+abstract class Controller extends AbstractController
 {
-    private $serializer;
+    protected $serializer;
+    protected $serializerContextFactory;
 
-    public function __construct(SerializerInterface $serializer)
+    public function __construct(SerializerInterface $serializer, SerializerContextFactory $serializerContextFactory)
     {
         $this->serializer = $serializer;
+        $this->serializerContextFactory = $serializerContextFactory;
     }
 
-    protected function json($data, int $status = 200, array $headers = [], array $context = []): JsonResponse
+    protected function json($data, int $status = 200, array $headers = [], $context = null): JsonResponse
     {
-        return new JsonResponse($this->serializer->serialize($data, "json"), $status, $headers, true);
+        return new JsonResponse($this->serializer->serialize($data, "json", $context), $status, $headers, true);
     }
-}
\ No newline at end of file
+}
diff --git a/src/Model/Departure.php b/src/Model/Departure.php
index a4d8c51..30bb185 100644
--- a/src/Model/Departure.php
+++ b/src/Model/Departure.php
@@ -2,6 +2,7 @@
 
 namespace App\Model;
 
+use App\Serialization\SerializeAs;
 use Carbon\Carbon;
 use JMS\Serializer\Annotation as Serializer;
 use Nelmio\ApiDocBundle\Annotation\Model;
@@ -22,11 +23,30 @@ class Departure implements Fillable
      * Information about line.
      * @var Line
      * @Serializer\Type(Line::class)
+     * @SerializeAs({"Default": "Default"})
      * @SWG\Property(ref=@Model(type=Line::class, groups={"Default"}))
      *
      */
     private $line;
 
+    /**
+     * Information about line.
+     * @var Track|null
+     * @Serializer\Type(Track::class)
+     * @SerializeAs({"Default": "Identity"})
+     * @SWG\Property(ref=@Model(type=Track::class, groups={"Identity"}))
+     */
+    private $track;
+
+    /**
+     * Information about line.
+     * @var Trip|null
+     * @Serializer\Type(Trip::class)
+     * @SerializeAs({"Default": "Identity"})
+     * @SWG\Property(ref=@Model(type=Trip::class, groups={"Identity"}))
+     */
+    private $trip;
+
     /**
      * Information about stop.
      * @var Stop
@@ -140,6 +160,26 @@ class Departure implements Fillable
         $this->stop = $stop;
     }
 
+    public function getTrack(): ?Track
+    {
+        return $this->track;
+    }
+
+    public function setTrack(?Track $track): void
+    {
+        $this->track = $track;
+    }
+
+    public function getTrip(): ?Trip
+    {
+        return $this->trip;
+    }
+
+    public function setTrip(?Trip $trip): void
+    {
+        $this->trip = $trip;
+    }
+
     /**
      * @Serializer\VirtualProperty()
      * @Serializer\Type("int")
diff --git a/src/Provider/Database/GenericScheduleRepository.php b/src/Provider/Database/GenericScheduleRepository.php
index d0f12b2..dabcccf 100644
--- a/src/Provider/Database/GenericScheduleRepository.php
+++ b/src/Provider/Database/GenericScheduleRepository.php
@@ -53,7 +53,9 @@ class GenericScheduleRepository extends DatabaseRepository implements ScheduleRe
             ]);
 
         return $schedule->map(function (TripStopEntity $entity) use ($stop) {
-            $line = $entity->getTrip()->getTrack()->getLine();
+            $trip = $entity->getTrip();
+            $track = $trip->getTrack();
+            $line = $track->getLine();
             /** @var StopEntity $last */
             $last = $entity->getTrip()->getTrack()->getStopsInTrack()->last()->getStop();
 
@@ -63,6 +65,8 @@ class GenericScheduleRepository extends DatabaseRepository implements ScheduleRe
                 'stop'      => $stop,
                 'display'   => $last->getName(),
                 'line'      => $this->convert($line),
+                'track'     => $this->convert($track),
+                'trip'      => $this->convert($trip),
             ]);
         });
     }
diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php
index a13c296..d1c3b99 100644
--- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php
+++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php
@@ -129,6 +129,8 @@ class ZtmGdanskDepartureRepository implements DepartureRepository
 
         $departure = clone $real;
         $departure->setDisplay($scheduled->getDisplay());
+        $departure->setTrack($scheduled->getTrack());
+        $departure->setTrip($scheduled->getTrip());
 
         return $departure;
     }
diff --git a/src/Serialization/SerializeAs.php b/src/Serialization/SerializeAs.php
new file mode 100644
index 0000000..43bef9b
--- /dev/null
+++ b/src/Serialization/SerializeAs.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Serialization;
+
+use Doctrine\Common\Annotations\Annotation\Required;
+
+/**
+ * @Annotation
+ * @Target({"PROPERTY","METHOD","ANNOTATION"})
+ */
+class SerializeAs
+{
+    /** @var array<string, string> @Required() */
+    public $map;
+}
diff --git a/src/Service/SerializerContextFactory.php b/src/Service/SerializerContextFactory.php
new file mode 100644
index 0000000..04bff01
--- /dev/null
+++ b/src/Service/SerializerContextFactory.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace App\Service;
+
+use App\Serialization\SerializeAs;
+use Doctrine\Common\Annotations\Reader;
+use JMS\Serializer\Metadata\PropertyMetadata;
+use JMS\Serializer\SerializationContext;
+use Metadata\AdvancedMetadataFactoryInterface;
+use Metadata\ClassHierarchyMetadata;
+use function Kadet\Functional\Transforms\property;
+
+final class SerializerContextFactory
+{
+    private $factory;
+    private $reader;
+
+    public function __construct(AdvancedMetadataFactoryInterface $factory, Reader $reader)
+    {
+        $this->factory = $factory;
+        $this->reader = $reader;
+    }
+
+    public function create($subject, array $groups)
+    {
+        return SerializationContext::create()->setGroups($this->groups($subject, $groups));
+    }
+
+    private function groups($subject, array $groups)
+    {
+        $metadata = $this->factory->getMetadataForClass(is_object($subject) ? get_class($subject) : $subject);
+        $properties = $metadata instanceof ClassHierarchyMetadata
+            ? collect($metadata->classMetadata)->flatMap(property('propertyMetadata'))
+            : $metadata->propertyMetadata;
+
+        $fields = [];
+        /** @var PropertyMetadata $property */
+        foreach ($properties as $property) {
+            try {
+                $annotation = $this->getAnnotationForProperty($property);
+                if ($annotation && !empty($fieldGroups = $this->map($annotation, $groups))) {
+                    $type  = $property->type;
+                    $class = $type['name'] !== 'array' ? $type['name'] : $type['params'][0];
+
+                    $fields[$property->name] = $this->groups($class, $fieldGroups);
+                }
+            } catch (\ReflectionException $e) { }
+        }
+
+        return array_merge($groups, $fields);
+    }
+
+    private function getAnnotationForProperty(PropertyMetadata $metadata)
+    {
+        $reflection = new \ReflectionClass($metadata->class);
+
+        try {
+            $property = $reflection->getProperty($metadata->name);
+            /** @var SerializeAs $annotation */
+            return $this->reader->getPropertyAnnotation($property, SerializeAs::class);
+        } catch (\ReflectionException $exception) {
+            $method = $reflection->getMethod($metadata->getter);
+            return $this->reader->getMethodAnnotation($method, SerializeAs::class);
+        }
+    }
+
+    private function map(SerializeAs $annotation, array $groups)
+    {
+        $result = [];
+
+        foreach ($groups as $group) {
+            if (array_key_exists($group, $annotation->map)) {
+                $result[] = $annotation->map[$group];
+            }
+        }
+
+        return $result;
+    }
+}