API Platform Serialization issue for custom Output - symfony

I have created this resource in order to request a password reset token:
POST {{api_base_url}}/users/password/reset-request
Payload example
{
"email" :"user#example.com"
}
Everything works fine but the response doesn't serialize ResetPasswordRequestOutput to show the result. I got this response instead (as User type)
{
"#context": {
"#vocab": "http://dev.api.xxxxxxxxxx.com/docs.jsonld#",
"hydra": "http://www.w3.org/ns/hydra/core#",
"token": "ResetPasswordRequestOutput/token"
},
"#type": "User",
"#id": "/users/00000000-0000-0000-0000-000000000002",
"token": "ORGOsGh01sr8R91J"
}
Expected output: (of type: ResetPasswordRequestOutput)
{
"#type": "ResetPasswordRequestOutput",
"token": "ORGOsGh01sr8R91J"
}
Code:
The controller class of the custom operation:
final class ResetPasswordRequest {
private $validator;
private $entityManager;
public function __construct(ValidatorInterface $validator, EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->validator = $validator;
}
public function __invoke(ResetPasswordRequestInput $data)
{
$this->validator->validate($data);
$userRepository = $this->entityManager->getRepository(User::class);
/** #var User $user */
$user = $userRepository->findOneBy(['email' => $data->email]);
if (!$user) {
throw new ItemNotFoundException("invalid token");
}
if ($user->getStatus() !== UserStatus::ACTIVE) {
throw new AccessDeniedException("user is inactive");
}
$generator = (new Factory())->getGenerator(new Strength(Strength::MEDIUM));
$token = $generator->generateString(16, 'BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
$user->setToken($token);
return $user;
}
}
The data transofrmer that converts User object to ResetPasswordRequestOutput
namespace App\DataTransformer;
final class ResetPasswordRequestOutputDataTransformer implements DataTransformerInterface
{
private $normalizer;
public function __construct(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}
/**
* {#inheritdoc}
*/
public function transform($data, string $to, array $context = [])
{
$output = new ResetPasswordRequestOutput();
$output->token = $data->getToken();
return $output;
}
/**
* {#inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return ResetPasswordRequestOutput::class === $to && $data instanceof User;
}
DTO object that holds input data
<?php
namespace App\Dto;
final class ResetPasswordRequestInput
{
/**
* #var string
*
* #Assert\Email
* #Groups({"user:write"})
*/
public $email;
}
DTO object that holds output data
namespace App\Dto;
final class ResetPasswordRequestOutput
{
/**
* #var string
*
* #Assert\NotNull()
* #Groups({"user:read"})
*/
public $token;
}
User Entity class with ApiResource annotation (partial) action=reset_password_request
/**
* #ApiResource(
* normalizationContext={"groups"={"user", "user:read"}},
* denormalizationContext={"groups"={"user", "user:write"}},
* collectionOperations={
* "get",
* "post",
* "reset_password_request" = {
* "method"= "POST",
* "read"=false,
* "path"="/users/password/reset-request",
* "controller"=ResetPasswordRequest::class,
* "status"=200,
* "input"=ResetPasswordRequestInput::class,
* "output"=ResetPasswordRequestOutput::class,
* "normalization_context"={"jsonld_embed_context"=false}
* },

Related

Modify POST data before storing in database with Symfony and API-Platform

I'm fairly new to the Symfony universe, so please bear with me if this question has already been answered.
I have provided endpoints with the api-platform to create a RegistrationRequest. A RegistrationRequest has a user that is connected via a ManyToOne relation, so it is stored in another table. In the API, the user or user_id is read-only, this is why the user can not be set in the post data. If a RegistrationRequest is made via the API, the creation fails because the user_id is obviously null.
This is why I would like to set the user manually after a registration request is made via the API but before the RegistrationRequest is stored in the database.
The user is known via the global REMOTE_USER from where I can derive the corresponding user object.
src/Entity/RegistrationRequest.php:
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=App\Repository\RegistrationRequestRepository::class)
* #ApiResource(
* normalizationContext={"groups" = {"read"}},
* denormalizationContext={"groups" = {"write"}},
* paginationEnabled=false,
* )
*/
class RegistrationRequest
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"read"})
*/
private $id;
/**
* #ORM\Column(type="string", length=64, nullable=true)
* #Groups({"read", "write"})
*/
private $opt_email;
/**
* #ORM\Column(type="string", length=255, nullable=true)
* #Groups({"read", "write"})
*/
private $title;
/**
* #Gedmo\Mapping\Annotation\Timestampable(on="create")
* #ORM\Column(type="datetime")
* #Groups({"read"})
*/
private $created_at;
/**
* #Gedmo\Mapping\Annotation\Timestampable(on="update")
* #ORM\Column(type="datetime")
* #Groups({"read"})
*/
private $updated_at;
/**
* #ORM\Column(type="text", nullable=true)
* #Groups({"read", "write"})
*/
private $notes;
/**
* #ORM\Column(type="string", length=16)
* #Groups({"read", "write"})
*/
private $language_code;
/**
* #ORM\Column(type="text")
* #Groups({"read", "write"})
*/
private $data;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="registrationRequests")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function getId(): ?int
{
return $this->id;
}
public function getOptEmail(): ?string
{
return $this->opt_email;
}
public function setOptEmail(?string $opt_email): self
{
$this->opt_email = $opt_email;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): self
{
$this->title = $title;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->created_at;
}
public function setCreatedAt(\DateTimeInterface $created_at): self
{
$this->created_at = $created_at;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updated_at;
}
public function setUpdatedAt(\DateTimeInterface $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}
public function getNotes(): ?string
{
return $this->notes;
}
public function setNotes(?string $notes): self
{
$this->notes = $notes;
return $this;
}
public function getLanguageCode(): ?string
{
return $this->language_code;
}
public function setLanguageCode(string $language_code): self
{
$this->language_code = $language_code;
return $this;
}
public function getData(): ?string
{
return $this->data;
}
public function setData(string $data): self
{
$this->data = $data;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
}
src/Controller/RegistrationRequestController.php:
<?php
namespace App\Controller;
use App\Service\IGSNService;
use App\Entity\RegistrationRequest;
use App\Form\RegistrationRequestType;
use App\Repository\RegistrationRequestRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* #Route("/registration_request")
*/
class RegistrationRequestController extends AbstractController
{
/**
* #Route("/")
*/
public function indexNoLocale(): Response
{
return $this->redirectToRoute('app_registration_request_index', ['_locale' => 'de']);
}
/**
* #Route("/{_locale<%app.supported_locales%>}/", name="app_registration_request_index", methods={"GET"})
*/
public function index(RegistrationRequestRepository $registrationRequestRepository): Response
{
return $this->render('registration_request/index.html.twig', [
'registration_requests' => $registrationRequestRepository->findAll(),
]);
}
/**
* #Route("/{_locale<%app.supported_locales%>}/new", name="app_registration_request_new", methods={"GET", "POST"})
*/
public function new(Request $request, RegistrationRequestRepository $registrationRequestRepository, IGSNService $igsnService, TranslatorInterface $translator): Response
{
$registrationRequest = new RegistrationRequest();
$form = $this->createForm(RegistrationRequestType::class, $registrationRequest);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$json_data = json_decode($registrationRequest->getData());
$err = $igsnService->validate_data($json_data);
if ($json_data !== null && empty($err)) {
$registrationRequestRepository->add($registrationRequest, true);
$this->addFlash(
'success',
$translator->trans('Your changes were saved!')
);
return $this->redirectToRoute('app_registration_request_index', [], Response::HTTP_SEE_OTHER);
} else {
$this->addFlash(
'schema_error',
$err
);
}
}
return $this->renderForm('registration_request/new.html.twig', [
'registration_request' => $registrationRequest,
'form' => $form,
]);
}
/**
* #Route("/{_locale<%app.supported_locales%>}/{id}", name="app_registration_request_show", methods={"GET"})
*/
public function show(RegistrationRequest $registrationRequest): Response
{
return $this->render('registration_request/show.html.twig', [
'registration_request' => $registrationRequest,
]);
}
/**
* #Route("/{_locale<%app.supported_locales%>}/{id}/edit", name="app_registration_request_edit", methods={"GET", "POST"})
*/
public function edit(Request $request, RegistrationRequest $registrationRequest, RegistrationRequestRepository $registrationRequestRepository, IGSNService $igsnService, TranslatorInterface $translator): Response
{
$form = $this->createForm(RegistrationRequestType::class, $registrationRequest);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$json_data = json_decode($registrationRequest->getData());
$err = $igsnService->validate_data($json_data);
if ($json_data !== null && empty($err)) {
$registrationRequestRepository->add($registrationRequest, true);
$this->addFlash(
'success',
$translator->trans('Your changes were saved!')
);
return $this->redirectToRoute('app_registration_request_index', [], Response::HTTP_SEE_OTHER);
} else {
$this->addFlash(
'schema_error',
$err
);
}
}
return $this->renderForm('registration_request/edit.html.twig', [
'registration_request' => $registrationRequest,
'form' => $form,
]);
}
/**
* #Route("/{_locale<%app.supported_locales%>}/{id}", name="app_registration_request_delete", methods={"POST"})
*/
public function delete(Request $request, RegistrationRequest $registrationRequest, RegistrationRequestRepository $registrationRequestRepository, TranslatorInterface $translator): Response
{
if ($this->isCsrfTokenValid('delete' . $registrationRequest->getId(), $request->request->get('_token'))) {
$registrationRequestRepository->remove($registrationRequest, true);
$this->addFlash(
'success',
$translator->trans('Request successfully deleted!')
);
}
return $this->redirectToRoute('app_registration_request_index', [], Response::HTTP_SEE_OTHER);
}
}
config/packages/api_platform.yaml
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
patch_formats:
json: ['application/merge-patch+json']
swagger:
versions: [3]
# Fixes empty api endpoint list with error:
# No operations defined in spec!
# See https://github.com/api-platform/core/issues/4485
metadata_backward_compatibility_layer: false
I have now got it solved with a DataPersister, as suggested by Julien B. in the comments.
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Entity\RegistrationRequest;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
final class RequestDataPersister implements DataPersisterInterface
{
private $security;
public function __construct(Security $security, EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->security = $security;
}
public function supports($data): bool
{
return $data instanceof RegistrationRequest;
}
public function persist($data)
{
$data->setUser($this->security->getUser());
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}
public function remove($data): void
{
// no action needed
}
}

