From 18164340ff5014a7cb40d4b9b6420876d2347bc4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 8 Apr 2024 11:23:18 +0200 Subject: [PATCH] user product --- ...04115740.php => Version20240408091731.php} | 8 +- src/ApiResource/CommentApi.php | 1 - src/ApiResource/PartnerFollowApi.php | 10 ++- src/ApiResource/UserProductApi.php | 72 ++++++++++++++++++ src/DataFixtures/AppFixtures.php | 19 +++++ src/Entity/Product.php | 34 +++++++++ src/Entity/User.php | 34 +++++++++ src/Entity/UserProduct.php | 74 +++++++++++++++++++ src/Factory/UserProductFactory.php | 70 ++++++++++++++++++ src/Mapper/UserProductApiToEntityMapper.php | 63 ++++++++++++++++ src/Mapper/UserProductEntityToApiMapper.php | 54 ++++++++++++++ src/Repository/UserProductRepository.php | 48 ++++++++++++ 12 files changed, 483 insertions(+), 4 deletions(-) rename migrations/{Version20240404115740.php => Version20240408091731.php} (94%) create mode 100644 src/ApiResource/UserProductApi.php create mode 100644 src/Entity/UserProduct.php create mode 100644 src/Factory/UserProductFactory.php create mode 100644 src/Mapper/UserProductApiToEntityMapper.php create mode 100644 src/Mapper/UserProductEntityToApiMapper.php create mode 100644 src/Repository/UserProductRepository.php diff --git a/migrations/Version20240404115740.php b/migrations/Version20240408091731.php similarity index 94% rename from migrations/Version20240404115740.php rename to migrations/Version20240408091731.php index 505c886..d4f0a4f 100644 --- a/migrations/Version20240404115740.php +++ b/migrations/Version20240408091731.php @@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration; /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20240404115740 extends AbstractMigration +final class Version20240408091731 extends AbstractMigration { public function getDescription(): string { @@ -35,6 +35,7 @@ final class Version20240404115740 extends AbstractMigration $this->addSql('CREATE TABLE task (id INT AUTO_INCREMENT NOT NULL, created_by_id INT NOT NULL, assigned_to_id INT NOT NULL, partner_id INT DEFAULT NULL, contact_id INT DEFAULT NULL, headline VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, due_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', prio VARCHAR(255) NOT NULL, completed TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_527EDB25B03A8386 (created_by_id), INDEX IDX_527EDB25F4BD7827 (assigned_to_id), INDEX IDX_527EDB259393F8FE (partner_id), INDEX IDX_527EDB25E7A1254A (contact_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE task_note (id INT AUTO_INCREMENT NOT NULL, owner_id INT NOT NULL, task_id INT NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_BC0E6E6F7E3C61F9 (owner_id), INDEX IDX_BC0E6E6F8DB60186 (task_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, image_id INT DEFAULT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, roles JSON NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) NOT NULL, active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), INDEX IDX_8D93D6493DA5256D (image_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE user_product (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, product_id INT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_8B471AA7A76ED395 (user_id), INDEX IDX_8B471AA74584665A (product_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C7E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)'); $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C9AE985F6 FOREIGN KEY (posting_id) REFERENCES posting (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE contact ADD CONSTRAINT FK_4C62E6389393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); @@ -71,6 +72,8 @@ final class Version20240404115740 extends AbstractMigration $this->addSql('ALTER TABLE task_note ADD CONSTRAINT FK_BC0E6E6F7E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE task_note ADD CONSTRAINT FK_BC0E6E6F8DB60186 FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE'); $this->addSql('ALTER TABLE `user` ADD CONSTRAINT FK_8D93D6493DA5256D FOREIGN KEY (image_id) REFERENCES media_object (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE user_product ADD CONSTRAINT FK_8B471AA7A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE user_product ADD CONSTRAINT FK_8B471AA74584665A FOREIGN KEY (product_id) REFERENCES product (id)'); } public function down(Schema $schema): void @@ -112,6 +115,8 @@ final class Version20240404115740 extends AbstractMigration $this->addSql('ALTER TABLE task_note DROP FOREIGN KEY FK_BC0E6E6F7E3C61F9'); $this->addSql('ALTER TABLE task_note DROP FOREIGN KEY FK_BC0E6E6F8DB60186'); $this->addSql('ALTER TABLE `user` DROP FOREIGN KEY FK_8D93D6493DA5256D'); + $this->addSql('ALTER TABLE user_product DROP FOREIGN KEY FK_8B471AA7A76ED395'); + $this->addSql('ALTER TABLE user_product DROP FOREIGN KEY FK_8B471AA74584665A'); $this->addSql('DROP TABLE comment'); $this->addSql('DROP TABLE contact'); $this->addSql('DROP TABLE contact_partner_product'); @@ -127,5 +132,6 @@ final class Version20240404115740 extends AbstractMigration $this->addSql('DROP TABLE task'); $this->addSql('DROP TABLE task_note'); $this->addSql('DROP TABLE `user`'); + $this->addSql('DROP TABLE user_product'); } } diff --git a/src/ApiResource/CommentApi.php b/src/ApiResource/CommentApi.php index 0981d2a..6736300 100644 --- a/src/ApiResource/CommentApi.php +++ b/src/ApiResource/CommentApi.php @@ -71,7 +71,6 @@ class CommentApi implements OwnerInterface public function getOwner(): ?UserApi { - //dd($this->owner); return $this->owner; } } \ No newline at end of file diff --git a/src/ApiResource/PartnerFollowApi.php b/src/ApiResource/PartnerFollowApi.php index fff46b0..147e8fc 100644 --- a/src/ApiResource/PartnerFollowApi.php +++ b/src/ApiResource/PartnerFollowApi.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\ApiResource; use App\Entity\Partner; use App\Entity\PartnerFollow; use App\Entity\User; +use App\Interface\OwnerInterface; use App\State\EntityClassDtoStateProcessor; use App\State\EntityToDtoStateProvider; use ApiPlatform\Metadata\Delete; @@ -37,7 +38,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; security: 'is_granted("ROLE_USER")', ), new Delete( - security: 'is_granted("ROLE_USER")', + security: 'is_granted("EDIT", object)', ) ], security: 'is_granted("ROLE_USER")', @@ -46,7 +47,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; stateOptions: new Options(entityClass: PartnerFollow::class), )] #[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'user' => 'exact'])] -class PartnerFollowApi +class PartnerFollowApi implements OwnerInterface { #[ApiProperty(readable: false, writable: false, identifier: true)] public ?int $id = null; @@ -65,4 +66,9 @@ class PartnerFollowApi #[ApiProperty(writable: false)] public ?\DateTimeImmutable $createdAt = null; + + public function getOwner(): ?UserApi + { + return $this->user; + } } \ No newline at end of file diff --git a/src/ApiResource/UserProductApi.php b/src/ApiResource/UserProductApi.php new file mode 100644 index 0000000..261dd4b --- /dev/null +++ b/src/ApiResource/UserProductApi.php @@ -0,0 +1,72 @@ + + * @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\PartnerProduct; +use App\Entity\UserProduct; +use App\Interface\OwnerInterface; +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\Validator\Constraints\NotBlank; + +#[ApiResource( + shortName: 'UserProduct', + 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("EDIT", object)', + ) + ], + security: 'is_granted("ROLE_USER")', + provider: EntityToDtoStateProvider::class, + processor: EntityClassDtoStateProcessor::class, + stateOptions: new Options(entityClass: UserProduct::class), +)] +#[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'product' => 'exact'])] +class UserProductApi implements OwnerInterface +{ + #[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 ?ProductApi $product = null; + + #[ApiProperty(writable: false)] + public ?string $productName = null; + + #[ApiProperty(writable: false)] + public ?\DateTimeImmutable $createdAt = null; + + public function getOwner(): ?UserApi + { + return $this->user; + } +} \ No newline at end of file diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 81215cb..99f9dc9 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -22,6 +22,7 @@ use App\Factory\SaleFactory; use App\Factory\TaskFactory; use App\Factory\TaskNoteFactory; use App\Factory\UserFactory; +use App\Factory\UserProductFactory; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Symfony\Component\HttpKernel\KernelInterface; @@ -189,6 +190,24 @@ class AppFixtures extends Fixture } } + $users = UserFactory::all(); + foreach ($users as $user) { + $productsPicked = []; + for ($i = 0; $i < 3; $i++) { + $product = $products[array_rand($products)]; + $productsPicked[$product->getId()] = $product; + } + + foreach ($productsPicked as $item) { + UserProductFactory::createOne( + [ + 'user' => $user, + 'product' => $item + ] + ); + } + } + } } diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 6bfec2d..0b39f67 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -45,6 +45,9 @@ class Product #[ORM\OneToMany(mappedBy: 'product', targetEntity: PartnerProduct::class)] private Collection $partnerProducts; + #[ORM\OneToMany(mappedBy: 'product', targetEntity: UserProduct::class)] + private Collection $userProducts; + public function __construct(User $createdBy) { $this->createdBy = $createdBy; @@ -53,6 +56,7 @@ class Product $this->documentObjects = new ArrayCollection(); $this->postings = new ArrayCollection(); $this->partnerProducts = new ArrayCollection(); + $this->userProducts = new ArrayCollection(); } public function getId(): ?int @@ -225,4 +229,34 @@ class Product return $this; } + + /** + * @return Collection + */ + public function getUserProducts(): Collection + { + return $this->userProducts; + } + + public function addUserProduct(UserProduct $userProduct): static + { + if (!$this->userProducts->contains($userProduct)) { + $this->userProducts->add($userProduct); + $userProduct->setProduct($this); + } + + return $this; + } + + public function removeUserProduct(UserProduct $userProduct): static + { + if ($this->userProducts->removeElement($userProduct)) { + // set the owning side to null (unless already changed) + if ($userProduct->getProduct() === $this) { + $userProduct->setProduct(null); + } + } + + return $this; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index e17fdab..9df6cf1 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -61,6 +61,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(mappedBy: 'createdBy', targetEntity: Document::class)] private Collection $documentObjects; + #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserProduct::class)] + private Collection $userProducts; + public function __construct() { @@ -71,6 +74,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->sales = new ArrayCollection(); $this->partnerFollows = new ArrayCollection(); $this->documentObjects = new ArrayCollection(); + $this->userProducts = new ArrayCollection(); } public function getId(): ?int @@ -348,4 +352,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection + */ + public function getUserProducts(): Collection + { + return $this->userProducts; + } + + public function addUserProduct(UserProduct $userProduct): static + { + if (!$this->userProducts->contains($userProduct)) { + $this->userProducts->add($userProduct); + $userProduct->setUser($this); + } + + return $this; + } + + public function removeUserProduct(UserProduct $userProduct): static + { + if ($this->userProducts->removeElement($userProduct)) { + // set the owning side to null (unless already changed) + if ($userProduct->getUser() === $this) { + $userProduct->setUser(null); + } + } + + return $this; + } } diff --git a/src/Entity/UserProduct.php b/src/Entity/UserProduct.php new file mode 100644 index 0000000..bd669fe --- /dev/null +++ b/src/Entity/UserProduct.php @@ -0,0 +1,74 @@ +user = $user; + $this->product = $product; + $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 getProduct(): ?Product + { + return $this->product; + } + + public function setProduct(?Product $product): static + { + $this->product = $product; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Factory/UserProductFactory.php b/src/Factory/UserProductFactory.php new file mode 100644 index 0000000..15f4693 --- /dev/null +++ b/src/Factory/UserProductFactory.php @@ -0,0 +1,70 @@ + + * + * @method UserProduct|Proxy create(array|callable $attributes = []) + * @method static UserProduct|Proxy createOne(array $attributes = []) + * @method static UserProduct|Proxy find(object|array|mixed $criteria) + * @method static UserProduct|Proxy findOrCreate(array $attributes) + * @method static UserProduct|Proxy first(string $sortedField = 'id') + * @method static UserProduct|Proxy last(string $sortedField = 'id') + * @method static UserProduct|Proxy random(array $attributes = []) + * @method static UserProduct|Proxy randomOrCreate(array $attributes = []) + * @method static UserProductRepository|RepositoryProxy repository() + * @method static UserProduct[]|Proxy[] all() + * @method static UserProduct[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static UserProduct[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static UserProduct[]|Proxy[] findBy(array $attributes) + * @method static UserProduct[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static UserProduct[]|Proxy[] randomSet(int $number, array $attributes = []) + */ +final class UserProductFactory 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()), + 'user' => UserFactory::new(), + 'product' => ProductFactory::new(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): self + { + return $this + // ->afterInstantiate(function(UserProduct $userProduct): void {}) + ; + } + + protected static function getClass(): string + { + return UserProduct::class; + } +} diff --git a/src/Mapper/UserProductApiToEntityMapper.php b/src/Mapper/UserProductApiToEntityMapper.php new file mode 100644 index 0000000..a19de44 --- /dev/null +++ b/src/Mapper/UserProductApiToEntityMapper.php @@ -0,0 +1,63 @@ +id) { + $entity = $this->repository->find($dto->id); + } else { + $user = $this->security->getUser(); + assert($user instanceof User); + + if ($dto->product === null) { + throw new \Exception('Product missing'); + } + $product = $this->microMapper->map($dto->product, Product::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + assert($product instanceof Product); + $entity = new UserProduct($user, $product); + } + + if (!$entity) { + throw new \Exception('UserProduct not found'); + } + + return $entity; + } + + public function populate(object $from, object $to, array $context): object + { + $dto = $from; + assert($dto instanceof UserProductApi); + $entity = $to; + assert($entity instanceof UserProduct); + return $entity; + } +} diff --git a/src/Mapper/UserProductEntityToApiMapper.php b/src/Mapper/UserProductEntityToApiMapper.php new file mode 100644 index 0000000..b8bbe35 --- /dev/null +++ b/src/Mapper/UserProductEntityToApiMapper.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 UserProduct); + assert($dto instanceof UserProductApi); + + $dto->user = $this->microMapper->map($entity->getUser(), UserApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->userName = $entity->getUser()?->getFirstName()." ".$entity->getUser()?->getLastName(); + + $dto->product = $this->microMapper->map($entity->getProduct(), ProductApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->productName = $entity->getProduct()?->getName(); + + $dto->createdAt = $entity->getCreatedAt(); + + return $dto; + } +} diff --git a/src/Repository/UserProductRepository.php b/src/Repository/UserProductRepository.php new file mode 100644 index 0000000..1e76914 --- /dev/null +++ b/src/Repository/UserProductRepository.php @@ -0,0 +1,48 @@ + + * + * @method UserProduct|null find($id, $lockMode = null, $lockVersion = null) + * @method UserProduct|null findOneBy(array $criteria, array $orderBy = null) + * @method UserProduct[] findAll() + * @method UserProduct[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserProductRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserProduct::class); + } + + // /** + // * @return UserProduct[] Returns an array of UserProduct objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?UserProduct + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +}