From ddf11ad4f8ab5c7b6fefd0e0f2e61abf2d73d7c1 Mon Sep 17 00:00:00 2001
From: Perreal Guillaume <guillaume.perreal@irstea.fr>
Date: Thu, 23 May 2019 15:34:09 +0200
Subject: [PATCH] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20l'ensemble=20pour?=
 =?UTF-8?q?=20produire=20du=20json-schema=20de=20fa=C3=A7on=20coh=C3=A9ren?=
 =?UTF-8?q?te.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 phpstan.neon                                  |   6 +
 .../Bundle/Controller/AbstractController.php  |  15 +-
 .../Bundle/Controller/OperationController.php |  40 ++++--
 .../Bundle/Controller/ResourceController.php  |  14 +-
 .../IrsteaApiMetadataExtension.php            |   2 +-
 .../Bundle/Resources/config/routing.xml       |   4 +-
 .../Bundle/Resources/config/services.xml      |  14 +-
 .../URI/OperationMetadataURIGenerator.php     |  22 +--
 .../URI/ResourceMetadataURIGenerator.php      |  14 +-
 .../Serializer/ObjectMetadataNormalizer.php   |  15 +-
 .../Serializer/TypeMetadataNormalizer.php     |   5 +-
 src/Exception/InvalidArgumentException.php    |  28 ++++
 .../Operation/NullOperationFactory.php        |   8 +-
 .../Operation/OperationFactoryInterface.php   |  17 ++-
 .../Operation/ResourceOperationFactory.php    |  67 +++++----
 .../Property/ResourcePropertyFactory.php      |   2 -
 src/Factory/Type/CollectionTypeFactory.php    |   3 +-
 src/Factory/Type/ObjectTypeFactory.php        |   1 -
 src/Factory/Type/ResourceTypeFactory.php      |  11 +-
 src/Model/Identity/OperationIdentity.php      | 130 ++++++++++++++++++
 .../Identity/OperationIdentityInterface.php   |  47 +++++++
 src/Model/Identity/ResourceIdentity.php       | 103 ++++++++++++++
 .../Identity/ResourceIdentityInterface.php    |  37 +++++
 src/Model/MapMetadata.php                     |   1 -
 src/Model/OperationMetadata.php               |  58 +++-----
 src/Model/ResourceMetadata.php                |  39 +++---
 src/Service/ResourceMap.php                   |  51 +++++--
 src/Service/ResourceMapInterface.php          |  19 ++-
 src/URI/CompositeURIGenerator.php             |   4 +-
 src/URI/URIGeneratorInterface.php             |   4 +-
 tests/Fixtures/Entity/Address.php             |  16 ---
 tests/FunctionalTest.php                      |   4 +-
 tests/Service/ResourceMapTest.php             |  27 ++--
 tests/TestRemoteRefProvider.php               |   4 +-
 34 files changed, 608 insertions(+), 224 deletions(-)
 create mode 100644 src/Exception/InvalidArgumentException.php
 create mode 100644 src/Model/Identity/OperationIdentity.php
 create mode 100644 src/Model/Identity/OperationIdentityInterface.php
 create mode 100644 src/Model/Identity/ResourceIdentity.php
 create mode 100644 src/Model/Identity/ResourceIdentityInterface.php

diff --git a/phpstan.neon b/phpstan.neon
index d25b5cc..0e7d34b 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,4 +2,10 @@ includes:
   - vendor/irstea/phpstan-config/strict.neon
   - vendor/irstea/phpstan-config/phpunit.neon
 
+parameters:
+    level: 7
+
+    paths:
+        - src
+        - tests
 
diff --git a/src/Bridge/Symfony/Bundle/Controller/AbstractController.php b/src/Bridge/Symfony/Bundle/Controller/AbstractController.php
index 99dc02f..60b6d43 100644
--- a/src/Bridge/Symfony/Bundle/Controller/AbstractController.php
+++ b/src/Bridge/Symfony/Bundle/Controller/AbstractController.php
@@ -22,6 +22,7 @@ namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\Controller;
 
 use Irstea\ApiMetadata\Factory\Type\TypeFactoryInterface;
 use Irstea\ApiMetadata\Service\ResourceMapInterface;
+use Irstea\ApiMetadata\URI\URIGeneratorInterface;
 use Symfony\Component\Serializer\SerializerInterface;
 
 /**
@@ -42,20 +43,26 @@ abstract class AbstractController
     /** @var SerializerInterface */
     protected $serializer;
 
+    /** @var URIGeneratorInterface */
+    protected $uriGenerator;
+
     /**
      * ResourceListAction constructor.
      *
-     * @param ResourceMapInterface $resourceMap
-     * @param TypeFactoryInterface $typeFactory
-     * @param SerializerInterface  $serializer
+     * @param ResourceMapInterface  $resourceMap
+     * @param TypeFactoryInterface  $typeFactory
+     * @param SerializerInterface   $serializer
+     * @param URIGeneratorInterface $uriGenerator
      */
     public function __construct(
         ResourceMapInterface $resourceMap,
         TypeFactoryInterface $typeFactory,
-        SerializerInterface $serializer
+        SerializerInterface $serializer,
+        URIGeneratorInterface $uriGenerator
     ) {
         $this->resourceMap = $resourceMap;
         $this->typeFactory = $typeFactory;
         $this->serializer = $serializer;
+        $this->uriGenerator = $uriGenerator;
     }
 }
diff --git a/src/Bridge/Symfony/Bundle/Controller/OperationController.php b/src/Bridge/Symfony/Bundle/Controller/OperationController.php
index d6d2f5a..9606431 100644
--- a/src/Bridge/Symfony/Bundle/Controller/OperationController.php
+++ b/src/Bridge/Symfony/Bundle/Controller/OperationController.php
@@ -20,9 +20,14 @@
 
 namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\Controller;
 
+use Irstea\ApiMetadata\Factory\Context;
 use Irstea\ApiMetadata\Factory\Operation\OperationFactoryInterface;
 use Irstea\ApiMetadata\Factory\Type\TypeFactoryInterface;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentity;
 use Irstea\ApiMetadata\Service\ResourceMapInterface;
+use Irstea\ApiMetadata\URI\URIGeneratorInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Serializer\SerializerInterface;
 
@@ -42,15 +47,17 @@ class OperationController extends AbstractController
      * @param ResourceMapInterface      $resourceMap
      * @param TypeFactoryInterface      $typeFactory
      * @param SerializerInterface       $serializer
+     * @param URIGeneratorInterface     $uriGenerator
      * @param OperationFactoryInterface $operationFactory
      */
     public function __construct(
         ResourceMapInterface $resourceMap,
         TypeFactoryInterface $typeFactory,
         SerializerInterface $serializer,
+        URIGeneratorInterface $uriGenerator,
         OperationFactoryInterface $operationFactory
     ) {
-        parent::__construct($resourceMap, $typeFactory, $serializer);
+        parent::__construct($resourceMap, $typeFactory, $serializer, $uriGenerator);
         $this->operationFactory = $operationFactory;
     }
 