How to mock variables in a PHPUnit Coverage Test?

I'm writing PHPUnit tests and running coverage tests. I admit its very difficult to have 100% coverage, however, I'd like to get as close as possible. In a following scenario, how to mock variables in a clause in order to test the code block?
class CalendarClientService
{
/** #var array SCOPES */
public const SCOPES = [Google_Service_Calendar::CALENDAR];
/** #var string ACCESS_TYPE */
public const ACCESS_TYPE = "offline";
/** #var string CALENDAR_ID */
public const CALENDAR_ID = "primary";
/** #var int MAX_RESULTS */
public const MAX_RESULTS = 25;
/** #var string ORDER_BY */
public const ORDER_BY = "startTime";
/** #var bool SINGLE_EVENTS */
public const SINGLE_EVENTS = true;
/** #var string|null TIME_MIN */
public const TIME_MIN = null;
/** #var bool CACHE_TIME_TO_LIVE */
public const CACHE_TIME_TO_LIVE = 604800;
/** #var string */
public string $clientSecretPath = "";
/** #var StorageAdapterFactoryInterface */
protected StorageAdapterFactoryInterface $storageAdapterFactory;
/** #var StorageInterface */
protected StorageInterface $storageInterfaceCache;
/**
* CalendarClientService constructor.
* #param string $clientSecretPath
* #param StorageAdapterFactoryInterface $storageAdapterFactory
* #param StorageInterface $storageInterfaceCache
*/
public function __construct(
string $clientSecretPath,
StorageAdapterFactoryInterface $storageAdapterFactory,
StorageInterface $storageInterfaceCache
) {
$this->clientSecretPath = $clientSecretPath;
$this->storageAdapterFactory = $storageAdapterFactory;
$this->storageInterfaceCache = $storageInterfaceCache;
}
/** #return string */
public function getClientSecretPath()
{
return $this->clientSecretPath;
}
/** #param string $secretFile */
public function setClientSecretPath(string $secretFile)
{
$this->clientSecretPath = $secretFile;
}
/**
* #param array
* #return Google_Service_Calendar_Event
*/
public function getGoogleServiceCalendarEvent($eventData)
{
return new Google_Service_Calendar_Event($eventData);
}
/**
* #param string
* #return Google_Service_Calendar_EventDateTime
*/
public function getGoogleServiceCalendarEventDateTime($dateTime)
{
$eventDateTime = new Google_Service_Calendar_EventDateTime();
$eventDateTime->setDateTime(Carbon::parse($dateTime)->toW3cString());
$eventDateTime->setTimeZone(Carbon::parse($dateTime)->timezone->getName());
return $eventDateTime;
}
/**
* #param Google_Client $client
* #return Events
*/
public function getGoogleServiceCalendarResourceEvents(Google_Client $client)
{
$service = new Google_Service_Calendar($client);
return $service->events;
}
/**
* #param int
* #return array
* #throws Exception
* #throws ExceptionInterface
*/
public function getEventData($id)
{
$client = $this->getClient();
if (!$this->authenticateClient($client)) {
return [
"error" => "authentication",
"url" => filter_var($client->createAuthUrl(), FILTER_SANITIZE_URL),
];
}
$service = $this->getGoogleServiceCalendarResourceEvents($client);
return ["event" => $service->get(self::CALENDAR_ID, $id)];
}
/**
* #return Google_Client
* #throws Exception
*/
public function getClient()
{
$client = new Google_Client();
$client->setApplicationName(Module::MODULE_NAME);
$client->setScopes(self::SCOPES);
$client->setAuthConfig($this->clientSecretPath);
$client->setAccessType(self::ACCESS_TYPE);
return $client;
}
/**
* #param Google_Client $client
* #return bool
* #throws ExceptionInterface
*/
public function authenticateClient(Google_Client $client)
{
if ($this->storageInterfaceCache->hasItem("api_access_token")) {
$accessToken = json_decode($this->storageInterfaceCache->getItem("api_access_token"), true);
if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
$this->storageInterfaceCache->removeItem("api_access_token");
} else {
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$client->setAccessToken($accessToken);
}
}
if ($client->isAccessTokenExpired()) {
$tokenValid = false;
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
$accessToken = $client->getAccessToken();
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$tokenValid = true;
} else {
$helper = new Helper();
if(!$helper->verifyAuthCode($_GET["code"])){
return $tokenValid;
}
$authCode = $_GET["code"];
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
$this->storageInterfaceCache->removeItem("api_access_token");
} else {
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$client->setAccessToken($accessToken);
$tokenValid = true;
}
}
} else {
$tokenValid = true;
}
return isset($tokenValid) ? $tokenValid : false;
}
I want to test this 6th line from top in authenticateClient method and want to mock this clause $accessToken["error"] == "invalid_grant" || empty($accessToken).
Now how to go about it?
Edit: Here's a test that I've written. Now whatever value I'm mocking in the $this->storageInterfaceCacheMock->method("getItem"), it always returns empty $accessToken. I've also attached the image for better understanding of what's happening and what I want.
public function testGetEventDataReturnsArrayOnSuccessfulAuthenticateClientThroughCache()
{
$this->storageInterfaceCacheMock->method("hasItem")->willReturn(true);
$this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"ya29.a0ARrdaM99pJTf1XzmD1ngxAH3XJud8lvHb0aTaOOABYdfdhsdfgsdfgVD9OoH4heiKoskDF7DMkHj1_aPuWIO5TE14KHJidFf66xwn_pTCkkSow6Kg4lRHwGrNQBQGI8sPlgnFO5U5hJvYdqgxDMHEqw1TER2w","expires_in":3599,"scope":"https:\/\/www.googleapis.com\/auth\/calendar","token_type":"Bearer","created":1637312218,"refresh_token":"1\/\/03psr5omKiljUCgYFDHDGJHGSHSNwF-L9Iraor5zcfe-h3BeCHSFGSDFGDGJHjy4UnEtKj974LXthS5bWexQcjviVGfJsdfGHSHgIrDn6Yk"}'));
$this->assertIsArray($this->calendarClientService->getEventData(1));
}
Another test which isn't performing as per required is mentioned below. (also visible in the screenshot)
public function testAccessTokenIsExpiredAndGotRefreshToken()
{
$this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
$this->googleClientMock->method("getRefreshToken")->willReturn(true);
$this->googleClientMock->method("fetchAccessTokenWithRefreshToken")->willReturnSelf();
$this->googleClientMock->method("getAccessToken")->willReturnSelf();
$this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
}
I believe you want json_decode you don't need double encode here:
$this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"...."}'));
just
$this->storageInterfaceCacheMock->method("getItem")->willReturn('{"access_token":"...."}');
Here's how I resolved the issue and got the test to be successful.
I declared a dummy ACCESS_TOKEN as below and then used in the test method.
class CalendarControllerTest extends AbstractApplicationTestCase
{
/** #var string CLIENT_SECRET */
public const CLIENT_SECRET = __DIR__ . "/../_fixtures/config/client_secret.json";
/** #var string CLIENT_SECRET */
public const ACCESS_TOKEN = [
"access_token" => "test-data",
"expires_in" => 3592,
"scope" => "https://www.googleapis.com/auth/calendar",
"token_type" => "Bearer",
"created" => 1640858809,
];
...
...
...
}
public function setUp(): void
{
parent::setUp();
$this->googleClientMock = $this->getMockBuilder(Google_Client::class)
->disableOriginalConstructor()
->onlyMethods(
[
"isAccessTokenExpired",
"setAuthConfig",
"getRefreshToken",
"fetchAccessTokenWithRefreshToken",
"fetchAccessTokenWithAuthCode",
"getAccessToken",
"setAccessToken",
]
)
->getMock();
$this->googleServiceCalendarResourceEventsMock = $this->getMockBuilder(Events::class)
->disableOriginalConstructor()
->onlyMethods(["get"])
->getMock();
$this->googleServiceCalendarEventMock = $this->getMockBuilder(Event::class)
->disableOriginalConstructor()
->getMock();
$this->storageInterfaceCacheMock = $this->getMockForAbstractClass(StorageInterface::class);
$this->container->setAllowOverride(true);
$this->container->setService(Google_Client::class, $this->googleClientMock);
$this->container->setService(Events::class, $this->googleServiceCalendarResourceEventsMock);
$this->container->setService(Event::class, $this->googleServiceCalendarEventMock);
$this->container->setService(StorageInterface::class, $this->storageInterfaceCacheMock);
$this->container->setAllowOverride(true);
$this->googleClientMock->method("setAuthConfig")->willReturn(true);
$this->calendarClientService = $this->container->get("ServiceManager")->get(CalendarClientService::class);
$this->calendarClientService->setClientSecretPath(CalendarControllerTest::CLIENT_SECRET);
}
/** #throws ExceptionInterface */
public function testAccessTokenIsExpiredAndFailureToRefreshTokenWillGenerateNewAccessToken()
{
$this->calendarClientService->setAuthCode(CalendarControllerTest::DEFAULT_TESTING_VALUE);
$this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
$this->googleClientMock->method("getRefreshToken")->willReturn(false);
$this->googleClientMock->method("fetchAccessTokenWithAuthCode")->willReturn(
CalendarControllerTest::ACCESS_TOKEN
);
$this->storageInterfaceCacheMock->method("setItem")->willReturn(true);
$this->googleClientMock->method("setAccessToken")->willReturnSelf();
$this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
}

