When api-platform try to serialize my entity, I get the Resource "Ramsey\Uuid\Uuid" not found. error. It seems to only happen on deep serialization. In the following stack trace, I'm exposing AppBundle\Entity\Theme entities which have a bidirectional ManyToOne to a AppBundle\Entity\Category.
Also, I tried to expose my AppBundle\Entity\User entities which use the same identifier configuration and serialization works well when using application/json format but not application/ld+json format.
Stack trace:
[1] ApiPlatform\Core\Exception\ResourceClassNotFoundException: Resource "Ramsey\Uuid\Uuid" not found.
at n/a
in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php line 72
at ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceMetadataFactory->handleNotFound(null, 'Ramsey\\Uuid\\Uuid')
in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php line 50
at ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/ShortNameResourceMetadataFactory.php line 35
at ApiPlatform\Core\Metadata\Resource\Factory\ShortNameResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/OperationResourceMetadataFactory.php line 35
at ApiPlatform\Core\Metadata\Resource\Factory\OperationResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/CachedResourceMetadataFactory.php line 53
at ApiPlatform\Core\Metadata\Resource\Factory\CachedResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
in /var/www/vendor/api-platform/core/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php line 156
at ApiPlatform\Core\Metadata\Property\Factory\SerializerPropertyMetadataFactory->getEffectiveSerializerGroups(array(), 'Ramsey\\Uuid\\Uuid')
in /var/www/vendor/api-platform/core/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php line 51
at ApiPlatform\Core\Metadata\Property\Factory\SerializerPropertyMetadataFactory->create('Ramsey\\Uuid\\Uuid', 'bytes', array())
in /var/www/vendor/api-platform/core/src/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php line 48
at ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory->create('Ramsey\\Uuid\\Uuid', 'bytes', array())
in /var/www/vendor/api-platform/core/src/Metadata/Property/Factory/CachedPropertyMetadataFactory.php line 53
at ApiPlatform\Core\Metadata\Property\Factory\CachedPropertyMetadataFactory->create('Ramsey\\Uuid\\Uuid', 'bytes')
in /var/www/vendor/api-platform/core/src/Bridge/Symfony/Routing/IriConverter.php line 134
at ApiPlatform\Core\Bridge\Symfony\Routing\IriConverter->getIdentifiersFromItem(object(Category))
in /var/www/vendor/api-platform/core/src/Bridge/Symfony/Routing/IriConverter.php line 83
at ApiPlatform\Core\Bridge\Symfony\Routing\IriConverter->getIriFromItem(object(Category))
in /var/www/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 420
at ApiPlatform\Core\Serializer\AbstractItemNormalizer->normalizeRelation(object(PropertyMetadata), object(Category), 'AppBundle\\Entity\\Category', 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true, 'cache_key' => '9db9240ee742c4b9577d52a00843bee3', 'circular_reference_limit' => array('00000000040ec8b30000000028ca7688' => 1)))
in /var/www/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 397
at ApiPlatform\Core\Serializer\AbstractItemNormalizer->getAttributeValue(object(Theme), 'categorie', 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true, 'cache_key' => '9db9240ee742c4b9577d52a00843bee3', 'circular_reference_limit' => array('00000000040ec8b30000000028ca7688' => 1)))
in /var/www/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php line 78
at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(Theme), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true, 'cache_key' => '9db9240ee742c4b9577d52a00843bee3', 'circular_reference_limit' => array('00000000040ec8b30000000028ca7688' => 1)))
in /var/www/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 84
at ApiPlatform\Core\Serializer\AbstractItemNormalizer->normalize(object(Theme), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true))
in /var/www/vendor/symfony/serializer/Serializer.php line 142
at Symfony\Component\Serializer\Serializer->normalize(object(Theme), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json'))
in /var/www/vendor/symfony/serializer/Serializer.php line 152
at Symfony\Component\Serializer\Serializer->normalize(object(Paginator), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json'))
in /var/www/vendor/symfony/serializer/Serializer.php line 115
at Symfony\Component\Serializer\Serializer->serialize(object(Paginator), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json'))
in /var/www/vendor/api-platform/core/src/EventListener/SerializeListener.php line 64
at ApiPlatform\Core\EventListener\SerializeListener->onKernelView(object(GetResponseForControllerResultEvent), 'kernel.view', object(TraceableEventDispatcher))
in line
at call_user_func(array(object(SerializeListener), 'onKernelView'), object(GetResponseForControllerResultEvent), 'kernel.view', object(TraceableEventDispatcher))
in /var/www/vendor/symfony/event-dispatcher/Debug/WrappedListener.php line 106
at Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(object(GetResponseForControllerResultEvent), 'kernel.view', object(ContainerAwareEventDispatcher))
in line
at call_user_func(object(WrappedListener), object(GetResponseForControllerResultEvent), 'kernel.view', object(ContainerAwareEventDispatcher))
in /var/www/vendor/symfony/event-dispatcher/EventDispatcher.php line 174
at Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(array(object(WrappedListener), array(object(ValidateListener), 'onKernelView'), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.view', object(GetResponseForControllerResultEvent))
in /var/www/vendor/symfony/event-dispatcher/EventDispatcher.php line 43
at Symfony\Component\EventDispatcher\EventDispatcher->dispatch('kernel.view', object(GetResponseForControllerResultEvent))
in /var/www/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php line 136
at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch('kernel.view', object(GetResponseForControllerResultEvent))
in /var/www/vendor/symfony/http-kernel/HttpKernel.php line 158
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
in /var/www/vendor/symfony/http-kernel/HttpKernel.php line 68
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
in /var/www/vendor/symfony/http-kernel/Kernel.php line 168
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
in /var/www/web/app_dev.php line 13
Identifier configuration:
/**
* @ORM\Id
* @ORM\Column(type="uuid")
* @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
*
* @var Uuid
*/
protected $id;
public function getId()
{
return $this->id;
}
public function setId(Uuid $id)
{
$this->id = $id;
}
@fbourigault did you try to persist such entity manually? Ramsey\Uuid\Uuid is an object, so unless you have the appropriate type for it (which you either do yourself it's relatively simple or there might be a lib for it) Doctrine has no idea on how to persist it, cf. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/custom-mapping-types.html
I use https://github.com/ramsey/uuid-doctrine to get an uuid type for doctrine. I use this library with a few projects, I never had issues with serialization but it's the first time I use Ramsey\Uuid\Uuid with api-platform. I also have a normalizer and a normalizer in my application. From the stack trace, I see that api-plaform tries to get an IRI because an object is encountered. But shouldn't this object have been normalized as a string before. By saying it I really need to check normalizers priorities! Stay tuned :)
EDIT: Normalizer priorities does not change anything.
This is related to #916.
@api-platform/core-team I think we need to make fixing this a priority.
I start working on that.
This can be closed @fbourigault @dunglas ?
@fbourigault Did you find a fix? Can you share how please?
I use string instead. See https://github.com/api-platform/core/pull/927#issuecomment-276894661.
@fbourigault OK I've found a solution.
According to your comment, it's not recommended to use objects as identifiers. Now I use a string identifier, but still use Ramsey as generator (overridden):
// src/AppBundle/Entity/Foo.php
class Foo
{
/**
* @var string
*
* @ORM\Id
* @ORM\Column(type="string")
* @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\CustomIdGenerator(class="AppBundle\Doctrine\Generator\UuidGenerator")
*/
private $id;
And the UuidGenerator overridden:
// src/AppBundle/Doctrine/Generator/UuidGenerator.php
namespace AppBundle\Doctrine\Generator;
use Doctrine\ORM\EntityManager;
use Ramsey\Uuid\Doctrine\UuidGenerator as BaseUuidGenerator;
use Ramsey\Uuid\Uuid;
class UuidGenerator extends BaseUuidGenerator
{
/**
* {@inheritdoc}
*/
public function generate(EntityManager $em, $entity)
{
/** @var Uuid $uuid */
$uuid = parent::generate($em, $entity);
return $uuid->toString();
}
}
Hope this solution could help if other people have this issue 馃槂
@vincentchalamon could this be added to the original library? You're basically just casting the UUID object to a string. Is there an upside of generate() returning an object instead?
/cc @ramsey (sorry for the mention, but you are the domain expert here)
The Uuid class allows more operations than a regular string (see https://github.com/ramsey/uuid/blob/master/src/Uuid.php). There is no reason to return a string and lose those features.
@vincentchalamon's solution is simple enough. I would just use composition instead of heritage in the custom generator.
@dunglas of course the UUID object has more features than a string. :)
I meant, is it valuable for it to return the object in this context? If using it as an ID, you're most often using it as an opaque value, you're not really interested in the content, same as auto-increments. Having it be an object is nice, but you run into all sorts of trouble, like this one or, for example, not being able to use the stock Symfony serializer on an entity (because the serializer will trigger an exception in getDateTime() which is not available with UUIDv4 used here).
If these are some of quite severe downsides, what are the upsides? For example, if you want to access UUID features, you can always toString() it back. I guess, UUID validation?
None of this is really related to this repo (sorry about that), but the interaction between these libs is important, why not make it as simple as possible.
How does the stock Symfony serializer work? The Uuid object implements JsonSerializable and Serializable, so if Symfony is using either of these methods to serialize/unserialize data, everything should Just Work.
See: https://github.com/ramsey/uuid/blob/master/src/Uuid.php#L178-L214
That's weird. I don't think it's calling it.
I've setup the serializer manually (because #1091), as shown in the docs:
$normalizers = [new \Symfony\Component\Serializer\Normalizer\ObjectNormalizer()];
$encoders = [new \Symfony\Component\Serializer\Encoder\JsonEncode()];
$this->serializer = new \Symfony\Component\Serializer\Serializer($normalizers, $encoders);
If I $body = $this->serializer->serialize($entity, 'json'); (entity is a random Doctrine entity properly using RamseyUuid as PK), I get:
[1] Ramsey\Uuid\Exception\UnsupportedOperationException: Not a time-based UUID
at n/a
in /app/vendor/ramsey/uuid/src/Uuid.php line 313
at Ramsey\Uuid\Uuid->getDateTime()
in /app/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 483
at Symfony\Component\PropertyAccess\PropertyAccessor->readProperty(array(object(Uuid)), 'dateTime')
in /app/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 406
at Symfony\Component\PropertyAccess\PropertyAccessor->readPropertiesUntil(array(object(Uuid)), object(PropertyPath), 1, true)
in /app/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 178
at Symfony\Component\PropertyAccess\PropertyAccessor->getValue(object(Uuid), object(PropertyPath))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php line 93
at Symfony\Component\Serializer\Normalizer\ObjectNormalizer->getAttributeValue(object(Uuid), 'dateTime', 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1, '000000004ee02dca0000000070ad8799' => 1)))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 79
at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(Uuid), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1, '000000004ee02dca0000000070ad8799' => 1)))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 142
at Symfony\Component\Serializer\Serializer->normalize(object(Uuid), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1)))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 97
at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(User), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1)))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 142
at Symfony\Component\Serializer\Serializer->normalize(object(User), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1)))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 97
at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(OauthAccessToken), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1)))
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 142
at Symfony\Component\Serializer\Serializer->normalize(object(OauthAccessToken), 'json', array())
in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 115
at Symfony\Component\Serializer\Serializer->serialize(object(OauthAccessToken), 'json')
in /app/src/AppBundle/Queue/EventConverter/DoctrineEventConverter.php line 146
...etc.
Yeah, I expected the ObjectNormalizer to figure out it needs to use JsonSerializableNormalizer, it doesn't. Never mind, PEBKAC.
The point about returning a string instead of object from generate() by default still stand, though.
We can add __toString support in getIdentifiersFromItem.
@teohhanhui @dunglas
Maybe it's too early to ask but are there any news for this issue? Yesterday I encountered very similar problem - I have an object identifier
final class MessageId extends Identity
{
// parent Identity has constructor accepting id as a string
public static function generate(): MessageId
{
return new self((string)Uuid::uuid4());
}
}
The stacktrace is almost identical to the one sent by @fbourigault but the exception message says that Resource of ...\MessageId not found.
Thanks.
@eps90 Does MessageId implement __toString? If yes, then my proposed solution above (yet to be implemented) should fix it.
@teohhanhui Yes it does. Great, I'll be waiting then!
@teohhanhui is this what you had in mind?
@@ -126,6 +126,9 @@ final class IriConverter implements IriConverterInterface
if (!is_object($identifiers[$propertyName])) {
continue;
+ } else if (method_exists($identifiers[$propertyName], '__toString')) {
+ $identifiers[$propertyName] = (string) $identifiers[$propertyName];
+ continue;
}
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
Most helpful comment
@fbourigault OK I've found a solution.
According to your comment, it's not recommended to use objects as identifiers. Now I use a string identifier, but still use Ramsey as generator (overridden):
And the UuidGenerator overridden:
Hope this solution could help if other people have this issue 馃槂