@@ -63,10 +70,21 @@ class OperationController extends AbstractController
      */
     public function item(string $shortName, string $type, string $name): Response
     {
-        [$className, $ctx] = $this->setupContext($shortName);
-        $operation = $this->operationFactory->create($className, $type, $name, $ctx);
+        $resourceId = $this->resourceMap->getByShortName($shortName);
 
-        return $this->createResponse($operation, $ctx);
+        $operationId = OperationIdentity::fromValues(
+            $resourceId->getClass(),
+            $resourceId->getShortName(),
+            $type,
+            $name
+        );
+
+        $ctx = new Context($this->typeFactory);
+        $metadata = $this->operationFactory->createOperation($operationId, $ctx);
+
+        $json = $this->serializer->serialize($metadata, 'json', ['root' => $metadata]);
+
+        return JsonResponse::fromJsonString($json);
     }
 
     /**
@@ -74,16 +92,18 @@ class OperationController extends AbstractController
      *
      * @return Response
      */
-    public function collection(string $shortName): Response
+    public function collection(Request $request, string $shortName): Response
     {
-        [$className, $ctx] = $this->setupContext($shortName);
-
+        $resourceId = $this->resourceMap->getByShortName($shortName);
         $operations = [];
 
-        foreach ($this->operationFactory->enumerate($className, $ctx) as $key => $operation) {
-            $operations[$key] = $operation;
+        foreach ($this->operationFactory->enumerateOperations($resourceId) as $operationId) {
+            $operations[] = ['$ref' => $this->uriGenerator->generateURI($operationId)];
         }
 
-        return $this->createResponse($operations, $ctx);
+        return JsonResponse::create([
+            '$id'        => $request->getUri(),
+            'operations' => $operations,
+        ]);
     }
 }
diff --git a/src/Bridge/Symfony/Bundle/Controller/ResourceController.php b/src/Bridge/Symfony/Bundle/Controller/ResourceController.php
index b4dd24c..a6c808a 100644
--- a/src/Bridge/Symfony/Bundle/Controller/ResourceController.php
+++ b/src/Bridge/Symfony/Bundle/Controller/ResourceController.php
@@ -20,9 +20,9 @@
 
 namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\Controller;
 
