I am using API Platform 3.2 with Symfony 6.4 to write a REST API.
Users are entities, and each user has a 1-1 relationship with a Profile entity.
For Profile operations, there are two endpoints:
GET /users/me/profile
: Return the profile of the currently authenticated userPATCH /users/me/profile
: Update the profile of the currently authenticated user
The GET
works fine. I use a custom State Provider to fetch the authenticated user and return their associated Profile.
However, I’m having trouble with the PATCH
. I’m not sure how to get it to update the profile of the authenticated user.
I tried creating a custom State Processor called ProfileStateProcessor
, but by the time it gets to ProfileStateProcessor::process()
, the $data is already a new Profile object. By default it will try to create a new Profile every call.
How can I make PATCH /users/me/profile
select the authenticated user’s profile for updating?
I know I could find their profile in the state processor, merge the data from the request, and save that — but it seems like there has to be a better way.
Relevant code
resources/User/Profile.yaml
AppEntityUserProfile:
normalizationContext:
groups: [ 'profile:read' ]
denormalizationContext:
groups: [ 'profile:write' ]
operations:
ApiPlatformMetadataGet:
uriTemplate: 'users/me/profile'
provider: AppStateUserProfileStateProvider
ApiPlatformMetadataPatch:
uriTemplate: 'users/me/profile'
processor: AppStateUserProfileStateProcessor
src/State/User/ProfileStateProvider.php
<?php
namespace AppStateUser;
use ApiPlatformMetadataOperation;
use ApiPlatformStateProviderInterface;
use SymfonyBundleSecurityBundleSecurity;
class ProfileStateProvider implements ProviderInterface
{
public function __construct(private readonly Security $security)
{
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$user = $this->security->getUser();
$profile = $user->getProfile();
return $profile;
}
}
src/State/User/ProfileStateProcessor.php
This solution works, but requires me to check every writable field.
<?php
namespace AppStateUser;
use ApiPlatformMetadataOperation;
use ApiPlatformStateProcessorInterface;
use AppEntityUserProfile;
use SymfonyBundleSecurityBundleSecurity;
use SymfonyComponentDependencyInjectionAttributeAutowire;
class ProfileStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private ProcessorInterface $persistProcessor,
private Security $security,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
assert($data instanceof Profile);
// Get the actual profile we want to update from security
$profile = $this->security->getUser()->getProfile();
// Compare with $data for every possible update
if ($data->getTimezone() != $profile->getTimezone()) {
$profile->setTimezone($data->getTimezone());
}
// etc...
return $this->persistProcessor->process($profile, $operation, $uriVariables, $context);
}
}