From e63314aaa0a61277f74fe48eeeb1d6f0536d5ce4 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <jr@rushlow.dev> Date: Fri, 29 Jan 2021 10:23:37 -0500 Subject: [PATCH 1/6] WIP - add docs to show API Platform implementation. --- README.md | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/README.md b/README.md index 7efd1597..3c5a587e 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,177 @@ Feel free to open an issue for questions, problems, or suggestions with our bund Issues pertaining to Symfony's Maker Bundle, specifically `make:reset-password`, should be addressed in the [Symfony Maker repository](https://github.com/symfony/maker-bundle). +## API Usage Example + +If you're using [API Platform](https://api-platform.com/), this example will +demonstrate how to implement ResetPasswordBundle into the API. + +```php +// src/Entity/ResetPasswordRequest + +<?php + +namespace App\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use App\Dto\ResetPasswordInput; +use App\Repository\ResetPasswordRequestRepository; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Validator\Constraints as Assert; +use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; +use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; + +/** + * @ApiResource( + * input=ResetPasswordInput::class, + * shortName="reset-password", + * collectionOperations={ + * "post" = {"security" = "is_granted('IS_ANONYMOUS')"}, + * }, + * itemOperations={ + * }, + * denormalizationContext={"groups"={"reset-password:write"}}, + * ) + * + * @ORM\Entity(repositoryClass=ResetPasswordRequestRepository::class) + */ +class ResetPasswordRequest implements ResetPasswordRequestInterface +{ + use ResetPasswordRequestTrait; + + /** + * @ORM\Id + * @ORM\Column(type="string", unique=true) + */ + private string $id; + + /** + * @ORM\ManyToOne(targetEntity=User::class) + * @ORM\JoinColumn(nullable=false) + */ + private User $user; + + /** + * This property is not persisted. It's needed when a reset is requested + * through the API. + * + * @Assert\NotBlank + * @Groups({"reset-password:write"}) + */ + private string $email; // email is not actually persisted. We need this to select the user in a API call. + + public function __construct(User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken) + { + $this->id = new UuidV4(); + $this->user = $user; + $this->initialize($expiresAt, $selector, $hashedToken); + } + + public function getId(): string + { + return $this->id; + } + + public function getUser(): User + { + return $this->user; + } +} +``` + +Because the `ResetPasswordHelper::generateResetToken()` method is responsible for +creating and persisting a `ResetPasswordRequest` object after the reset token has been +generated, we can't call `POST /api/reset-passwords` with `['email' => 'someone@example.com']`. + +We'll create a Data Transfer Object (`DTO`) first, that will be used by a Data Persister +to generate the actual `ResetPasswordRequest` object from the email address provided +in the `POST` api call. + +```php +<?php + +namespace App\Dto; + +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * @author Jesse Rushlow <jr@rushlow.dev> + */ +class ResetPasswordInput +{ + /** + * @Groups({"reset-password:write"}) + * @Assert\NotBlank + * @Assert\Email() + */ + public ?string $email = null; +} +``` + +Finally we'll create a Data Persister that is responsible for using the +`ResetPasswordHelper::class` to generate a `ResetPasswordRequest` and email the +token to the user. + +```php +<?php + +namespace App\DataPersister; + +use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; +use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use App\Dto\ResetPasswordInput; +use App\Entity\User; +use App\Repository\UserRepository; +use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; + +/** + * @author Jesse Rushlow <jr@rushlow.dev> + */ +class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface +{ + private DataPersisterInterface $decoratedDataPersister; + private UserRepository $userRepository; + private ResetPasswordHelperInterface $resetPasswordHelper; + + public function __construct(DataPersisterInterface $decoratedDataPersister, UserRepository $userRepository, ResetPasswordHelperInterface $resetPasswordHelper) + { + $this->decoratedDataPersister = $decoratedDataPersister; + $this->userRepository = $userRepository; + $this->resetPasswordHelper = $resetPasswordHelper; + } + + public function supports($data, array $context = []): bool + { + // Make sure to check if data is an instance of the DTO, not the ResetPasswordRequest. + return $data instanceof ResetPasswordInput; + } + + public function persist($data, array $context = []): void + { + /** @var ResetPasswordInput $data */ + $user = $this->userRepository->findOneBy(['email' => $data->email]); + + if (!$user instanceof User) { + return; + } + + $token = $this->resetPasswordHelper->generateResetToken($user); + + // Send email || Dispatch Email w/ Messenger + + return; + } + + public function remove($data, array $context = []): void + { + $this->decoratedDataPersister->remove($data); + } +} +``` + ## Security Issues For **security related vulnerabilities**, we ask that you send an email to `ryan [at] symfonycasts.com` instead of creating an issue. From b713e09f3f4b1ba052e8e4e1decd0bdb5d05d48b Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <jr@rushlow.dev> Date: Fri, 29 Jan 2021 18:07:38 -0500 Subject: [PATCH 2/6] return status 202 w/ no output --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c5a587e..122af324 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,10 @@ use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; /** * @ApiResource( * input=ResetPasswordInput::class, + * output=false, * shortName="reset-password", * collectionOperations={ - * "post" = {"security" = "is_granted('IS_ANONYMOUS')"}, + * "post" = {"security" = "is_granted('IS_ANONYMOUS')", "status" = 202}, * }, * itemOperations={ * }, From 9cdbce59fc8b962a3cce0ae92f9d986a36d1c23f Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <jr@rushlow.dev> Date: Sun, 31 Jan 2021 14:46:41 -0500 Subject: [PATCH 3/6] add data provider and update existing models --- README.md | 144 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 122af324..77e9bb03 100644 --- a/README.md +++ b/README.md @@ -116,23 +116,29 @@ use ApiPlatform\Core\Annotation\ApiResource; use App\Dto\ResetPasswordInput; use App\Repository\ResetPasswordRequestRepository; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Uid\UuidV4; -use Symfony\Component\Validator\Constraints as Assert; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; /** * @ApiResource( + * security="is_granted('IS_ANONYMOUS')", * input=ResetPasswordInput::class, * output=false, * shortName="reset-password", * collectionOperations={ - * "post" = {"security" = "is_granted('IS_ANONYMOUS')", "status" = 202}, + * "post" = { + * "denormalization_context"={"groups"={"reset-password:post"}}, + * "status" = 202, + * "validation_groups"={"postValidation"}, + * }, * }, * itemOperations={ + * "put" = { + * "denormalization_context"={"groups"={"reset-password:put"}}, + * "validation_groups"={"putValidation"}, + * }, * }, - * denormalizationContext={"groups"={"reset-password:write"}}, * ) * * @ORM\Entity(repositoryClass=ResetPasswordRequestRepository::class) @@ -153,15 +159,6 @@ class ResetPasswordRequest implements ResetPasswordRequestInterface */ private User $user; - /** - * This property is not persisted. It's needed when a reset is requested - * through the API. - * - * @Assert\NotBlank - * @Groups({"reset-password:write"}) - */ - private string $email; // email is not actually persisted. We need this to select the user in a API call. - public function __construct(User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken) { $this->id = new UuidV4(); @@ -203,11 +200,58 @@ use Symfony\Component\Validator\Constraints as Assert; class ResetPasswordInput { /** - * @Groups({"reset-password:write"}) - * @Assert\NotBlank - * @Assert\Email() + * @Assert\NotBlank(groups={"postValidation"}) + * @Assert\Email(groups={"postValidation"}) + * @Groups({"reset-password:post"}) + */ + public string $email; + + /** + * @Assert\NotBlank(groups={"putValidation"}) + * @Groups({"reset-password:put"}) */ - public ?string $email = null; + public string $token; + + /** + * @Assert\NotBlank(groups={"putValidation"}) + * @Groups({"reset-password:put"}) + */ + public string $plainTextPassword; +} +``` + +```php +<?php + +namespace App\DataProvider; + +use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; +use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; +use App\Entity\ResetPasswordRequest; +use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; + +class ResetPasswordDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface +{ + private ResetPasswordHelperInterface $resetPasswordHelper; + + public function __construct(ResetPasswordHelperInterface $resetPasswordHelper) + { + $this->resetPasswordHelper = $resetPasswordHelper; + } + + public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + { + $user = $this->resetPasswordHelper->validateTokenAndFetchUser($id); + + $this->resetPasswordHelper->removeResetRequest($id); + + return $user; + } + + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + return ResetPasswordRequest::class === $resourceClass && 'put' === $operationName; + } } ``` @@ -221,10 +265,12 @@ token to the user. namespace App\DataPersister; use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; -use ApiPlatform\Core\DataPersister\DataPersisterInterface; use App\Dto\ResetPasswordInput; use App\Entity\User; +use App\Message\SendResetPasswordMessage; use App\Repository\UserRepository; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; /** @@ -232,27 +278,60 @@ use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; */ class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface { - private DataPersisterInterface $decoratedDataPersister; private UserRepository $userRepository; private ResetPasswordHelperInterface $resetPasswordHelper; + private MessageBusInterface $messageBus; + private UserPasswordEncoderInterface $userPasswordEncoder; - public function __construct(DataPersisterInterface $decoratedDataPersister, UserRepository $userRepository, ResetPasswordHelperInterface $resetPasswordHelper) + public function __construct(UserRepository $userRepository, ResetPasswordHelperInterface $resetPasswordHelper, MessageBusInterface $messageBus, UserPasswordEncoderInterface $userPasswordEncoder) { - $this->decoratedDataPersister = $decoratedDataPersister; $this->userRepository = $userRepository; $this->resetPasswordHelper = $resetPasswordHelper; + $this->messageBus = $messageBus; + $this->userPasswordEncoder = $userPasswordEncoder; } public function supports($data, array $context = []): bool { - // Make sure to check if data is an instance of the DTO, not the ResetPasswordRequest. - return $data instanceof ResetPasswordInput; + if (!$data instanceof ResetPasswordInput) { + return false; + } + + if (isset($context['collection_operation_name']) && 'post' === $context['collection_operation_name']) { + return true; + } + + if (isset($context['item_operation_name']) && 'put' === $context['item_operation_name']) { + return true; + } + + return false; } + /** + * @param ResetPasswordInput $data + */ public function persist($data, array $context = []): void { - /** @var ResetPasswordInput $data */ - $user = $this->userRepository->findOneBy(['email' => $data->email]); + if (isset($context['collection_operation_name']) && 'post' === $context['collection_operation_name']) { + $this->generateRequest($data->email); + + return; + } + + if (isset($context['item_operation_name']) && 'put' === $context['item_operation_name']) { + $this->changePassword($context['previous_data'], $data->plainTextPassword); + } + } + + public function remove($data, array $context = []): void + { + throw new \RuntimeException('Operation not supported.'); + } + + private function generateRequest(string $email): void + { + $user = $this->userRepository->findOneBy(['email' => $email]); if (!$user instanceof User) { return; @@ -260,14 +339,19 @@ class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface $token = $this->resetPasswordHelper->generateResetToken($user); - // Send email || Dispatch Email w/ Messenger - - return; + /** @psalm-suppress PossiblyNullArgument */ + $this->messageBus->dispatch(new SendResetPasswordMessage($user->getEmail(), $token)); } - public function remove($data, array $context = []): void + private function changePassword(User $previousUser, string $plainTextPassword): void { - $this->decoratedDataPersister->remove($data); + $userId = $previousUser->getId(); + + $user = $this->userRepository->find($userId); + + $encoded = $this->userPasswordEncoder->encodePassword($user, $plainTextPassword); + + $this->userRepository->upgradePassword($user, $encoded); } } ``` From eaf9a7f72c47cef969ddd368ac940b9ff0fd39d2 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <jr@rushlow.dev> Date: Sun, 31 Jan 2021 15:04:32 -0500 Subject: [PATCH 4/6] model cleanup and refactoring --- README.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 77e9bb03..6c8937cd 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,8 @@ namespace App\DataProvider; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use App\Entity\ResetPasswordRequest; +use App\Entity\User; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; class ResetPasswordDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface @@ -239,19 +241,27 @@ class ResetPasswordDataProvider implements ItemDataProviderInterface, Restricted $this->resetPasswordHelper = $resetPasswordHelper; } - public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + return ResetPasswordRequest::class === $resourceClass && 'put' === $operationName; + } + + public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): User { + if (!is_string($id)) { + throw new NotFoundHttpException('Invalid token.'); + } + $user = $this->resetPasswordHelper->validateTokenAndFetchUser($id); + if (!$user instanceof User) { + throw new NotFoundHttpException('Invalid token.'); + } + $this->resetPasswordHelper->removeResetRequest($id); return $user; } - - public function supports(string $resourceClass, string $operationName = null, array $context = []): bool - { - return ResetPasswordRequest::class === $resourceClass && 'put' === $operationName; - } } ``` @@ -320,6 +330,10 @@ class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface } if (isset($context['item_operation_name']) && 'put' === $context['item_operation_name']) { + if (!$context['previous_data'] instanceof User) { + return; + } + $this->changePassword($context['previous_data'], $data->plainTextPassword); } } @@ -349,6 +363,10 @@ class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface $user = $this->userRepository->find($userId); + if (null === $user) { + return; + } + $encoded = $this->userPasswordEncoder->encodePassword($user, $plainTextPassword); $this->userRepository->upgradePassword($user, $encoded); From bd2f8ccd21b811e935a43421fb7d6fa4747decd4 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <jr@rushlow.dev> Date: Sun, 31 Jan 2021 15:39:45 -0500 Subject: [PATCH 5/6] WIP - add missing transformer --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 6c8937cd..15f6b16d 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,36 @@ class ResetPasswordInput ```php <?php +namespace App\DataTransformer; + +use ApiPlatform\Core\DataTransformer\DataTransformerInterface; +use App\Dto\ResetPasswordInput; +use App\Entity\ResetPasswordRequest; + +/** + * @author Jesse Rushlow <jr@rushlow.dev> + */ +class ResetPasswordInputDataTransformer implements DataTransformerInterface +{ + public function transform($object, string $to, array $context = []): object + { + return $object; + } + + public function supportsTransformation($data, string $to, array $context = []): bool + { + if ($data instanceof ResetPasswordRequest) { + return false; + } + + return ResetPasswordRequest::class === $to && ($context['input']['class'] ?? null) === ResetPasswordInput::class; + } +} +``` + +```php +<?php + namespace App\DataProvider; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; From d316a1b21bfe1d0ea535a8d936960cee3d8da0a8 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow <jr@rushlow.dev> Date: Mon, 15 Feb 2021 09:28:35 -0500 Subject: [PATCH 6/6] stop using the request entity and use 2 dto's --- README.md | 110 +++++++++++++++++++++++++----------------------------- 1 file changed, 51 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 15f6b16d..14a42506 100644 --- a/README.md +++ b/README.md @@ -121,26 +121,6 @@ use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; /** - * @ApiResource( - * security="is_granted('IS_ANONYMOUS')", - * input=ResetPasswordInput::class, - * output=false, - * shortName="reset-password", - * collectionOperations={ - * "post" = { - * "denormalization_context"={"groups"={"reset-password:post"}}, - * "status" = 202, - * "validation_groups"={"postValidation"}, - * }, - * }, - * itemOperations={ - * "put" = { - * "denormalization_context"={"groups"={"reset-password:put"}}, - * "validation_groups"={"putValidation"}, - * }, - * }, - * ) - * * @ORM\Entity(repositoryClass=ResetPasswordRequestRepository::class) */ class ResetPasswordRequest implements ResetPasswordRequestInterface @@ -182,39 +162,38 @@ Because the `ResetPasswordHelper::generateResetToken()` method is responsible fo creating and persisting a `ResetPasswordRequest` object after the reset token has been generated, we can't call `POST /api/reset-passwords` with `['email' => 'someone@example.com']`. -We'll create a Data Transfer Object (`DTO`) first, that will be used by a Data Persister +We'll create 2 Data Transfer Objects (`DTO`) ~that will be used by a Data Persister to generate the actual `ResetPasswordRequest` object from the email address provided -in the `POST` api call. +in the `POST` api call.~ ```php <?php namespace App\Dto; -use Symfony\Component\Serializer\Annotation\Groups; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; use Symfony\Component\Validator\Constraints as Assert; /** + * @ApiResource( + * output=false, + * collectionOperations={}, + * itemOperations={"put"}, + * shortName="reset-password" + * ) + * * @author Jesse Rushlow <jr@rushlow.dev> */ class ResetPasswordInput { /** - * @Assert\NotBlank(groups={"postValidation"}) - * @Assert\Email(groups={"postValidation"}) - * @Groups({"reset-password:post"}) - */ - public string $email; - - /** - * @Assert\NotBlank(groups={"putValidation"}) - * @Groups({"reset-password:put"}) + * @ApiProperty(identifier=true, writable=false) */ public string $token; /** - * @Assert\NotBlank(groups={"putValidation"}) - * @Groups({"reset-password:put"}) + * @Assert\NotBlank() */ public string $plainTextPassword; } @@ -223,30 +202,34 @@ class ResetPasswordInput ```php <?php -namespace App\DataTransformer; +namespace App\Dto; -use ApiPlatform\Core\DataTransformer\DataTransformerInterface; -use App\Dto\ResetPasswordInput; -use App\Entity\ResetPasswordRequest; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use Symfony\Component\Validator\Constraints as Assert; /** + * @ApiResource( + * output=false, + * collectionOperations={ + * "post" = { + * "status" = 202, + * }, + * }, + * itemOperations={}, + * shortName="reset-password-request" + * ) + * * @author Jesse Rushlow <jr@rushlow.dev> */ -class ResetPasswordInputDataTransformer implements DataTransformerInterface +class ResetPasswordRequestInput { - public function transform($object, string $to, array $context = []): object - { - return $object; - } - - public function supportsTransformation($data, string $to, array $context = []): bool - { - if ($data instanceof ResetPasswordRequest) { - return false; - } - - return ResetPasswordRequest::class === $to && ($context['input']['class'] ?? null) === ResetPasswordInput::class; - } + /** + * @Assert\NotBlank() + * @Assert\Email() + * @ApiProperty(identifier=true) + */ + public string $email; } ``` @@ -257,11 +240,15 @@ namespace App\DataProvider; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; -use App\Entity\ResetPasswordRequest; +use App\Dto\ResetPasswordInput; use App\Entity\User; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; +/** + * @author Jesse Rushlow <jr@rushlow.dev> + */ class ResetPasswordDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { private ResetPasswordHelperInterface $resetPasswordHelper; @@ -273,7 +260,7 @@ class ResetPasswordDataProvider implements ItemDataProviderInterface, Restricted public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { - return ResetPasswordRequest::class === $resourceClass && 'put' === $operationName; + return ResetPasswordInput::class === $resourceClass && 'put' === $operationName; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): User @@ -282,7 +269,12 @@ class ResetPasswordDataProvider implements ItemDataProviderInterface, Restricted throw new NotFoundHttpException('Invalid token.'); } - $user = $this->resetPasswordHelper->validateTokenAndFetchUser($id); + try { + $user = $this->resetPasswordHelper->validateTokenAndFetchUser($id); + } catch (ResetPasswordExceptionInterface $ex) { + // Log exception id needed + throw new NotFoundHttpException(); + } if (!$user instanceof User) { throw new NotFoundHttpException('Invalid token.'); @@ -306,6 +298,7 @@ namespace App\DataPersister; use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; use App\Dto\ResetPasswordInput; +use App\Dto\ResetPasswordRequestInput; use App\Entity\User; use App\Message\SendResetPasswordMessage; use App\Repository\UserRepository; @@ -333,7 +326,7 @@ class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface public function supports($data, array $context = []): bool { - if (!$data instanceof ResetPasswordInput) { + if (!($data instanceof ResetPasswordInput || $data instanceof ResetPasswordRequestInput)) { return false; } @@ -348,18 +341,17 @@ class ResetPasswordDataPersister implements ContextAwareDataPersisterInterface return false; } - /** - * @param ResetPasswordInput $data - */ public function persist($data, array $context = []): void { if (isset($context['collection_operation_name']) && 'post' === $context['collection_operation_name']) { + /** @var ResetPasswordRequestInput $data */ $this->generateRequest($data->email); return; } if (isset($context['item_operation_name']) && 'put' === $context['item_operation_name']) { + /** @var ResetPasswordInput $data */ if (!$context['previous_data'] instanceof User) { return; }