Groups annotation doesn't work with api-platform when i want to serialize

I can't use group annotation to get only values who I decide to show. In my exemple, I want to get only emails of my users when I show all users with collectionOperations. It's the same problem with itemOperations
Here is my entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\Collection;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
* #ApiResource(
* collectionOperations={
* "post",
* "get"={
* "normalization_context"={"groups"={"user:read"}}
* }
* }
* )
*/
class User implements UserInterface
{
use ResourceId;
use Timestamble;
/**
* #ORM\Column(type="string", length=180, unique=true)
* #Groups("user:read")
*
*/
private $email;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\OneToMany(targetEntity=Article::class, mappedBy="author", orphanRemoval=true)
*/
private $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/**
* #return Collection|Article[]
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles[] = $article;
$article->setAuthor($this);
}
return $this;
}
public function removeArticle(Article $article): self
{
if ($this->articles->contains($article)) {
$this->articles->removeElement($article);
// set the owning side to null (unless already changed)
if ($article->getAuthor() === $this) {
$article->setAuthor(null);
}
}
return $this;
}
}
My API should return only email when I get in all my users, but returns:
"#context": "/api/contexts/User",
"#id": "/api/users",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/users/1",
"#type": "User"
},
{
"#id": "/api/users/2",
"#type": "User"
},
{
"#id": "/api/users/3",
"#type": "User"
},
// ...
],
"hydra:totalItems": 10
}
I don't understand why my emails are not visible, the collectionOperations works, the problem comes to #Groups("user:read") which isn't detected.
just replace this
* #Groups("user:read")
by this:
* #Groups({"user:read"})
and don't forget to clear the cache each time you modify the metadata

API Platform - onPreSerialize with MediaObject URI Resolver in normalization group

I have a MediaObject exposed as a subresource within a normalization context group "modules":
/**
* #ApiResource(
* attributes={"access_control"="is_granted('ROLE_ADMIN')"},
* collectionOperations={
* "get",
* "post",
* "allData"={
* "method"="GET",
* "path"="/appdata",
* "normalization_context"={"groups"={"modules"}},
* "access_control"="is_granted('IS_AUTHENTICATED_ANONYMOUSLY')"
* }
* }
* )
* #ORM\Entity(repositoryClass="App\Repository\LearningModuleRepository")
* #UniqueEntity("identifier")
*
*/
class LearningModule
/**
* #ORM\Entity(
* repositoryClass="App\Repository\MediaObjectRepository"
* )
* #ApiResource(
* iri="http://schema.org/MediaObject",
* normalizationContext={
* "groups"={"media_object_read"},
* },
* attributes={"access_control"="is_granted('ROLE_ADMIN')"},
* collectionOperations={
* "post"={
* "controller"=CreateMediaObject::class,
* "deserialize"=false,
* "validation_groups"={"Default", "media_object_create"},
* "swagger_context"={
* "consumes"={
* "multipart/form-data",
* },
* "parameters"={
* {
* "in"="formData",
* "name"="file",
* "type"="file",
* "description"="The file to upload",
* },
* },
* },
* },
* "get",
* },
* itemOperations={
* "get",
* "delete"
* },
* )
* #Vich\Uploadable
*/
class MediaObject
/**
* #var string|null
*
* #ApiProperty(iri="http://schema.org/contentUrl")
* #Groups({"media_object_read", "modules"})
*/
public $contentUrl;
/**
* #var string|null
* #Groups({"modules"})
* #ORM\Column(nullable=true)
*/
public $filePath;
When exposed through the normalization group I only get filePath but not contentUrl. Im assuming the propblem has to do with the the ContentUrlSubscriber as outlined in the official docs:
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onPreSerialize', EventPriorities::PRE_SERIALIZE],
];
}
public function onPreSerialize(GetResponseForControllerResultEvent $event): void
{
$controllerResult = $event->getControllerResult();
$request = $event->getRequest();
if ($controllerResult instanceof Response || !$request->attributes->getBoolean('_api_respond', true)) {
return;
}
if (!($attributes = RequestAttributesExtractor::extractAttributes($request)) || !\is_a($attributes['resource_class'], MediaObject::class, true)) {
return;
}
$mediaObjects = $controllerResult;
if (!is_iterable($mediaObjects)) {
$mediaObjects = [$mediaObjects];
}
foreach ($mediaObjects as $mediaObject) {
if (!$mediaObject instanceof MediaObject) {
continue;
}
$mediaObject->contentUrl = $this->storage->resolveUri($mediaObject, 'file');
}
}
Does anybody have a clue how to handle the preSerialization in this case?
Thank You
I have found a solution to this problem:
The callback on PRE_SERIALIZE is aborted, since a LearningModule Object (not a MediaObject) has triggered the serialization ($attributes['resource_class'] === LearningModule).
To overcome this limitation, a decorated normalizer should be used as follows:
<?php
namespace App\Serializer;
use App\Entity\MediaObject;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Vich\UploaderBundle\Storage\StorageInterface;
final class MediaObjectContentUrlNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
private $decorated;
private $storage;
public function __construct(NormalizerInterface $decorated, StorageInterface $storage)
{
if (!$decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}
$this->decorated = $decorated;
$this->storage = $storage;
}
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
public function normalize($object, $format = null, array $context = [])
{
$data = $this->decorated->normalize($object, $format, $context);
if ($object instanceof MediaObject) {
$data['contentUrl'] = $this->storage->resolveUri( $object, 'file');
}
return $data;
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->decorated->supportsDenormalization($data, $type, $format);
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->decorated->denormalize($data, $class, $format, $context);
}
public function setSerializer(SerializerInterface $serializer)
{
if($this->decorated instanceof SerializerAwareInterface) {
$this->decorated->setSerializer($serializer);
}
}
}
Implementation details can be found here:
https://api-platform.com/docs/core/serialization/#decorating-a-serializer-and-adding-extra-data

