New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Not able to send ID in POST method #343
Comments
What do you mean ? If you do
And the setters exists (and the good groups if you use any), that should work. |
Yes this, but this does not work, the result is: |
Could you please paste the entity you are using ? |
Problem is, when in |
Any idea? |
From me, no, i am leaving this bundle and going back to other solution |
It's weird to have a setter for an ID that is generated externally. I'm not sure to get what you try to achieve. |
@dunglas it is normal to send ID from app. I case you have app with optimistic ui and all ID's are in UUID format, you could generate ID in you app and send it to api endpoint and you don't have to wait for positive response. |
@akadlec I agree on that, but in this case you shouldn't have this line |
Right now i'm not 100% surre but i thing i did a test where i remove this row. Problem is not in doctrine mapping but in apiplatform. There is a method which check request and when an ID is present in request and request is in POST method, platform refuse it |
This issue still exists for me and I only get the error message "Update is not allowed for this operation" if actually post an id that has the property name "id". For other entities that have an id on the property "code" for example I don't get this error message. So this is pretty weird. Should this check not rely on that a IdGenerator is actually defined in Doctrine for that entity? |
Any news on this? Or ideas how to prepare a merge request to add support for this? |
+1 |
This issue still exists. I'm trying to fix it but I'm pretty lost and it's hard to fix it without guidance as I'm not sure why things are the way they are at several places in the code.
|
Original issue was #132 so cc'ing @meyerbaptiste here too. |
That whole check does not make much sense to me. I mean, what if my identifier was named |
hello |
See reasoning in api-platform/core#2022 (comment). You can always decorate the ItemNormalizer to implement your own logic. |
Seriously there is no way to [POST] an object with a specified |
no, we had to implement it ourselves (see my comment) |
I tried your solution but it leads to this error: btw thanks for the code |
This is the full code of our class that allows positing custom <?php
declare(strict_types=1);
namespace App\Serializer\Normalizer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* This class overrides api-platform's built in ItemNormalizer in order to make it possible to POST resources
* with custom provided ID
*
* Related not merged PR and discussion: https://github.com/api-platform/core/pull/2022
*/
class ItemNormalizer extends AbstractItemNormalizer
{
private const IDENTIFIER = 'id';
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory);
$this->logger = $logger ?: new NullLogger();
}
/**
* @param mixed $data
* @param string $class
* @param string $format
* @param array $context
*
* @return object
*/
public function denormalize($data, $class, $format = null, array $context = [])
{
$context['api_denormalize'] = true;
if (!isset($context['resource_class'])) {
$context['resource_class'] = $class;
}
$this->setObjectToPopulate($data, $context);
return parent::denormalize($data, $class, $format, $context);
}
/**
* @param string|object $classOrObject
* @param array $context
* @param bool $attributesAsString
*
* @return array|bool|string[]|\Symfony\Component\Serializer\Mapping\AttributeMetadataInterface[]
*/
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
{
$allowedAttributes = parent::getAllowedAttributes(
$classOrObject,
$context,
$attributesAsString
);
if (\array_key_exists('allowed_extra_attributes', $context)) {
$allowedAttributes = array_merge($allowedAttributes, $context['allowed_extra_attributes']);
}
return $allowedAttributes;
}
/**
* @param mixed $data
* @param array $context
*/
protected function setObjectToPopulate($data, array &$context): void
{
// in PUT request OBJECT_TO_POPULATE is already set by this moment
if (!\is_array($data) || isset($context[self::OBJECT_TO_POPULATE])) {
return;
}
[$identifierName, $identifierMetadata] = $this->getResourceIdentifierData($context);
$isUpdateAllowed = (bool) ($context['api_allow_update'] ?? false);
$hasIdentifierInRequest = \array_key_exists(self::IDENTIFIER, $data);
$hasWritableIdentifierInRequest = $hasIdentifierInRequest && $identifierMetadata->isWritable();
// when it is POST, update is not allowed for top level resource, but is allowed for nested resources
$isTopLevelResourceInPostRequest = !$isUpdateAllowed
&& $context['operation_type'] === 'collection'
&& $context['collection_operation_name'] === 'post';
// if Resource does not have an ID OR if it is writable custom id - we should not populate Entity from DB
if (!$hasIdentifierInRequest || ($hasWritableIdentifierInRequest && $isTopLevelResourceInPostRequest)) {
return;
}
if (!$isUpdateAllowed) {
throw new InvalidArgumentException('Update is not allowed for this operation.');
}
try {
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri(
(string) $data[self::IDENTIFIER],
$context + ['fetch_data' => true]
);
} catch (InvalidArgumentException $e) {
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri(
sprintf(
'%s/%s',
$this->iriConverter->getIriFromResourceClass($context['resource_class']),
$data[$identifierName]
),
$context + ['fetch_data' => true]
);
}
}
private function getResourceIdentifierData(array $context): array
{
$identifierPropertyName = null;
$identifierPropertyMetadata = null;
$className = $context['resource_class'];
$properties = $this->propertyNameCollectionFactory->create($className, $context);
foreach ($properties as $propertyName) {
$property = $this->propertyMetadataFactory->create($className, $propertyName);
if ($property->isIdentifier()) {
$identifierPropertyName = $propertyName;
$identifierPropertyMetadata = $property;
break;
}
}
if ($identifierPropertyMetadata === null) {
throw new \LogicException(
sprintf(
'Resource "%s" must have an identifier. Properties: %s.',
$className,
implode(',', iterator_to_array($properties->getIterator()))
)
);
}
return [$identifierPropertyName, $identifierPropertyMetadata];
}
} |
With the full code on api-platform version 2.4.6, it works for simple entities. But if there are nested entities, it does not work. Thanks for the code and the time. |
I have found out that posting with the |
Thanks. We added a slight modification to allow for nested id references, which we automatically convert to iris. Here is our adapted version: |
Same here with a custom POST action, using a custom input DTO object. |
Thanks @maks-rafalko for the code it works perfectly in a simple entities |
Simplest solution : creating two decorators for Json & JsonLd item normalizers. Code snippetNormalizer for JsonLD requests : <?php
declare(strict_types=1);
namespace App\Serializer;
use ApiPlatform\Metadata\Post;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Custom ItemNormalizer class.
*/
#[AsDecorator('api_platform.jsonld.normalizer.item', priority: -900)]
class ItemJsonLdNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
public function __construct(
#[AutowireDecorated] private readonly NormalizerInterface $decorated,
) {}
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
return $this->decorated->normalize($object, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $this->decorated->supportsDenormalization($data, $type, $format, $context);
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
// When denormalizing some API Platform resource,
// we want to allow clients to send the resource identifier in POST operations.
// By default, it's only allowed on PUT/PATCH operations.
if ($context['operation'] instanceof Post) {
$context['api_allow_update'] = true;
}
return $this->decorated->denormalize($data, $type, $format, $context);
}
public function hasCacheableSupportsMethod(): bool
{
return $this->decorated->hasCacheableSupportsMethod();
}
public function supportsNormalization(mixed $data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
public function setSerializer(SerializerInterface $serializer)
{
return $this->decorated->setSerializer($serializer);
}
} Normalizer for Json requests : <?php
declare(strict_types=1);
namespace App\Serializer;
use ApiPlatform\Metadata\Post;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
#[AsDecorator('api_platform.serializer.normalizer.item', priority: -900)]
class ItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
public function __construct(
#[AutowireDecorated] private readonly NormalizerInterface $decorated,
) {}
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
return $this->decorated->normalize($object, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $this->decorated->supportsDenormalization($data, $type, $format, $context);
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
// When denormalizing some API Platform resource,
// we want to allow clients to send the resource identifier in POST operations.
// By default, it's only allowed on PUT/PATCH operations.
if ($context['operation'] instanceof Post) {
$context['api_allow_update'] = true;
}
return $this->decorated->denormalize($data, $type, $format, $context);
}
public function hasCacheableSupportsMethod(): bool
{
return $this->decorated->hasCacheableSupportsMethod();
}
public function supportsNormalization(mixed $data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
public function setSerializer(SerializerInterface $serializer)
{
return $this->decorated->setSerializer($serializer);
}
} |
I would like to send generated ID from UI to API but right now it is not possible. API is trying to do update, response is "Method is not allowed" So right now i'm sending it as uuid and then in the event doing extraction from request setting to entity.
It will be great to allow send ID param to POST and to have "optional" parameter for writing. What do you think?
The text was updated successfully, but these errors were encountered: