diff --git a/src/ApiResource/Project/RewardApiResource.php b/src/ApiResource/Project/RewardApiResource.php index bf34027..8d33dbb 100644 --- a/src/ApiResource/Project/RewardApiResource.php +++ b/src/ApiResource/Project/RewardApiResource.php @@ -6,8 +6,8 @@ use ApiPlatform\Metadata as API; use App\Entity\Money; use App\Entity\Project\Reward; -use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use App\State\Project\RewardStateProcessor; use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; @@ -18,7 +18,7 @@ shortName: 'ProjectReward', stateOptions: new Options(entityClass: Reward::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class + processor: RewardStateProcessor::class )] class RewardApiResource { diff --git a/src/ApiResource/Project/RewardClaimApiResource.php b/src/ApiResource/Project/RewardClaimApiResource.php index 153eabb..f9c1d38 100644 --- a/src/ApiResource/Project/RewardClaimApiResource.php +++ b/src/ApiResource/Project/RewardClaimApiResource.php @@ -6,8 +6,8 @@ use ApiPlatform\Metadata as API; use App\ApiResource\User\UserApiResource; use App\Entity\Project\RewardClaim; -use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProvider; +use App\State\Project\RewardClaimStateProcessor; use Symfony\Component\Validator\Constraints as Assert; /** @@ -17,7 +17,7 @@ shortName: 'ProjectRewardClaim', stateOptions: new Options(entityClass: RewardClaim::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class + processor: RewardClaimStateProcessor::class )] class RewardClaimApiResource { diff --git a/src/Entity/Project/Project.php b/src/Entity/Project/Project.php index d91b71a..2487053 100644 --- a/src/Entity/Project/Project.php +++ b/src/Entity/Project/Project.php @@ -9,11 +9,14 @@ use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Entity\User\User; +use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\ProjectRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: ProjectRepository::class)] class Project implements UserOwnedInterface, AccountingOwnerInterface { diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index 7d9e4e1..eca3970 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -3,7 +3,9 @@ namespace App\Entity\Project; use App\Entity\Money; +use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\RewardRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -12,6 +14,7 @@ /** * A ProjectReward is something the Project owner wishes to give in exchange for contributions to their Project. */ +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: RewardRepository::class)] class Reward { diff --git a/src/Entity/Project/RewardClaim.php b/src/Entity/Project/RewardClaim.php index 4dc14fd..1da5454 100644 --- a/src/Entity/Project/RewardClaim.php +++ b/src/Entity/Project/RewardClaim.php @@ -4,9 +4,12 @@ use App\Entity\Interface\UserOwnedInterface; use App\Entity\User\User; +use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\RewardClaimRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\ORM\Mapping as ORM; +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: RewardClaimRepository::class)] class RewardClaim implements UserOwnedInterface { diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index d913e88..a29fba4 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -8,7 +8,9 @@ use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; +use App\Mapping\Provider\EntityMapProvider; use App\Repository\User\UserRepository; +use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -24,10 +26,11 @@ * This allows to keep an User's "wallet", withholding their non-raised fundings into their Accounting. */ #[Gedmo\Loggable()] -#[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] -#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] +#[MapProvider(EntityMapProvider::class)] #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Index(fields: ['migratedId'])] +#[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] +#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] class User implements UserInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { use MigratedEntity; @@ -68,7 +71,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account /** * The projects owned by this User. */ - #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class)] + #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class, cascade: ['persist'])] private Collection $projects; /** diff --git a/src/Mapping/Provider/EntityMapProvider.php b/src/Mapping/Provider/EntityMapProvider.php new file mode 100644 index 0000000..c41065f --- /dev/null +++ b/src/Mapping/Provider/EntityMapProvider.php @@ -0,0 +1,28 @@ +id)) { + return null; + } + + $repository = $this->entityManager->getRepository($targetType); + + if (!$repository) { + throw new \Exception(\sprintf("No repository found for '%s' class. Is it an Entity?", $targetType)); + } + + return $repository->find($source->id); + } +} diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 5725156..8d507ee 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -4,6 +4,8 @@ use App\Entity\User\User; use App\Entity\User\UserToken; +use App\Repository\User\UserRepository; +use Symfony\Bundle\SecurityBundle\Security; class AuthService { @@ -13,6 +15,8 @@ class AuthService public function __construct( private string $appSecret, + private Security $security, + private UserRepository $userRepository, ) {} /** @@ -40,4 +44,17 @@ public function generateUserToken(User $user, AuthTokenType $type): UserToken return $token; } + + public function getUser(): ?User + { + $loggedInUser = $this->security->getUser(); + + if (!$loggedInUser) { + return null; + } + + return $this->userRepository->findOneBy( + ['username' => $loggedInUser->getUserIdentifier()] + ); + } } diff --git a/src/Service/Project/RewardService.php b/src/Service/Project/RewardService.php new file mode 100644 index 0000000..131c1c7 --- /dev/null +++ b/src/Service/Project/RewardService.php @@ -0,0 +1,34 @@ +getReward(); + + if (!$reward->hasUnits()) { + return $claim; + } + + $available = $reward->getUnitsAvailable(); + + if ($available < 1) { + throw new \Exception('The claimed Reward has no units available'); + } + + $reward->addClaim($claim); + $reward->setUnitsAvailable($available - 1); + + return $claim->setReward($reward); + } +} diff --git a/src/State/ApiResourceStateProcessor.php b/src/State/ApiResourceStateProcessor.php index 8bbfbb5..f2994ce 100644 --- a/src/State/ApiResourceStateProcessor.php +++ b/src/State/ApiResourceStateProcessor.php @@ -42,7 +42,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = public function getEntity(mixed $data, Options $options): object { /** @var object */ - $entity = $this->autoMapper->map($data, $options->getEntityClass(), ['skip_null_values' => true]); + $entity = $this->autoMapper->map($data, $options->getEntityClass()); return $entity; } diff --git a/src/State/EntityStateProcessor.php b/src/State/EntityStateProcessor.php new file mode 100644 index 0000000..6cbe456 --- /dev/null +++ b/src/State/EntityStateProcessor.php @@ -0,0 +1,32 @@ +deleteProcessor->process($data, $operation, $uriVariables, $context); + } + + return $this->persistProcessor->process($data, $operation, $uriVariables, $context); + } +} diff --git a/src/State/Project/ProjectStateProcessor.php b/src/State/Project/ProjectStateProcessor.php index 0934a4e..673686c 100644 --- a/src/State/Project/ProjectStateProcessor.php +++ b/src/State/Project/ProjectStateProcessor.php @@ -2,28 +2,21 @@ namespace App\State\Project; -use ApiPlatform\Doctrine\Common\State\PersistProcessor; -use ApiPlatform\Doctrine\Common\State\RemoveProcessor; -use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Project\ProjectApiResource; use App\Entity\Project\Project; use App\Mapping\AutoMapper; -use App\Repository\User\UserRepository; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\DependencyInjection\Attribute\Autowire; +use App\Service\Auth\AuthService; +use App\State\EntityStateProcessor; +use Symfony\Component\Security\Core\Exception\AuthenticationException; class ProjectStateProcessor implements ProcessorInterface { public function __construct( - #[Autowire(service: RemoveProcessor::class)] - private ProcessorInterface $deleteProcessor, - #[Autowire(service: PersistProcessor::class)] - private ProcessorInterface $persistProcessor, + private EntityStateProcessor $entityStateProcessor, private AutoMapper $autoMapper, - private Security $security, - private UserRepository $userRepository, + private AuthService $authService, ) {} /** @@ -34,22 +27,19 @@ public function __construct( public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { /** @var Project */ - $project = $this->autoMapper->map($data, Project::class, ['skip_null_values' => true]); + $project = $this->autoMapper->map($data, Project::class); - if (!isset($data->id)) { - $user = $this->security->getUser(); - $owner = $this->userRepository->findOneBy(['username' => $user->getUserIdentifier()]); + if (!$project->getId()) { + $owner = $this->authService->getUser(); - $project->setOwner($owner); - } + if (!$owner) { + throw new AuthenticationException(); + } - if ($operation instanceof DeleteOperationInterface) { - $this->deleteProcessor->process($project, $operation, $uriVariables, $context); - - return null; + $project->setOwner($owner); } - $this->persistProcessor->process($project, $operation, $uriVariables, $context); + $project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context); return $this->autoMapper->map($project, $data); } diff --git a/src/State/Project/RewardClaimStateProcessor.php b/src/State/Project/RewardClaimStateProcessor.php new file mode 100644 index 0000000..d3c5a94 --- /dev/null +++ b/src/State/Project/RewardClaimStateProcessor.php @@ -0,0 +1,49 @@ +autoMapper->map($data, RewardClaim::class); + + if (!$claim->getId()) { + $owner = $this->authService->getUser(); + + if (!$owner) { + throw new AuthenticationException(); + } + + $claim->setOwner($owner); + } + + $claim = $this->rewardService->processClaim($claim); + $claim = $this->entityStateProcessor->process($claim, $operation, $uriVariables, $context); + + return $this->autoMapper->map($claim, $data); + } +} diff --git a/src/State/Project/RewardStateProcessor.php b/src/State/Project/RewardStateProcessor.php new file mode 100644 index 0000000..77d978f --- /dev/null +++ b/src/State/Project/RewardStateProcessor.php @@ -0,0 +1,37 @@ +autoMapper->map($data, Reward::class); + + if (!$reward->getId()) { + $reward->setUnitsAvailable($reward->getUnitsTotal()); + } + + $reward = $this->entityStateProcessor->process($reward, $operation, $uriVariables, $context); + + return $this->autoMapper->map($reward, $data); + } +}