-use Irstea\ApiMetadata\Exception\ResourceNotFoundException;
 use Irstea\ApiMetadata\Factory\Context;
 use Irstea\ApiMetadata\Helper\PropertyInfoType;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
 use Irstea\ApiMetadata\Model\Reference;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -40,14 +40,11 @@ class ResourceController extends AbstractController
      */
     public function item(string $shortName): Response
     {
-        $className = $this->resourceMap->getClassName($shortName);
-        if ($className === null) {
-            throw new ResourceNotFoundException($shortName);
-        }
+        $resourceId = $this->resourceMap->getByShortName($shortName);
 
         $ctx = new Context($this->typeFactory);
 
-        $type = PropertyInfoType::create($className);
+        $type = PropertyInfoType::create($resourceId->getClass());
 
         $metadata = $ctx->createType($type, $ctx);
         if ($metadata instanceof Reference) {
@@ -67,8 +64,9 @@ class ResourceController extends AbstractController
     public function collection(Request $request): Response
     {
         $resources = [];
-        foreach ($this->resourceMap as $shortName) {
-            $resources[$shortName] = ['$ref' => $shortName];
+        /** @var ResourceIdentityInterface $resourceId */
+        foreach ($this->resourceMap as $resourceId) {
+            $resources[$resourceId->getShortName()] = ['$ref' => $this->uriGenerator->generateURI($resourceId)];
         }
 
         return JsonResponse::create([
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/IrsteaApiMetadataExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/IrsteaApiMetadataExtension.php
index c6f6f77..5414724 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/IrsteaApiMetadataExtension.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/IrsteaApiMetadataExtension.php
@@ -33,7 +33,7 @@ class IrsteaApiMetadataExtension extends Extension
     /**
      * {@inheritdoc}
      */
-    public function load(array $configs, ContainerBuilder $container)
+    public function load(array $configs, ContainerBuilder $container): void
     {
         $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
         $loader->load('services.xml');
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/routing.xml b/src/Bridge/Symfony/Bundle/Resources/config/routing.xml
index 550fbf5..923c6e9 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/routing.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/routing.xml
@@ -7,7 +7,7 @@
 >
 
    <route id="api_metadata_operation_get" path="/metadata/{shortName}/operations/{type}_{name}.{_format}" methods="GET">
-        <default key="_controller">api_metadata.operation.resource::item</default>
+        <default key="_controller">api_metadata.controller.operation::item</default>
         <default key="_format">json</default>
 
        <requirement key="type">(item|collection)</requirement>
@@ -15,7 +15,7 @@
     </route>
 
    <route id="api_metadata_operation_list" path="/metadata/{shortName}/operations.{_format}" methods="GET">
-        <default key="_controller">api_metadata.operation.resource::collection</default>
+        <default key="_controller">api_metadata.controller.operation::collection</default>
         <default key="_format">json</default>
     </route>
 
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/services.xml b/src/Bridge/Symfony/Bundle/Resources/config/services.xml
index 126bc7d..3154a83 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/services.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/services.xml
@@ -17,6 +17,7 @@
             <argument type="service" id="irstea_api_metadata.service.resource_map"/>
             <argument type="service" id="irstea_api_metadata.type_factory"/>
             <argument type="service" id="serializer"/>
+            <argument type="service" id="irstea_api_metadata.uri_generator"/>
         </service>
 
         <service
@@ -27,6 +28,7 @@
             <argument type="service" id="irstea_api_metadata.service.resource_map"/>
             <argument type="service" id="irstea_api_metadata.type_factory"/>
             <argument type="service" id="serializer"/>
+            <argument type="service" id="irstea_api_metadata.uri_generator"/>
             <argument type="service" id="irstea_api_metadata.operation_factory"/>
         </service>
 
@@ -179,21 +181,21 @@
         </service>
 
         <service
-            id="irstea_api_metadata.uri_generator.resource"
-            class="Irstea\ApiMetadata\Bridge\Symfony\Bundle\URI\ResourceMetadataURIGenerator"
+            id="irstea_api_metadata.uri_generator.operation"
+            class="Irstea\ApiMetadata\Bridge\Symfony\Bundle\URI\OperationMetadataURIGenerator"
         >
             <argument type="service" id="router"/>
 
-            <tag name="irstea_api_metadata.uri_generator" priority="30"></tag>
+            <tag name="irstea_api_metadata.uri_generator" priorty="30"></tag>
         </service>
 
         <service
-            id="irstea_api_metadata.uri_generator.operation"
-            class="Irstea\ApiMetadata\Bridge\Symfony\Bundle\URI\OperationMetadataURIGenerator"
+            id="irstea_api_metadata.uri_generator.resource"
+            class="Irstea\ApiMetadata\Bridge\Symfony\Bundle\URI\ResourceMetadataURIGenerator"
         >
             <argument type="service" id="router"/>
 
-            <tag name="irstea_api_metadata.uri_generator" priorty="20"></tag>
+            <tag name="irstea_api_metadata.uri_generator" priority="20"></tag>
         </service>
 
         <service
diff --git a/src/Bridge/Symfony/Bundle/URI/OperationMetadataURIGenerator.php b/src/Bridge/Symfony/Bundle/URI/OperationMetadataURIGenerator.php
index 3402f1f..e6ce839 100644
--- a/src/Bridge/Symfony/Bundle/URI/OperationMetadataURIGenerator.php
+++ b/src/Bridge/Symfony/Bundle/URI/OperationMetadataURIGenerator.php
@@ -21,7 +21,7 @@
 namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\URI;
 
 use Assert\Assertion;
-use Irstea\ApiMetadata\Model\OperationMetadata;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentityInterface;
 use Irstea\ApiMetadata\URI\URIGeneratorInterface;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 
@@ -44,27 +44,27 @@ class OperationMetadataURIGenerator implements URIGeneratorInterface
     /**
      * {@inheritdoc}
      */
-    public function generateURI($resource): string
+    public function generateURI($operation): string
     {
-        Assertion::isInstanceOf($resource, OperationMetadata::class);
-        /* @var OperationMetadata $resource */
+        Assertion::isInstanceOf($operation, OperationIdentityInterface::class);
+        /* @var OperationIdentityInterface $operation */
 
         return $this->urlGenerator->generate(
             'api_metadata_operation_get',
             [
-                'shortName' => $resource->getResource(),
-                'type'      => $resource->getType(),
-                'name'      => $resource->getName(),
-            ],
+                    'shortName' => $operation->getShortName(),
+                    'type'      => $operation->getType(),
+                    'name'      => $operation->getName(),
+                ],
             UrlGeneratorInterface::ABSOLUTE_URL
-        ) . '#';
+            ) . '#';
     }
 
     /**
      * {@inheritdoc}
      */
-    public function supports($resource): bool
+    public function supports($operation): bool
     {
-        return $resource instanceof OperationMetadata;
+        return $operation instanceof OperationIdentityInterface;
     }
 }
diff --git a/src/Bridge/Symfony/Bundle/URI/ResourceMetadataURIGenerator.php b/src/Bridge/Symfony/Bundle/URI/ResourceMetadataURIGenerator.php
index 878e6e6..b42af0f 100644
--- a/src/Bridge/Symfony/Bundle/URI/ResourceMetadataURIGenerator.php
+++ b/src/Bridge/Symfony/Bundle/URI/ResourceMetadataURIGenerator.php
@@ -21,7 +21,7 @@
 namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\URI;
 
 use Assert\Assertion;
-use Irstea\ApiMetadata\Model\ResourceMetadata;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
 use Irstea\ApiMetadata\URI\URIGeneratorInterface;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 
@@ -46,16 +46,14 @@ class ResourceMetadataURIGenerator implements URIGeneratorInterface
      */
     public function generateURI($resource): string
     {
-        Assertion::isInstanceOf($resource, ResourceMetadata::class);
-        /* @var ResourceMetadata $resource */
+        Assertion::isInstanceOf($resource, ResourceIdentityInterface::class);
+        /* @var ResourceIdentityInterface $resource */
 
         return $this->urlGenerator->generate(
             'api_metadata_resource_get',
-            [
-                'shortName' => $resource->getShortName(),
-            ],
+            ['shortName' => $resource->getShortName()],
             UrlGeneratorInterface::ABSOLUTE_URL
-        ) . '#';
+            ) . '#';
     }
 
     /**
@@ -63,6 +61,6 @@ class ResourceMetadataURIGenerator implements URIGeneratorInterface
      */
     public function supports($resource): bool
     {
-        return $resource instanceof ResourceMetadata;
+        return $resource instanceof ResourceIdentityInterface;
     }
 }
diff --git a/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php b/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php
index f8cbb36..f2f331c 100644
--- a/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php
+++ b/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php
@@ -110,9 +110,7 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
 
         $operations = [];
         foreach ($root->getOperations() as $operation) {
-            $operations[$operation->getKey()] = [
-                '$ref' => $this->generateURI($operation, $context, true),
-            ];
+            $operations[] = ['$ref' => $this->generateURI($operation, $context, true)];
         }
 
         return array_merge(
@@ -122,14 +120,14 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
             ],
             $data,
             [
-                'operations'  => (object) $operations,
+                'operations'  => $operations,
                 'definitions' => (object) ($defs->getArrayCopy()),
             ]
         );
     }
 
     /**
-     * @param $resource
+     * @param mixed $resource
      * @param array $context
      * @param bool  $relative
      *
@@ -151,7 +149,7 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
     }
 
     /**
-     * @param ObjectMetadata $root
+     * @param ObjectMetadata $object
      * @param string|null    $format
      * @param array          $context
      *
@@ -191,6 +189,9 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
     {
         $attrs = $object->getTypeAttributes();
 
-        return ['type' => $object->getBaseType()] + $this->normalizer->normalize($attrs, $format, $context);
+        $normalizedAttrs = $this->normalizer->normalize($attrs, $format, $context);
+        Assertion::isArray($normalizedAttrs);
+
+        return ['type' => $object->getBaseType()] + $normalizedAttrs;
     }
 }
diff --git a/src/Bridge/Symfony/Serializer/TypeMetadataNormalizer.php b/src/Bridge/Symfony/Serializer/TypeMetadataNormalizer.php
index 64e8ff9..260282c 100644
--- a/src/Bridge/Symfony/Serializer/TypeMetadataNormalizer.php
+++ b/src/Bridge/Symfony/Serializer/TypeMetadataNormalizer.php
@@ -50,6 +50,9 @@ class TypeMetadataNormalizer implements NormalizerAwareInterface, NormalizerInte
         /** @var TypeMetadata $object */
         $attrs = $object->getTypeAttributes();
 
-        return ['type' => $object->getBaseType()] + $this->normalizer->normalize($attrs, $format, $context);
+        $normalizedAttrs = $this->normalizer->normalize($attrs, $format, $context);
+        Assertion::isArray($normalizedAttrs);
+
+        return ['type' => $object->getBaseType()] + $normalizedAttrs;
     }
 }
diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000..d75fa45
--- /dev/null
+++ b/src/Exception/InvalidArgumentException.php
@@ -0,0 +1,28 @@
+<?php declare(strict_types=1);
+/*
+ * This file is part of "irstea/api-metadata".
+ *
+ * Copyright (C) 2019 IRSTEA
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License and the GNU
+ * Lesser General Public License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+namespace Irstea\ApiMetadata\Exception;
+
+/**
+ * Class InvalidArgumentException.
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ApiMetadataException
+{
+}
diff --git a/src/Factory/Operation/NullOperationFactory.php b/src/Factory/Operation/NullOperationFactory.php
index 0a6a9eb..b1b77e8 100644
--- a/src/Factory/Operation/NullOperationFactory.php
+++ b/src/Factory/Operation/NullOperationFactory.php
@@ -22,6 +22,8 @@ namespace Irstea\ApiMetadata\Factory\Operation;
 
 use Irstea\ApiMetadata\Exception\OperationNotFoundException;
 use Irstea\ApiMetadata\Factory\ContextInterface;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentityInterface;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
 use Irstea\ApiMetadata\Model\OperationMetadata;
 
 /**
@@ -32,15 +34,15 @@ class NullOperationFactory implements OperationFactoryInterface
     /**
      * {@inheritdoc}
      */
-    public function create(string $class, string $type, string $name, ContextInterface $context): OperationMetadata
+    public function createOperation(OperationIdentityInterface $operationId, ContextInterface $context): OperationMetadata
     {
-        throw new OperationNotFoundException("No operation metadata for $class/$name ($type)");
+        throw new OperationNotFoundException('No operation metadata for ' . $operationId->getClass());
     }
 
     /**
      * {@inheritdoc}
      */
