diff --git a/migrations/Version20240322113223.php b/migrations/Version20240322113223.php new file mode 100644 index 0000000..d9fc5e5 --- /dev/null +++ b/migrations/Version20240322113223.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE partner_follow (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, partner_id INT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_55FFED0BA76ED395 (user_id), INDEX IDX_55FFED0B9393F8FE (partner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE partner_follow ADD CONSTRAINT FK_55FFED0BA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE partner_follow ADD CONSTRAINT FK_55FFED0B9393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE partner_follow DROP FOREIGN KEY FK_55FFED0BA76ED395'); + $this->addSql('ALTER TABLE partner_follow DROP FOREIGN KEY FK_55FFED0B9393F8FE'); + $this->addSql('DROP TABLE partner_follow'); + } +} diff --git a/src/ApiResource/PartnerFollowApi.php b/src/ApiResource/PartnerFollowApi.php new file mode 100644 index 0000000..ef9fd13 --- /dev/null +++ b/src/ApiResource/PartnerFollowApi.php @@ -0,0 +1,68 @@ + + * @date 12.12.23 + */ + + +namespace App\ApiResource; + +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use App\Entity\Partner; +use App\Entity\PartnerFollow; +use App\Entity\User; +use App\State\EntityClassDtoStateProcessor; +use App\State\EntityToDtoStateProvider; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints\NotBlank; + +#[ApiResource( + shortName: 'PartnerFollow', + operations: [ + new Get( + security: 'is_granted("ROLE_USER")' + ), + new GetCollection( + security: 'is_granted("ROLE_USER")', + ), + new Post( + security: 'is_granted("ROLE_USER")', + ), + new Delete( + security: 'is_granted("ROLE_USER")', + ) + ], + security: 'is_granted("ROLE_USER")', + provider: EntityToDtoStateProvider::class, + processor: EntityClassDtoStateProcessor::class, + stateOptions: new Options(entityClass: PartnerFollow::class), +)] +#[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'contact' => 'exact'])] +class PartnerFollowApi +{ + #[ApiProperty(readable: false, writable: false, identifier: true)] + public ?int $id = null; + + #[ApiProperty(writable: false)] + public ?UserApi $user = null; + + #[ApiProperty(writable: false)] + public ?string $userName = null; + + #[NotBlank] + public ?PartnerApi $partner = null; + + #[ApiProperty(writable: false)] + public ?string $partnerName = null; + + #[ApiProperty(writable: false)] + public ?\DateTimeImmutable $createdAt = null; +} \ No newline at end of file diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index e30acf4..8a6864a 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -10,6 +10,7 @@ use App\Factory\MediaObjectProductFactory; use App\Factory\MediaObjectContactFactory; use App\Factory\MediaObjectUserFactory; use App\Factory\PartnerFactory; +use App\Factory\PartnerFollowFactory; use App\Factory\PostingFactory; use App\Factory\ProductFactory; use App\Factory\SaleFactory; @@ -75,5 +76,6 @@ class AppFixtures extends Fixture TaskFactory::createMany(50); TaskNoteFactory::createMany(100); DocumentObjectFactory::createMany(50); + PartnerFollowFactory::createMany(100); } } diff --git a/src/Entity/DocumentObject.php b/src/Entity/DocumentObject.php index f519736..ecc47fd 100644 --- a/src/Entity/DocumentObject.php +++ b/src/Entity/DocumentObject.php @@ -6,6 +6,8 @@ namespace App\Entity; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; @@ -67,6 +69,7 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; normalizationContext: ['groups' => ['document_object:read']], security: 'is_granted("ROLE_USER")', )] +#[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'product' => 'exact'])] class DocumentObject { #[ORM\Id, ORM\Column, ORM\GeneratedValue] diff --git a/src/Entity/Partner.php b/src/Entity/Partner.php index cc9f2b8..f51c381 100644 --- a/src/Entity/Partner.php +++ b/src/Entity/Partner.php @@ -60,6 +60,9 @@ class Partner #[ORM\OneToMany(mappedBy: 'partner', targetEntity: DocumentObject::class)] private Collection $documentObjects; + #[ORM\OneToMany(mappedBy: 'partner', targetEntity: PartnerFollow::class)] + private Collection $partnerFollows; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -67,6 +70,7 @@ class Partner $this->postings = new ArrayCollection(); $this->sales = new ArrayCollection(); $this->documentObjects = new ArrayCollection(); + $this->partnerFollows = new ArrayCollection(); } public function getId(): ?int @@ -274,4 +278,34 @@ class Partner return $this; } + /** + * @return Collection + */ + public function getPartnerFollows(): Collection + { + return $this->partnerFollows; + } + + public function addPartnerFollow(PartnerFollow $partnerFollow): static + { + if (!$this->partnerFollows->contains($partnerFollow)) { + $this->partnerFollows->add($partnerFollow); + $partnerFollow->setPartner($this); + } + + return $this; + } + + public function removePartnerFollow(PartnerFollow $partnerFollow): static + { + if ($this->partnerFollows->removeElement($partnerFollow)) { + // set the owning side to null (unless already changed) + if ($partnerFollow->getPartner() === $this) { + $partnerFollow->setPartner(null); + } + } + + return $this; + } + } diff --git a/src/Entity/PartnerFollow.php b/src/Entity/PartnerFollow.php new file mode 100644 index 0000000..57f0a61 --- /dev/null +++ b/src/Entity/PartnerFollow.php @@ -0,0 +1,74 @@ +user = $user; + $this->partner = $partner; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): static + { + $this->user = $user; + + return $this; + } + + public function getPartner(): ?Partner + { + return $this->partner; + } + + public function setPartner(?Partner $partner): static + { + $this->partner = $partner; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index f2a63e7..0430bfd 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -55,6 +55,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Sale::class)] private Collection $sales; + #[ORM\OneToMany(mappedBy: 'user', targetEntity: PartnerFollow::class)] + private Collection $partnerFollows; + public function __construct() { @@ -63,6 +66,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->active = true; $this->comments = new ArrayCollection(); $this->sales = new ArrayCollection(); + $this->partnerFollows = new ArrayCollection(); } public function getId(): ?int @@ -280,4 +284,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection + */ + public function getPartnerFollows(): Collection + { + return $this->partnerFollows; + } + + public function addPartnerFollow(PartnerFollow $partnerFollow): static + { + if (!$this->partnerFollows->contains($partnerFollow)) { + $this->partnerFollows->add($partnerFollow); + $partnerFollow->setUser($this); + } + + return $this; + } + + public function removePartnerFollow(PartnerFollow $partnerFollow): static + { + if ($this->partnerFollows->removeElement($partnerFollow)) { + // set the owning side to null (unless already changed) + if ($partnerFollow->getUser() === $this) { + $partnerFollow->setUser(null); + } + } + + return $this; + } } diff --git a/src/Factory/PartnerFollowFactory.php b/src/Factory/PartnerFollowFactory.php new file mode 100644 index 0000000..d9c45df --- /dev/null +++ b/src/Factory/PartnerFollowFactory.php @@ -0,0 +1,70 @@ + + * + * @method PartnerFollow|Proxy create(array|callable $attributes = []) + * @method static PartnerFollow|Proxy createOne(array $attributes = []) + * @method static PartnerFollow|Proxy find(object|array|mixed $criteria) + * @method static PartnerFollow|Proxy findOrCreate(array $attributes) + * @method static PartnerFollow|Proxy first(string $sortedField = 'id') + * @method static PartnerFollow|Proxy last(string $sortedField = 'id') + * @method static PartnerFollow|Proxy random(array $attributes = []) + * @method static PartnerFollow|Proxy randomOrCreate(array $attributes = []) + * @method static PartnerFollowRepository|RepositoryProxy repository() + * @method static PartnerFollow[]|Proxy[] all() + * @method static PartnerFollow[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static PartnerFollow[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static PartnerFollow[]|Proxy[] findBy(array $attributes) + * @method static PartnerFollow[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static PartnerFollow[]|Proxy[] randomSet(int $number, array $attributes = []) + */ +final class PartnerFollowFactory extends ModelFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + parent::__construct(); + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function getDefaults(): array + { + return [ + 'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), + 'partner' => PartnerFactory::random(), + 'user' => UserFactory::random(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): self + { + return $this + // ->afterInstantiate(function(PartnerFollow $partnerFollow): void {}) + ; + } + + protected static function getClass(): string + { + return PartnerFollow::class; + } +} diff --git a/src/Mapper/CommentEntityToApiMapper.php b/src/Mapper/CommentEntityToApiMapper.php index c9227a8..423ff5c 100644 --- a/src/Mapper/CommentEntityToApiMapper.php +++ b/src/Mapper/CommentEntityToApiMapper.php @@ -6,7 +6,6 @@ use App\ApiResource\CommentApi; use App\ApiResource\UserApi; use App\ApiResource\PostingApi; use App\Entity\Comment; -use Symfony\Bundle\SecurityBundle\Security; use Symfonycasts\MicroMapper\AsMapper; use Symfonycasts\MicroMapper\MapperInterface; use Symfonycasts\MicroMapper\MicroMapperInterface; diff --git a/src/Mapper/PartnerFollowApiToEntityMapper.php b/src/Mapper/PartnerFollowApiToEntityMapper.php new file mode 100644 index 0000000..59c285c --- /dev/null +++ b/src/Mapper/PartnerFollowApiToEntityMapper.php @@ -0,0 +1,62 @@ +id) { + $entity = $this->repository->find($dto->id); + } else { + $user = $this->security->getUser(); + assert($user instanceof User); + if ($dto->partner === null) { + throw new \Exception('Partner missing'); + } + $partner = $this->microMapper->map($dto->partner, Partner::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + assert($partner instanceof Partner); + $entity = new PartnerFollow($user, $partner); + } + + if (!$entity) { + throw new \Exception('PartnerFollow not found'); + } + + return $entity; + } + + public function populate(object $from, object $to, array $context): object + { + $dto = $from; + assert($dto instanceof PartnerFollowApi); + $entity = $to; + assert($entity instanceof PartnerFollow); + return $entity; + } +} diff --git a/src/Mapper/PartnerFollowEntityToApiMapper.php b/src/Mapper/PartnerFollowEntityToApiMapper.php new file mode 100644 index 0000000..13f79ca --- /dev/null +++ b/src/Mapper/PartnerFollowEntityToApiMapper.php @@ -0,0 +1,54 @@ +id = $entity->getId(); + + return $dto; + } + + public function populate(object $from, object $to, array $context): object + { + $entity = $from; + $dto = $to; + assert($entity instanceof PartnerFollow); + assert($dto instanceof PartnerFollowApi); + + $dto->user = $this->microMapper->map($entity->getUser(), UserApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->userName = $entity->getUser()?->getFirstName()." ".$entity->getUser()?->getLastName(); + + $dto->partner = $this->microMapper->map($entity->getPartner(), PartnerApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->partnerName = $entity->getPartner()?->getName(); + + $dto->createdAt = $entity->getCreatedAt(); + + return $dto; + } +} diff --git a/src/Repository/PartnerFollowRepository.php b/src/Repository/PartnerFollowRepository.php new file mode 100644 index 0000000..7bbab4e --- /dev/null +++ b/src/Repository/PartnerFollowRepository.php @@ -0,0 +1,48 @@ + + * + * @method PartnerFollow|null find($id, $lockMode = null, $lockVersion = null) + * @method PartnerFollow|null findOneBy(array $criteria, array $orderBy = null) + * @method PartnerFollow[] findAll() + * @method PartnerFollow[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PartnerFollowRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, PartnerFollow::class); + } + + // /** + // * @return PartnerFollow[] Returns an array of PartnerFollow objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('p') + // ->andWhere('p.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('p.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?PartnerFollow + // { + // return $this->createQueryBuilder('p') + // ->andWhere('p.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/tests/Functional/PartnerFollowResourceTest.php b/tests/Functional/PartnerFollowResourceTest.php new file mode 100644 index 0000000..3b7c6c9 --- /dev/null +++ b/tests/Functional/PartnerFollowResourceTest.php @@ -0,0 +1,83 @@ + + * @date 12.12.23 + */ + + +namespace App\Tests\Functional; + +use App\Factory\MediaObjectLogoFactory; +use App\Factory\PartnerFactory; +use App\Factory\PartnerFollowFactory; +use App\Factory\UserFactory; +use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Browser\Test\HasBrowser; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +class PartnerFollowResourceTest extends KernelTestCase +{ + use HasBrowser; + use ResetDatabase; + use Factories; + + private JWTTokenManagerInterface $JWTManager; + + protected function setUp(): void + { + parent::setUp(); + $this->JWTManager = self::getContainer()->get('lexik_jwt_authentication.jwt_manager'); + } + + public function testPostPartnerFollow(): void + { + $user = UserFactory::createOne( + [ + 'firstName' => 'Peter', + 'lastName' => 'Test', + 'password' => 'test', + 'email' => 'peter@test.de', + ] + ); + MediaObjectLogoFactory::createOne(); + $partner = PartnerFactory::createOne(); + PartnerFollowFactory::createOne(); + + $token = $this->JWTManager->create($user->object()); + + $this->browser() + ->get('/api/partner_follows', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ], + ]) + ->assertSuccessful() + ->assertJsonMatches('"hydra:totalItems"', 1) + ; + + + $this->browser() + ->post('/api/partner_follows' , [ + 'json' => [ + 'partner' => '/api/partners/' . $partner->getId(), + ], + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ] + ]) + ->assertSuccessful() + ; + + $this->browser() + ->get('/api/partner_follows', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ], + ]) + ->assertSuccessful() + ->assertJsonMatches('"hydra:totalItems"', 2) + ; + } +} \ No newline at end of file