Setters and getters aren't accessible from custom user entity in Symfony2

I'm trying to implement the hwioauthbundle for connecting with Google.
However, I'm facing the problem that symfony can't seem to find the method declared in the User entity - I believe it has something to do with the FOSUserBundle that I'm also using.
Here is my GoogleProvider.php:
<?php
namespace AppBundle\Security\User\Provider;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use Symfony\Component\Security\Core\User\UserInterface;
class GoogleProvider extends BaseClass
{
/**
* {#inheritDoc}
*/
public function connect(UserInterface $user, UserResponseInterface $response)
{
$property = $this->getProperty($response);
$username = $response->getUsername();
//on connect - get the access token and the user ID
$service = $response->getResourceOwner()->getName();
$setter = 'set'.ucfirst($service);
$setter_id = $setter.'Id';
$setter_token = $setter.'AccessToken';
//we "disconnect" previously connected users
if (null !== $previousUser = $this->userManager->findUserBy(array($property => $username))) {
$previousUser->$setter_id(null);
$previousUser->$setter_token(null);
$this->userManager->updateUser($previousUser);
}
//we connect current user
$user->$setter_id($username);
$user->$setter_token($response->getAccessToken());
$this->userManager->updateUser($user);
}
/**
* {#inheritdoc}
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$username = $response->getUsername();
$user = $this->userManager->findUserBy(array($this->getProperty($response) => $username));
//when the user is registrating
if (null === $user) {
$service = $response->getResourceOwner()->getName();
$setter = 'set'.ucfirst($service);
$setter_id = $setter.'Id';
$setter_token = $setter.'AccessToken';
// create new user here
$user = $this->userManager->createUser();
$user->$setter_id($username);
$user->$setter_token($response->getAccessToken());
//I have set all requested data with the user's username
//modify here with relevant data
$user->setUsername($username);
$user->setEmail($username);
$user->setPassword($username);
$user->setEnabled(true);
$this->userManager->updateUser($user);
return $user;
}
//if user exists - go with the HWIOAuth way
$user = parent::loadUserByOAuthUserResponse($response);
$serviceName = $response->getResourceOwner()->getName();
$setter = 'set' . ucfirst($serviceName) . 'AccessToken';
//update access token
$user->$setter($response->getAccessToken());
return $user;
}
}
And here is FOSUBUserProvider.php:
<?php
namespace HWI\Bundle\OAuthBundle\Security\Core\User;
use FOS\UserBundle\Model\User;
use FOS\UserBundle\Model\UserManagerInterface;
use HWI\Bundle\OAuthBundle\Connect\AccountConnectorInterface;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\Exception\AccountNotLinkedException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class FOSUBUserProvider implements UserProviderInterface, AccountConnectorInterface, OAuthAwareUserProviderInterface
{
/**
* #var UserManagerInterface
*/
protected $userManager;
/**
* #var array
*/
protected $properties = array(
'identifier' => 'id',
);
/**
* #var PropertyAccessor
*/
protected $accessor;
/**
* Constructor.
*
* #param UserManagerInterface $userManager FOSUB user provider.
* #param array $properties Property mapping.
*/
public function __construct(UserManagerInterface $userManager, array $properties)
{
$this->userManager = $userManager;
$this->properties = array_merge($this->properties, $properties);
$this->accessor = PropertyAccess::createPropertyAccessor();
}
/**
* {#inheritDoc}
*/
public function loadUserByUsername($username)
{
// Compatibility with FOSUserBundle < 2.0
if (class_exists('FOS\UserBundle\Form\Handler\RegistrationFormHandler')) {
return $this->userManager->loadUserByUsername($username);
}
return $this->userManager->findUserByUsername($username);
}
/**
* {#inheritdoc}
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$username = $response->getUsername();
$user = $this->userManager->findUserBy(array($this->getProperty($response) => $username));
if (null === $user || null === $username) {
throw new AccountNotLinkedException(sprintf("User '%s' not found.", $username));
}
return $user;
}
/**
* {#inheritDoc}
*/
public function connect(UserInterface $user, UserResponseInterface $response)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user)));
}
$property = $this->getProperty($response);
if (!$this->accessor->isWritable($user, $property)) {
throw new \RuntimeException(sprintf("Class '%s' must have defined setter method for property: '%s'.", get_class($user), $property));
}
$username = $response->getUsername();
if (null !== $previousUser = $this->userManager->findUserBy(array($property => $username))) {
$this->accessor->setValue($previousUser, $property, null);
$this->userManager->updateUser($previousUser);
}
$this->accessor->setValue($user, $property, $username);
$this->userManager->updateUser($user);
}
/**
* {#inheritDoc}
*/
public function refreshUser(UserInterface $user)
{
// Compatibility with FOSUserBundle < 2.0
if (class_exists('FOS\UserBundle\Form\Handler\RegistrationFormHandler')) {
return $this->userManager->refreshUser($user);
}
$identifier = $this->properties['identifier'];
if (!$user instanceof User || !$this->accessor->isReadable($user, $identifier)) {
throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user)));
}
$userId = $this->accessor->getValue($user, $identifier);
if (null === $user = $this->userManager->findUserBy(array($identifier => $userId))) {
throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $userId));
}
return $user;
}
/**
* {#inheritDoc}
*/
public function supportsClass($class)
{
$userClass = $this->userManager->getClass();
return $userClass === $class || is_subclass_of($class, $userClass);
}
/**
* Gets the property for the response.
*
* #param UserResponseInterface $response
*
* #return string
*
* #throws \RuntimeException
*/
protected function getProperty(UserResponseInterface $response)
{
$resourceOwnerName = $response->getResourceOwner()->getName();
if (!isset($this->properties[$resourceOwnerName])) {
throw new \RuntimeException(sprintf("No property defined for entity for resource owner '%s'.", $resourceOwnerName));
}
return $this->properties[$resourceOwnerName];
}
}
And here is my service:
parameters:
my_user_provider.class: AppBundle\Security\User\Provider\GoogleProvider
services:
my_user_provider:
class: "%my_user_provider.class%"
#this is the place where the properties are passed to the UserProvider - see config.yml
arguments: [#fos_user.user_manager,{facebook: facebook_id, google: google_id}]
Here is my User entity:
<?php
namespace AppBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
*/
private $name;
/**
* #var integer
*/
private $facebook_id;
/**
* #var string
*/
private $facebookAccessToken;
/**
* #var integer
*/
private $google_id;
/**
* #var string
*/
private $googleAccessToken;
/**
* #var \Doctrine\Common\Collections\Collection
*/
private $keyword;
/**
* Constructor
*/
public function __construct()
{
$this->keyword = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return User
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set facebook_id
*
* #param integer $facebookId
* #return User
*/
public function setFacebookId($facebookId)
{
$this->facebook_id = $facebookId;
return $this;
}
/**
* Get facebook_id
*
* #return integer
*/
public function getFacebookId()
{
return $this->facebook_id;
}
/**
* Set facebookAccessToken
*
* #param string $facebookAccessToken
* #return User
*/
public function setFacebookAccessToken($facebookAccessToken)
{
$this->facebookAccessToken = $facebookAccessToken;
return $this;
}
/**
* Get facebookAccessToken
*
* #return string
*/
public function getFacebookAccessToken()
{
return $this->facebookAccessToken;
}
/**
* Set google_id
*
* #param integer $googleId
* #return User
*/
public function setGoogleId($googleId)
{
$this->google_id = $googleId;
return $this;
}
/**
* Get google_id
*
* #return integer
*/
public function getGoogleId()
{
return $this->google_id;
}
/**
* Set googleAccessToken
*
* #param string $googleAccessToken
* #return User
*/
public function setGoogleAccessToken($googleAccessToken)
{
$this->googleAccessToken = $googleAccessToken;
return $this;
}
/**
* Get googleAccessToken
*
* #return string
*/
public function getGoogleAccessToken()
{
return $this->googleAccessToken;
}
/**
* Add keyword
*
* #param \AppBundle\Entity\Keyword $keyword
* #return User
*/
public function addKeyword(\AppBundle\Entity\Keyword $keyword)
{
$this->keyword[] = $keyword;
return $this;
}
/**
* Remove keyword
*
* #param \AppBundle\Entity\Keyword $keyword
*/
public function removeKeyword(\AppBundle\Entity\Keyword $keyword)
{
$this->keyword->removeElement($keyword);
}
/**
* Get keyword
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getKeyword()
{
return $this->keyword;
}
}
Does anyone have any idea what I'm doing wrong?
I works when I do:
$user->setUsername($username);
$user->setEmail($username);
$user->setPassword($username);
$user->setEnabled(true);
But for instance I can't do:
$user->setGoogleId(123);
but I guess it is because they are not in my User entity, but in FOSUserBundle.
It seems like it doesn't extend my User entity.
I appreciate all kinds of help!
You need to add every method you expect to work in your User entity. There is no magic. So, add a setFacebookId(), setFacebookAccessToken(), etc. Indeed, you could even add a setGoogleId(), but there is no property called $googleId in your entity.

Resources