-    public function enumerate(string $class, ContextInterface $context): array
+    public function enumerateOperations(ResourceIdentityInterface $resourceId): array
     {
         return [];
     }
diff --git a/src/Factory/Operation/OperationFactoryInterface.php b/src/Factory/Operation/OperationFactoryInterface.php
index ce2ea7d..7357e8c 100644
--- a/src/Factory/Operation/OperationFactoryInterface.php
+++ b/src/Factory/Operation/OperationFactoryInterface.php
@@ -21,6 +21,8 @@
 namespace Irstea\ApiMetadata\Factory\Operation;
 
 use Irstea\ApiMetadata\Factory\ContextInterface;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentityInterface;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
 use Irstea\ApiMetadata\Model\OperationMetadata;
 
 /**
@@ -29,20 +31,17 @@ use Irstea\ApiMetadata\Model\OperationMetadata;
 interface OperationFactoryInterface
 {
     /**
-     * @param string           $class
-     * @param string           $type
-     * @param string           $name
-     * @param ContextInterface $context
+     * @param OperationIdentityInterface $operationId
+     * @param ContextInterface           $context
      *
      * @return OperationMetadata
      */
-    public function create(string $class, string $type, string $name, ContextInterface $context): OperationMetadata;
+    public function createOperation(OperationIdentityInterface $operationId, ContextInterface $context): OperationMetadata;
 
     /**
-     * @param string           $class
-     * @param ContextInterface $context
+     * @param ResourceIdentityInterface $resourceId
      *
-     * @return OperationMetadata[]
+     * @return OperationIdentityInterface[]
      */
-    public function enumerate(string $class, ContextInterface $context): array;
+    public function enumerateOperations(ResourceIdentityInterface $resourceId): array;
 }
diff --git a/src/Factory/Operation/ResourceOperationFactory.php b/src/Factory/Operation/ResourceOperationFactory.php
index 91382a2..77d91fe 100644
--- a/src/Factory/Operation/ResourceOperationFactory.php
+++ b/src/Factory/Operation/ResourceOperationFactory.php
@@ -25,15 +25,20 @@ use ApiPlatform\Core\Api\OperationType;
 use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
 use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
 use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
+use Irstea\ApiMetadata\Exception\InvalidArgumentException;
 use Irstea\ApiMetadata\Exception\OperationNotFoundException;
 use Irstea\ApiMetadata\Factory\ContextInterface;
