From b71eb8cf35683a600bb846329f94a7f24e35f8a2 Mon Sep 17 00:00:00 2001
From: Tobias Gross-Vogt <tgros@tugraz.at>
Date: Wed, 23 Mar 2022 09:00:02 +0100
Subject: [PATCH] optionally include local data

---
 composer.json                                 |  8 +++-
 composer.lock                                 | 38 ++++++++---------
 src/DataProvider/CourseItemDataProvider.php   | 13 +++++-
 src/Entity/Course.php                         |  3 +-
 src/Entity/CourseTrait.php                    | 16 +++++--
 src/Resources/config/services.yaml            |  5 +++
 .../Normalizer/CourseNormalizer.php           | 42 +++++++++++++++++++
 7 files changed, 100 insertions(+), 25 deletions(-)
 create mode 100644 src/Serializer/Normalizer/CourseNormalizer.php

diff --git a/composer.json b/composer.json
index eef9e41..d25f933 100644
--- a/composer.json
+++ b/composer.json
@@ -8,7 +8,13 @@
         "api-platform/core": "^2.6",
         "dbp/relay-base-person-bundle": "^0.1.3",
         "dbp/relay-core-bundle": "^0.1.11",
-        "symfony/framework-bundle": "^5.4"
+        "symfony/config": "^5.4",
+        "symfony/framework-bundle": "^5.4",
+        "symfony/security-bundle": "^5.4",
+        "symfony/security-core": "^5.4",
+        "symfony/security-guard": "^5.4",
+        "symfony/validator": "^5.4",
+        "symfony/yaml": "^5.4"
     },
     "require-dev": {
         "friendsofphp/php-cs-fixer": "^3.0",
diff --git a/composer.lock b/composer.lock
index a88955f..8ce0029 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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": "0df1de6656e47e39e70adb1f054ff0ab",
+    "content-hash": "b9c94ef304f615c39444318e0d0175e8",
     "packages": [
         {
             "name": "api-platform/core",
@@ -4533,16 +4533,16 @@
         },
         {
             "name": "symfony/security-bundle",
-            "version": "v5.4.3",
+            "version": "v5.4.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/security-bundle.git",
-                "reference": "d3239128269ae67d78df535f65f41cf02cabdc6c"
+                "reference": "d6ae2f605fa8e4e0860c1a6574271af2bb4ba16c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/security-bundle/zipball/d3239128269ae67d78df535f65f41cf02cabdc6c",
-                "reference": "d3239128269ae67d78df535f65f41cf02cabdc6c",
+                "url": "https://api.github.com/repos/symfony/security-bundle/zipball/d6ae2f605fa8e4e0860c1a6574271af2bb4ba16c",
+                "reference": "d6ae2f605fa8e4e0860c1a6574271af2bb4ba16c",
                 "shasum": ""
             },
             "require": {
@@ -4615,7 +4615,7 @@
             "description": "Provides a tight integration of the Security component into the Symfony full-stack framework",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/security-bundle/tree/v5.4.3"
+                "source": "https://github.com/symfony/security-bundle/tree/v5.4.5"
             },
             "funding": [
                 {
@@ -4631,20 +4631,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-01-02T09:53:40+00:00"
+            "time": "2022-02-18T16:06:09+00:00"
         },
         {
             "name": "symfony/security-core",
-            "version": "v5.4.3",
+            "version": "v5.4.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/security-core.git",
-                "reference": "b26a44457a4d1a60c79f1c23273e812c4077ce85"
+                "reference": "11d815ccbff929899a4ec545f9f85185071abd12"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/security-core/zipball/b26a44457a4d1a60c79f1c23273e812c4077ce85",
-                "reference": "b26a44457a4d1a60c79f1c23273e812c4077ce85",
+                "url": "https://api.github.com/repos/symfony/security-core/zipball/11d815ccbff929899a4ec545f9f85185071abd12",
+                "reference": "11d815ccbff929899a4ec545f9f85185071abd12",
                 "shasum": ""
             },
             "require": {
@@ -4708,7 +4708,7 @@
             "description": "Symfony Security Component - Core Library",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/security-core/tree/v5.4.3"
+                "source": "https://github.com/symfony/security-core/tree/v5.4.5"
             },
             "funding": [
                 {
@@ -4724,7 +4724,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-01-02T09:53:40+00:00"
+            "time": "2022-02-18T16:06:09+00:00"
         },
         {
             "name": "symfony/security-csrf",
@@ -5586,16 +5586,16 @@
         },
         {
             "name": "symfony/validator",
-            "version": "v5.4.3",
+            "version": "v5.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/validator.git",
-                "reference": "b420894e98f414b9ad5d4494650bf281f6dd6028"
+                "reference": "ab461eab209e3be062ba9c609d37b37e8364dbe4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/validator/zipball/b420894e98f414b9ad5d4494650bf281f6dd6028",
-                "reference": "b420894e98f414b9ad5d4494650bf281f6dd6028",
+                "url": "https://api.github.com/repos/symfony/validator/zipball/ab461eab209e3be062ba9c609d37b37e8364dbe4",
+                "reference": "ab461eab209e3be062ba9c609d37b37e8364dbe4",
                 "shasum": ""
             },
             "require": {
@@ -5679,7 +5679,7 @@
             "description": "Provides tools to validate values",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/validator/tree/v5.4.3"
+                "source": "https://github.com/symfony/validator/tree/v5.4.6"
             },
             "funding": [
                 {
@@ -5695,7 +5695,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-01-26T16:28:35+00:00"
+            "time": "2022-03-02T12:42:23+00:00"
         },
         {
             "name": "symfony/var-dumper",
diff --git a/src/DataProvider/CourseItemDataProvider.php b/src/DataProvider/CourseItemDataProvider.php
index 4b5252b..7d51ffa 100644
--- a/src/DataProvider/CourseItemDataProvider.php
+++ b/src/DataProvider/CourseItemDataProvider.php
@@ -8,7 +8,9 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
 use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
 use Dbp\Relay\BaseCourseBundle\API\CourseProviderInterface;
 use Dbp\Relay\BaseCourseBundle\Entity\Course;
+use Dbp\Relay\CoreBundle\Exception\ApiError;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Response;
 
 final class CourseItemDataProvider extends AbstractController implements ItemDataProviderInterface, RestrictedDataProviderInterface
 {
@@ -28,8 +30,17 @@ final class CourseItemDataProvider extends AbstractController implements ItemDat
     {
         $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
 
+        $options = [];
         $filters = $context['filters'] ?? [];
-        $options = ['lang' => $filters['lang'] ?? 'de'];
+        $options['lang'] = $filters['lang'] ?? 'de';
+
+        if ($include = ($filters['include'] ?? null)) {
+            if ($include === 'localData') {
+                $options['includeLocalData'] = true;
+            } else {
+                throw new ApiError(Response::HTTP_BAD_REQUEST, 'requested inclusion of unknown resource '.$include);
+            }
+        }
 
         return $this->api->getCourseById($id, $options);
     }
diff --git a/src/Entity/Course.php b/src/Entity/Course.php
index 88f9169..9e51ace 100644
--- a/src/Entity/Course.php
+++ b/src/Entity/Course.php
@@ -60,7 +60,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
  *                 "tags" = {"BaseCourse"},
  *                 "parameters" = {
  *                     {"name" = "identifier", "in" = "path", "description" = "Id of course", "required" = true, "type" = "string", "example" = "257571"},
- *                     {"name" = "lang", "in" = "query", "description" = "Language of result", "type" = "string", "enum" = {"de", "en"}, "example" = "de"}
+ *                     {"name" = "lang", "in" = "query", "description" = "Language of result", "type" = "string", "enum" = {"de", "en"}, "example" = "de"},
+ *                     {"name" = "include", "in" = "query", "description" = "Optional resources to include ", "type" = "string", "example" = "localData"}
  *                 }
  *             }
  *         }
diff --git a/src/Entity/CourseTrait.php b/src/Entity/CourseTrait.php
index b36d1f0..32193d5 100644
--- a/src/Entity/CourseTrait.php
+++ b/src/Entity/CourseTrait.php
@@ -6,6 +6,7 @@ namespace Dbp\Relay\BaseCourseBundle\Entity;
 
 use ApiPlatform\Core\Annotation\ApiProperty;
 use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\Ignore;
 
 trait CourseTrait
 {
@@ -86,23 +87,32 @@ trait CourseTrait
         $this->description = $description;
     }
 
+    public function getLocalData(): array
+    {
+        return $this->localData;
+    }
+
     /**
      * Allows attaching local data to a Course object.
      *
      * @param ?mixed $value
      */
-    public function setLocalData(string $key, $value): void
+    public function setLocalDataValue(string $key, $value): void
     {
+        if (!$this->localData) {
+            $this->localData = [];
+        }
         $this->localData[$key] = $value;
     }
 
     /**
+     * @Ignore
      * Gets local data from a Course object.
      *
      * @return ?mixed
      */
-    public function getLocalData(string $key)
+    public function getLocalDataValue(string $key)
     {
-        return $this->localData[$key] ?? null;
+        return $this->localData ? ($this->localData[$key] ?? null) : null;
     }
 }
diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml
index a725920..ed2c03d 100644
--- a/src/Resources/config/services.yaml
+++ b/src/Resources/config/services.yaml
@@ -9,6 +9,11 @@ services:
     autowire: true
     autoconfigure: true
 
+#  Dbp\Relay\BaseCourseBundle\Serializer\Normalizer\CourseNormalizer:
+#    autowire: true
+#    autoconfigure: true
+#    tags: ['serializer.normalizer']
+
   Dbp\Relay\BaseCourseBundle\Service\DummyCourseProvider:
     autowire: true
     autoconfigure: true
diff --git a/src/Serializer/Normalizer/CourseNormalizer.php b/src/Serializer/Normalizer/CourseNormalizer.php
new file mode 100644
index 0000000..d9d69fc
--- /dev/null
+++ b/src/Serializer/Normalizer/CourseNormalizer.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\BaseCourseBundle\Serializer\Normalizer;
+
+use Dbp\Relay\BaseCourseBundle\Entity\Course;
+use Symfony\Component\Serializer\Exception\ExceptionInterface;
+use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
+
+class CourseNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
+{
+    use NormalizerAwareTrait;
+
+    private const ALREADY_CALLED = 'COURSE_NORMALIZER_CURRENT_USER_ALREADY_CALLED';
+
+    /**
+     * @return array|string|int|float|bool|\ArrayObject|null
+     *
+     * @throws ExceptionInterface
+     */
+    public function normalize($object, $format = null, array $context = [])
+    {
+        // TODO: only add localData group if it is requested?
+        // problem: $context['filters'] not available at this point
+        $context['groups'][] = 'BaseCourse:localData';
+        $context[self::ALREADY_CALLED] = true;
+
+        return $this->normalizer->normalize($object, $format, $context);
+    }
+
+    public function supportsNormalization($data, $format = null, array $context = []): bool
+    {
+        if (isset($context[self::ALREADY_CALLED])) {
+            return false;
+        }
+
+        return $data instanceof Course;
+    }
+}
-- 
GitLab