-use Irstea\ApiMetadata\Factory\Type\TypeFactoryInterface;
 use Irstea\ApiMetadata\Helper\PropertyInfoType;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentity;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentityInterface;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
 use Irstea\ApiMetadata\Model\OperationMetadata;
 use Irstea\ApiMetadata\Model\TypeMetadata;
 
 /**
  * Class ResourceOperationFactory.
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @TODO Refactorer la classe
  */
 class ResourceOperationFactory implements OperationFactoryInterface
 {
@@ -55,7 +60,6 @@ class ResourceOperationFactory implements OperationFactoryInterface
      * @param ResourceMetadataFactoryInterface $metadataFactory
      * @param OperationMethodResolverInterface $methodResolver
      * @param OperationPathResolverInterface   $pathResolver
-     * @param TypeFactoryInterface             $typeFactory
      * @param OperationFactoryInterface        $next
      */
     public function __construct(
@@ -73,70 +77,75 @@ class ResourceOperationFactory implements OperationFactoryInterface
     /**
      * {@inheritdoc}
      */
-    public function create(string $class, string $type, string $name, ContextInterface $context): OperationMetadata
+    public function createOperation(OperationIdentityInterface $operationId, ContextInterface $context): OperationMetadata
     {
-        $resource = $this->metadataFactory->create($class);
+        $class = $operationId->getClass();
+        $type = $operationId->getType();
+
+        $resourceMetadata = $this->metadataFactory->create($class);
 
-        switch ($type) {
+        switch ($operationId->getType()) {
              case OperationType::ITEM:
-                 $ops = $resource->getItemOperations();
+                 $ops = $resourceMetadata->getItemOperations();
                  break;
 
             case OperationType::COLLECTION:
-                $ops = $resource->getCollectionOperations();
+                $ops = $resourceMetadata->getCollectionOperations();
                 break;
 
             default:
-                throw new \InvalidArgumentException("unhandled operationt type: $type");
+                throw new \InvalidArgumentException("unhandled operation type: $type");
         }
 
         foreach ((array) $ops as $opName => $attributes) {
-            if ($opName === $name) {
-                return $this->extractMetadata($class, $resource, $type, $name, $attributes, $context);
+            if ($opName === $operationId->getName()) {
+                return $this->extractMetadata($operationId, $resourceMetadata, $attributes, $context);
             }
         }
 
-        throw new OperationNotFoundException("unknown ${type} operation: ${name}");
+        throw new OperationNotFoundException("unknown ${type} operation: " . $operationId->getName());
     }
 
     /**
      * {@inheritdoc}
      */
-    public function enumerate(string $class, ContextInterface $context): array
+    public function enumerateOperations(ResourceIdentityInterface $resourceId): array
     {
-        $operations = $this->next->enumerate($class, $context);
+        $class = $resourceId->getClass();
+
+        $operations = $this->next->enumerateOperations($resourceId);
 
         $resource = $this->metadataFactory->create($class);
 
         foreach ((array) $resource->getItemOperations() as $name => $attributes) {
-            $operation = $this->extractMetadata($class, $resource, OperationType::ITEM, $name, $attributes, $context);
-            $operations[$operation->getKey()] = $operation;
+            $operations[] = OperationIdentity::fromValues($resourceId->getClass(), $resourceId->getShortName(), OperationType::ITEM, $name);
         }
 
         foreach ((array) $resource->getCollectionOperations() as $name => $attributes) {
-            $operation = $this->extractMetadata($class, $resource, OperationType::COLLECTION, $name, $attributes, $context);
-            $operations[$operation->getKey()] = $operation;
+            $operations[] = OperationIdentity::fromValues($resourceId->getClass(), $resourceId->getShortName(), OperationType::COLLECTION, $name);
         }
 
         return $operations;
     }
 
     /**
-     * @param string           $class
-     * @param ResourceMetadata $resource
-     * @param string           $type
-     * @param string           $name
-     * @param array            $attributes
-     * @param ContextInterface $context
+     * @param OperationIdentityInterface $operationId
+     * @param ResourceMetadata           $resource
+     * @param array                      $attributes
+     * @param ContextInterface           $context
      *
      * @return OperationMetadata
      */
-    private function extractMetadata(string $class, ResourceMetadata $resource, string $type, string $name, array $attributes, ContextInterface $context): OperationMetadata
+    private function extractMetadata(OperationIdentityInterface $operationId, ResourceMetadata $resource, array $attributes, ContextInterface $context): OperationMetadata
     {
-        $path = $this->pathResolver->resolveOperationPath($resource->getShortName() ?: $class, $attributes, $type);
+        $class = $operationId->getClass();
+        $type = $operationId->getType();
+        $name = $operationId->getName();
+
+        $path = $this->pathResolver->resolveOperationPath($resource->getShortName() ?: $class, $attributes, $operationId->getType());
         $path = str_replace('.{_format}', '', $path);
 
-        switch ($type) {
+        switch ($operationId->getType()) {
             case OperationType::ITEM:
                 $method = $this->methodResolver->getItemOperationMethod($class, $name);
                 $input = $resource->getItemOperationAttribute($name, 'input', $class, true);
@@ -154,9 +163,7 @@ class ResourceOperationFactory implements OperationFactoryInterface
         }
 
         return new OperationMetadata(
-            $resource->getShortName(),
-            $type,
-            $name,
+            $operationId,
             $path,
             $method,
             $method !== 'GET' && $method !== 'HEAD' ? $this->findInOuts($input, $context) : null,
@@ -187,6 +194,6 @@ class ResourceOperationFactory implements OperationFactoryInterface
             return $context->createType(PropertyInfoType::create($definition), $context);
         }
 
-        throw new \InvalidArgumentException('unknown input/output argument');
+        throw new InvalidArgumentException('unknown input/output argument');
     }
 }
diff --git a/src/Factory/Property/ResourcePropertyFactory.php b/src/Factory/Property/ResourcePropertyFactory.php
index ce76d76..4f77f10 100644
--- a/src/Factory/Property/ResourcePropertyFactory.php
+++ b/src/Factory/Property/ResourcePropertyFactory.php
@@ -25,7 +25,6 @@ use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
 use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
 use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
 use Irstea\ApiMetadata\Factory\ContextInterface;
-use Irstea\ApiMetadata\Factory\Type\TypeFactoryInterface;
 use Irstea\ApiMetadata\Helper\PropertyInfoType;
 use Irstea\ApiMetadata\Model\PropertyMetadata;
 
@@ -48,7 +47,6 @@ class ResourcePropertyFactory implements PropertyFactoryInterface
      *
      * @param PropertyNameCollectionFactoryInterface $namesFactory
      * @param PropertyMetadataFactoryInterface       $metadataFactory
-     * @param TypeFactoryInterface                   $typeFactory
      * @param PropertyFactoryInterface|null          $next
      */
     public function __construct(
diff --git a/src/Factory/Type/CollectionTypeFactory.php b/src/Factory/Type/CollectionTypeFactory.php
index 26a3e91..8bb60ff 100644
--- a/src/Factory/Type/CollectionTypeFactory.php
+++ b/src/Factory/Type/CollectionTypeFactory.php
@@ -20,6 +20,7 @@
 
 namespace Irstea\ApiMetadata\Factory\Type;
 
+use Irstea\ApiMetadata\Exception\InvalidArgumentException;
 use Irstea\ApiMetadata\Factory\ContextInterface;
 use Irstea\ApiMetadata\Helper\PropertyInfoType;
 use Irstea\ApiMetadata\Model\ArrayMetadata;
@@ -56,6 +57,6 @@ class CollectionTypeFactory extends TypeFactoryDecorator
                 );
         }
 
-        throw new \InvalidArgumentException("unsupported key type: ${keyType}");
+        throw new InvalidArgumentException('unsupported key type');
     }
 }
diff --git a/src/Factory/Type/ObjectTypeFactory.php b/src/Factory/Type/ObjectTypeFactory.php
index fdceaf1..db57978 100644
--- a/src/Factory/Type/ObjectTypeFactory.php
+++ b/src/Factory/Type/ObjectTypeFactory.php
@@ -39,7 +39,6 @@ class ObjectTypeFactory extends AbstractClassTypeFactory
     /**
      * ObjectTypeFactory constructor.
      *
-     * @param TypeFactoryInterface     $typeFactory
      * @param PropertyFactoryInterface $propertyFactory
      * @param TypeFactoryInterface     $next
      */
diff --git a/src/Factory/Type/ResourceTypeFactory.php b/src/Factory/Type/ResourceTypeFactory.php
index e6e5295..7896483 100644
--- a/src/Factory/Type/ResourceTypeFactory.php
+++ b/src/Factory/Type/ResourceTypeFactory.php
@@ -27,6 +27,7 @@ use Irstea\ApiMetadata\Factory\ContextInterface;
 use Irstea\ApiMetadata\Factory\Operation\OperationFactoryInterface;
 use Irstea\ApiMetadata\Factory\Property\PropertyFactoryInterface;
 use Irstea\ApiMetadata\Factory\ReferenceFactoryInterface;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentity;
 use Irstea\ApiMetadata\Model\ResourceMetadata;
 use Irstea\ApiMetadata\Model\TypeMetadata;
 
@@ -103,16 +104,16 @@ class ResourceTypeFactory extends AbstractClassTypeFactory
     private function doCreateClass(string $className, ContextInterface $context): TypeMetadata
     {
         $resource = $this->metadataFactory->create($className);
-
         $shortName = $resource->getShortName();
-        Assertion::string($shortName);
+        Assertion::notNull($shortName);
+
+        $resourceId = ResourceIdentity::fromValues($className, $shortName);
 
         return new ResourceMetadata(
-            $className,
-            $shortName,
+            $resourceId,
             null, //$parentMeta,
             $this->propertyFactory->enumerate($className, $context),
-            $this->operationFactory->enumerate($className, $context)
+            $this->operationFactory->enumerateOperations($resourceId)
         );
     }
 }
diff --git a/src/Model/Identity/OperationIdentity.php b/src/Model/Identity/OperationIdentity.php
new file mode 100644
index 0000000..524303f
--- /dev/null
+++ b/src/Model/Identity/OperationIdentity.php
@@ -0,0 +1,130 @@
+<?php declare(strict_types=1);
+/*
+ * This file is part of "irstea/api-metadata".
+ *
+ * Copyright (C) 2019 IRSTEA
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License and the GNU
+ * Lesser General Public License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+namespace Irstea\ApiMetadata\Model\Identity;
+
+use ApiPlatform\Core\Api\OperationType;
+use Assert\Assertion;
+
+/**
+ * Class OperationID.
+ */
+final class OperationIdentity implements OperationIdentityInterface
+{
+    /** @var ResourceIdentityInterface */
+    private $resourceId;
+
+    /** @var string */
+    private $type;
+
+    /** @var string */
+    private $name;
+
+    /**
+     * OperationIdentifiers constructor.
+     *
+     * @param ResourceIdentityInterface $resourceId
+     * @param string                    $type
+     * @param string                    $name
+     */
+    public function __construct(ResourceIdentityInterface $resourceId, string $type, string $name)
+    {
+        Assertion::choice($type, OperationType::TYPES);
+
+        $this->resourceId = $resourceId;
+        $this->type = $type;
+        $this->name = $name;
+    }
+
+    /**
+     * Get shortName.
+     *
+     * @return string
+     */
+    public function getClass(): string
+    {
+        return $this->resourceId->getClass();
+    }
+
+    /**
+     * Get shortName.
+     *
+     * @return string
+     */
+    public function getShortName(): string
+    {
+        return $this->resourceId->getShortName();
+    }
+
+    /**
+     * Get type.
+     *
+     * @return string
+     */
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    /**
+     * Get name.
+     *
+     * @return string
+     */
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString(): string
+    {
+        return sprintf('%s::%s_%s', $this->resourceId->getShortName(), $this->name, $this->type);
+    }
+
+    /**
+     * @param string $class
+     * @param string $shortName
+     * @param string $type
+     * @param string $name
+     *
+     * @return self
+     */
+    public static function fromValues(string $class, string $shortName, string $type, string $name): self
+    {
+        return new self(ResourceIdentity::fromValues($class, $shortName), $type, $name);
+    }
+
+    /**
+     * @param OperationIdentityInterface $operation
+     *
+     * @return self
+     */
+    public static function fromOperation(OperationIdentityInterface $operation): self
+    {
+        if ($operation instanceof self) {
+            return $operation;
+        }
+
+        return new self(ResourceIdentity::fromValues($operation->getClass(), $operation->getShortName()), $operation->getType(), $operation->getName());
+    }
+}
diff --git a/src/Model/Identity/OperationIdentityInterface.php b/src/Model/Identity/OperationIdentityInterface.php
new file mode 100644
index 0000000..4b7dec7
--- /dev/null
+++ b/src/Model/Identity/OperationIdentityInterface.php
@@ -0,0 +1,47 @@
+<?php declare(strict_types=1);
+/*
+ * This file is part of "irstea/api-metadata".
+ *
+ * Copyright (C) 2019 IRSTEA
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License and the GNU
+ * Lesser General Public License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+namespace Irstea\ApiMetadata\Model\Identity;
+
+/**
+ * Class OperationIdentityInterface.
+ */
+interface OperationIdentityInterface
+{
+    /**
+     * @return string
+     */
+    public function getShortName(): string;
+
+    /**
+     * @return string
+     */
+    public function getClass(): string;
+
+    /**
+     * @return string
+     */
+    public function getType(): string;
+
+    /**
+     * @return string
+     */
+    public function getName(): string;
+}
diff --git a/src/Model/Identity/ResourceIdentity.php b/src/Model/Identity/ResourceIdentity.php
new file mode 100644
index 0000000..0915ca8
--- /dev/null
+++ b/src/Model/Identity/ResourceIdentity.php
@@ -0,0 +1,103 @@
+<?php declare(strict_types=1);
+/*
+ * This file is part of "irstea/api-metadata".
+ *
+ * Copyright (C) 2019 IRSTEA
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License and the GNU
+ * Lesser General Public License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+namespace Irstea\ApiMetadata\Model\Identity;
+
+use Assert\Assertion;
+
+/**
+ * Class ResourceIdentity.
+ */
+final class ResourceIdentity implements ResourceIdentityInterface
+{
+    /**
+     * @var string
+     */
+    private $shortName;
+    /**
+     * @var string
+     */
+    private $class;
+
+    /**
+     * ResourceIdentifiers constructor.
+     *
+     * @param string $class
+     * @param string $shortName
+     */
+    private function __construct(string $class, string $shortName)
+    {
+        Assertion::classExists($class);
+
+        $this->shortName = $shortName;
+        $this->class = $class;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getClass(): string
+    {
+        return $this->class;
+    }
+
+    /**
+     * Get shortName.
+     *
+     * @return string
+     */
+    public function getShortName(): string
+    {
+        return $this->shortName;
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString(): string
+    {
+        return $this->shortName;
+    }
+
+    /**
+     * @param string $class
+     * @param string $shortName
+     *
+     * @return self
+     */
+    public static function fromValues(string $class, string $shortName): self
+    {
+        return new self($class, $shortName);
+    }
+
+    /**
+     * @param ResourceIdentityInterface $resource
+     *
+     * @return self
+     */
+    public static function fromResource(ResourceIdentityInterface $resource): self
+    {
+        if ($resource instanceof self) {
+            return $resource;
+        }
+
+        return new self($resource->getClass(), $resource->getShortName());
+    }
+}
diff --git a/src/Model/Identity/ResourceIdentityInterface.php b/src/Model/Identity/ResourceIdentityInterface.php
new file mode 100644
index 0000000..4aa749c
--- /dev/null
+++ b/src/Model/Identity/ResourceIdentityInterface.php
@@ -0,0 +1,37 @@
+<?php declare(strict_types=1);
+/*
+ * This file is part of "irstea/api-metadata".
+ *
+ * Copyright (C) 2019 IRSTEA
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License and the GNU
+ * Lesser General Public License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+namespace Irstea\ApiMetadata\Model\Identity;
+
+/**
+ * Interface ResourceIdentityInterface.
+ */
+interface ResourceIdentityInterface
+{
+    /**
+     * @return string
+     */
+    public function getClass(): string;
+
+    /**
+     * @return string
+     */
+    public function getShortName(): string;
+}
diff --git a/src/Model/MapMetadata.php b/src/Model/MapMetadata.php
index 2aea3f1..83da68c 100644
--- a/src/Model/MapMetadata.php
+++ b/src/Model/MapMetadata.php
@@ -31,7 +31,6 @@ final class MapMetadata implements TypeMetadata
     /**
      * MapMetadata constructor.
      *
-     * @param TypeMetadata $key
      * @param TypeMetadata $valueType
      */
     public function __construct(TypeMetadata $valueType)
diff --git a/src/Model/OperationMetadata.php b/src/Model/OperationMetadata.php
index e060c54..b299333 100644
--- a/src/Model/OperationMetadata.php
+++ b/src/Model/OperationMetadata.php
@@ -20,19 +20,15 @@
 
 namespace Irstea\ApiMetadata\Model;
 
+use Irstea\ApiMetadata\Model\Identity\OperationIdentityInterface;
+
 /**
  * Class OperationMetadata.
  */
-final class OperationMetadata
+final class OperationMetadata implements OperationIdentityInterface
 {
-    /** @var string */
-    private $resource;
-
-    /** @var string */
-    private $type;
-
-    /** @var string */
-    private $name;
+    /** @var OperationIdentityInterface */
+    private $id;
 
     /** @var string */
     private $path;
@@ -49,19 +45,15 @@ final class OperationMetadata
     /**
      * OperationMetadata constructor.
      *
-     * @param string            $resource
-     * @param string            $type
-     * @param string            $name
-     * @param string            $path
-     * @param string            $method
-     * @param TypeMetadata|null $input
-     * @param TypeMetadata|null $output
+     * @param OperationIdentityInterface $id
+     * @param string                     $path
+     * @param string                     $method
+     * @param TypeMetadata|null          $input
+     * @param TypeMetadata|null          $output
      */
-    public function __construct(string $resource, string $type, string $name, string $path, string $method, ?TypeMetadata $input, ?TypeMetadata $output)
+    public function __construct(OperationIdentityInterface $id, string $path, string $method, ?TypeMetadata $input, ?TypeMetadata $output)
     {
-        $this->resource = $resource;
-        $this->type = $type;
-        $this->name = $name;
+        $this->id = $id;
         $this->path = $path;
         $this->method = $method;
         $this->input = $input;
@@ -69,41 +61,35 @@ final class OperationMetadata
     }
 
     /**
-     * @return string
+     * {@inheritdoc}
      */
-    public function getKey(): string
+    public function getShortName(): string
     {
-        return $this->type . '_' . $this->name;
+        return $this->id->getShortName();
     }
 
     /**
-     * Get resource.
-     *
-     * @return string
+     * {@inheritdoc}
      */
-    public function getResource(): string
+    public function getClass(): string
     {
-        return $this->resource;
+        return $this->id->getClass();
     }
 
     /**
-     * Get type.
-     *
-     * @return string
+     * {@inheritdoc}
      */
     public function getType(): string
     {
-        return $this->type;
+        return $this->id->getType();
     }
 
     /**
-     * Get name.
-     *
-     * @return string
+     * {@inheritdoc}
      */
     public function getName(): string
     {
-        return $this->name;
+        return $this->id->getName();
     }
 
     /**
diff --git a/src/Model/ResourceMetadata.php b/src/Model/ResourceMetadata.php
index bccb23a..28f5285 100644
--- a/src/Model/ResourceMetadata.php
+++ b/src/Model/ResourceMetadata.php
@@ -21,53 +21,50 @@
 namespace Irstea\ApiMetadata\Model;
 
 use Assert\Assertion;
+use Irstea\ApiMetadata\Model\Identity\OperationIdentityInterface;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
 
 /**
  * Class ResourceMetadata.
  */
-final class ResourceMetadata extends ObjectMetadata
+final class ResourceMetadata extends ObjectMetadata implements ResourceIdentityInterface
 {
-    /**
-     * @var string
-     */
-    private $shortName;
+    /** @var ResourceIdentityInterface */
+    private $id;
 
-    /** @var array<OperationMetadata> */
+    /** @var array<OperationIdentityInterface> */
     private $operations = [];
 
     /**
      * ResourceMetadata constructor.
      *
-     * @param string              $class
-     * @param string              $shortName
-     * @param ObjectMetadata|null $parent
-     * @param array               $properties
-     * @param OperationMetadata[] $operations
+     * @param ResourceIdentityInterface    $id
+     * @param ObjectMetadata|null          $parent
+     * @param array                        $properties
+     * @param OperationIdentityInterface[] $operations
      */
-    public function __construct(string $class, string $shortName, ?ObjectMetadata $parent, array $properties, array $operations)
+    public function __construct(ResourceIdentityInterface $id, ?ObjectMetadata $parent, array $properties, array $operations)
     {
-        Assertion::allIsInstanceOf($operations, OperationMetadata::class);
+        Assertion::allIsInstanceOf($operations, OperationIdentityInterface::class);
 
-        parent::__construct($class, $parent, $properties);
+        parent::__construct($id->getClass(), $parent, $properties);
 
-        $this->shortName = $shortName;
+        $this->id = $id;
         $this->operations = $operations;
     }
 
     /**
-     * Get shortName.
-     *
-     * @return string
+     * {@inheritdoc}
      */
     public function getShortName(): string
     {
-        return $this->shortName;
+        return $this->id->getShortName();
     }
 
     /**
      * Get operations.
      *
-     * @return OperationMetadata[]
+     * @return OperationIdentityInterface[]
      */
     public function getOperations(): array
     {
@@ -79,6 +76,6 @@ final class ResourceMetadata extends ObjectMetadata
      */
     public function getTypeAttributes(): array
     {
-        return ['shortName' => $this->shortName] + parent::getTypeAttributes();
+        return ['shortName' => $this->getShortName()] + parent::getTypeAttributes();
     }
 }
diff --git a/src/Service/ResourceMap.php b/src/Service/ResourceMap.php
index 3de4539..54cba62 100644
--- a/src/Service/ResourceMap.php
+++ b/src/Service/ResourceMap.php
@@ -20,18 +20,25 @@
 
 namespace Irstea\ApiMetadata\Service;
 
+use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
 use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
 use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ArrayIterator;
+use Assert\Assertion;
+use Irstea\ApiMetadata\Exception\ResourceNotFoundException;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentity;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
+use Iterator;
 
 /**
  * Class ResourceMap.
  */
 class ResourceMap implements ResourceMapInterface
 {
-    /** @var array<string,string>|null */
+    /** @var array<string, ResourceIdentityInterface>|null */
     private $resourceMap = null;
 
-    /** @var array<string,string>|null */
+    /** @var array<string, ResourceIdentityInterface>|null */
     private $reverseMap = null;
 
     /** @var ResourceMetadataFactoryInterface */
@@ -55,13 +62,17 @@ class ResourceMap implements ResourceMapInterface
     }
 
     /**
-     * {@inheritdoc}
+     * @return Iterator<string, ResourceIdentityInterface>
      */
-    public function getIterator(): \Iterator
+    public function getIterator(): Iterator
     {
         $this->init();
+        Assertion::notNull($this->resourceMap);
+
+        /** @var Iterator<string, ResourceIdentityInterface> $iter */
+        $iter = new ArrayIterator($this->resourceMap);
 
-        return new \ArrayIterator($this->resourceMap);
+        return $iter;
     }
 
     /**
@@ -77,37 +88,47 @@ class ResourceMap implements ResourceMapInterface
     /**
      * {@inheritdoc}
      */
-    public function getShortName(string $className): ?string
+    public function isResourceName(string $shortName): bool
     {
         $this->init();
 
-        return $this->resourceMap[$className] ?? null;
+        return isset($this->reverseMap[$shortName]);
     }
 
     /**
      * {@inheritdoc}
      */
-    public function isResourceName(string $shortName): bool
+    public function getByClassName(string $className): ResourceIdentityInterface
     {
         $this->init();
 
-        return isset($this->reverseMap[$shortName]);
+        $resourceId = $this->resourceMap[$className] ?? null;
+        if ($resourceId === null) {
+            throw new ResourceNotFoundException("No ressource found for class $className");
+        }
+
+        return $resourceId;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function getClassName(string $shortName): ?string
+    public function getByShortName(string $shortName): ResourceIdentityInterface
     {
         $this->init();
 
-        return $this->reverseMap[$shortName] ?? null;
+        $resourceId = $this->reverseMap[$shortName] ?? null;
+        if ($resourceId === null) {
+            throw new ResourceNotFoundException("No ressource found for name $shortName");
+        }
+
+        return $resourceId;
     }
 
     /**
      * Initialise les maps de correspondance shortName <=> className.
      *
-     * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException
+     * @throws ResourceClassNotFoundException
      */
     private function init(): void
     {
@@ -119,9 +140,11 @@ class ResourceMap implements ResourceMapInterface
 
         foreach ($this->nameFactory->create() as $className) {
             $metadata = $this->metadataFactory->create($className);
+
             $shortName = $metadata->getShortName();
-            $this->resourceMap[$className] = $shortName;
-            $this->reverseMap[$shortName] = $className;
+            Assertion::notNull($shortName);
+
+            $this->resourceMap[$className] = $this->reverseMap[$shortName] = ResourceIdentity::fromValues($className, $shortName);
         }
     }
 }
diff --git a/src/Service/ResourceMapInterface.php b/src/Service/ResourceMapInterface.php
index 2fdaeb5..ed4267d 100644
--- a/src/Service/ResourceMapInterface.php
+++ b/src/Service/ResourceMapInterface.php
@@ -20,6 +20,9 @@
 
 namespace Irstea\ApiMetadata\Service;
 
+use Irstea\ApiMetadata\Exception\ResourceNotFoundException;
+use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
+
 /**
  * Interface ResourceMapInterface.
  */
@@ -40,16 +43,20 @@ interface ResourceMapInterface extends \IteratorAggregate
     public function isResourceName(string $shortName): bool;
 
     /**
-     * @param string $shortName
+     * @param string $className
+     *
+     * @throws ResourceNotFoundException
      *
-     * @return string|null
+     * @return ResourceIdentityInterface
      */
-    public function getClassName(string $shortName): ?string;
+    public function getByClassName(string $className): ResourceIdentityInterface;
 
     /**
-     * @param string $className
+     * @param string $shortName
+     *
+     * @throws ResourceNotFoundException
      *
-     * @return string|null
+     * @return ResourceIdentityInterface
      */
-    public function getShortName(string $className): ?string;
+    public function getByShortName(string $shortName): ResourceIdentityInterface;
 }
diff --git a/src/URI/CompositeURIGenerator.php b/src/URI/CompositeURIGenerator.php
index 998ec5e..54ec214 100644
--- a/src/URI/CompositeURIGenerator.php
+++ b/src/URI/CompositeURIGenerator.php
@@ -28,14 +28,14 @@ use Assert\Assertion;
 class CompositeURIGenerator implements URIGeneratorInterface
 {
     /**
-     * @var URIGeneratorInterface[]
+     * @var iterable<URIGeneratorInterface>
      */
     private $generators;
 
     /**
      * CompositeURIGenerator constructor.
      *
-     * @param URIGeneratorInterface[] $generators
+     * @param iterable<URIGeneratorInterface> $generators
      */
     public function __construct(iterable $generators)
     {
diff --git a/src/URI/URIGeneratorInterface.php b/src/URI/URIGeneratorInterface.php
index f67de02..3764c0c 100644
--- a/src/URI/URIGeneratorInterface.php
+++ b/src/URI/URIGeneratorInterface.php
@@ -26,14 +26,14 @@ namespace Irstea\ApiMetadata\URI;
 interface URIGeneratorInterface
 {
     /**
-     * @param $resource
+     * @param mixed $resource
      *
      * @return string
      */
     public function generateURI($resource): string;
 
     /**
-     * @param $resource
+     * @param mixed $resource
      *
      * @return bool
      */
diff --git a/tests/Fixtures/Entity/Address.php b/tests/Fixtures/Entity/Address.php
index fc462fb..f70e893 100644
--- a/tests/Fixtures/Entity/Address.php
+++ b/tests/Fixtures/Entity/Address.php
@@ -138,20 +138,4 @@ class Address
     {
         $this->country = $country;
     }
-
-    /**
-     * @param mixed $other
-     *
-     * @return bool
-     *
-     * @API\ApiProperty(required=false)
-     */
-    public function isEqualTo($other): bool
-    {
-        return $other instanceof self
-            && $other->street === $this->street
-            && $other->postalCode === $this->postalCode
-            && $other->city === $this->city
-            && $other->country === $this->country;
-    }
 }
diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php
index d126c70..7684ac3 100644
--- a/tests/FunctionalTest.php
+++ b/tests/FunctionalTest.php
@@ -109,8 +109,8 @@ class FunctionalTest extends WebTestCase
             [
                 '$id'       => 'http://localhost/metadata',
                 'resources' => [
-                    'User'   => ['$ref' => 'User'],
-                    'Person' => ['$ref' => 'Person'],
+                    'User'   => ['$ref' => 'http://localhost/metadata/User#'],
+                    'Person' => ['$ref' => 'http://localhost/metadata/Person#'],
                 ],
             ],
             $collection
diff --git a/tests/Service/ResourceMapTest.php b/tests/Service/ResourceMapTest.php
index 2668f31..4776e84 100644
--- a/tests/Service/ResourceMapTest.php
+++ b/tests/Service/ResourceMapTest.php
@@ -34,6 +34,7 @@ use Prophecy\Prophecy\ObjectProphecy;
  * Class ResourceMapTest.
  *
  * @covers \Irstea\ApiMetadata\Service\ResourceMap
+ * @covers \Irstea\ApiMetadata\Model\Identity\ResourceIdentity
  */
 class ResourceMapTest extends TestCase
 {
@@ -46,11 +47,11 @@ class ResourceMapTest extends TestCase
     protected function setUp()
     {
         $this->resources = [
-            '\\App\\Foo' => new ResourceMetadata('Foo'),
-            '\\App\\Bar' => new ResourceMetadata('Bar'),
+            \stdClass::class   => new ResourceMetadata('stdClass'),
+            ResourceMap::class => new ResourceMetadata('ResourceMap'),
         ];
 
-        /** @var ResourceNameCollectionFactoryInterface|ObjectProphecy $nameFactoryProph */
+        /** @var ObjectProphecy<ResourceNameCollectionFactoryInterface> $nameFactoryProph */
         $nameFactoryProph = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
 
         /** @var MethodProphecy $proph */
@@ -59,7 +60,7 @@ class ResourceMapTest extends TestCase
             new ResourceNameCollection(array_keys($this->resources))
         );
 
-        /** @var ResourceMetadataFactoryInterface|ObjectProphecy $metadataFactoryProph */
+        /** @var ObjectProphecy<ResourceMetadataFactoryInterface> $metadataFactoryProph */
         $metadataFactoryProph = $this->prophesize(ResourceMetadataFactoryInterface::class);
 
         /** @var string $stringArg */
@@ -80,25 +81,27 @@ class ResourceMapTest extends TestCase
         $this->map = new ResourceMap($nameFactory, $metadataFactory);
     }
 
-    public function testGetClassName()
+    public function testGetByShortName()
     {
-        self::assertEquals('\\App\\Foo', $this->map->getClassName('Foo'));
+        $resourceId = $this->map->getByShortName('stdClass');
+        self::assertEquals(\stdClass::class, $resourceId->getClass());
     }
 
-    public function testGetShortName()
+    public function testGetByClassName()
     {
-        self::assertEquals('Foo', $this->map->getShortName('\\App\\Foo'));
+        $resourceId = $this->map->getByClassName(\stdClass::class);
+        self::assertEquals(\stdClass::class, $resourceId->getClass());
     }
 
     public function testIsResourceClass()
     {
-        self::assertTrue($this->map->isResourceClass('\\App\\Foo'));
-        self::assertFalse($this->map->isResourceClass(\stdClass::class));
+        self::assertTrue($this->map->isResourceClass(ResourceMap::class));
+        self::assertFalse($this->map->isResourceClass(__CLASS__));
     }
 
     public function testIsResourceName()
     {
-        self::assertTrue($this->map->isResourceName('Foo'));
-        self::assertFalse($this->map->isResourceName('Baz'));
+        self::assertTrue($this->map->isResourceName('ResourceMap'));
+        self::assertFalse($this->map->isResourceName('ResourceMapTest'));
     }
 }
diff --git a/tests/TestRemoteRefProvider.php b/tests/TestRemoteRefProvider.php
index 4d353f0..90a3b6b 100644
--- a/tests/TestRemoteRefProvider.php
+++ b/tests/TestRemoteRefProvider.php
@@ -29,9 +29,7 @@ use Symfony\Component\HttpFoundation\Response;
  */
 class TestRemoteRefProvider implements RemoteRefProvider
 {
-    /**
-     * @var
-     */
+    /** @var Client */
     private $client;
 
     /**
-- 
GitLab