| @@ -6,6 +6,7 @@ | |||
| /config/secrets/prod/prod.decrypt.private.php | |||
| /public/bundles/ | |||
| /public/media/ | |||
| /public/document/ | |||
| /var/ | |||
| /vendor/ | |||
| ###< symfony/framework-bundle ### | |||
| @@ -11,8 +11,8 @@ vich_uploader: | |||
| inject_on_load: false | |||
| delete_on_update: true | |||
| delete_on_remove: true | |||
| download: | |||
| uri_prefix: /download | |||
| upload_destination: '%kernel.project_dir%/public/download' | |||
| document_object: | |||
| uri_prefix: /document | |||
| upload_destination: '%kernel.project_dir%/public/document' | |||
| # Will rename uploaded files using a uniqueid as a prefix. | |||
| namer: Vich\UploaderBundle\Naming\SmartUniqueNamer | |||
| @@ -0,0 +1,35 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace DoctrineMigrations; | |||
| use Doctrine\DBAL\Schema\Schema; | |||
| use Doctrine\Migrations\AbstractMigration; | |||
| /** | |||
| * Auto-generated Migration: Please modify to your needs! | |||
| */ | |||
| final class Version20240321094704 extends AbstractMigration | |||
| { | |||
| public function getDescription(): string | |||
| { | |||
| return ''; | |||
| } | |||
| public function up(Schema $schema): void | |||
| { | |||
| // this up() migration is auto-generated, please modify it to your needs | |||
| $this->addSql('CREATE TABLE document_object (id INT AUTO_INCREMENT NOT NULL, partner_id INT DEFAULT NULL, product_id INT DEFAULT NULL, file_path VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_16CF1A8A9393F8FE (partner_id), INDEX IDX_16CF1A8A4584665A (product_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); | |||
| $this->addSql('ALTER TABLE document_object ADD CONSTRAINT FK_16CF1A8A9393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id)'); | |||
| $this->addSql('ALTER TABLE document_object ADD CONSTRAINT FK_16CF1A8A4584665A FOREIGN KEY (product_id) REFERENCES product (id)'); | |||
| } | |||
| public function down(Schema $schema): void | |||
| { | |||
| // this down() migration is auto-generated, please modify it to your needs | |||
| $this->addSql('ALTER TABLE document_object DROP FOREIGN KEY FK_16CF1A8A9393F8FE'); | |||
| $this->addSql('ALTER TABLE document_object DROP FOREIGN KEY FK_16CF1A8A4584665A'); | |||
| $this->addSql('DROP TABLE document_object'); | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace DoctrineMigrations; | |||
| use Doctrine\DBAL\Schema\Schema; | |||
| use Doctrine\Migrations\AbstractMigration; | |||
| /** | |||
| * Auto-generated Migration: Please modify to your needs! | |||
| */ | |||
| final class Version20240322113223 extends AbstractMigration | |||
| { | |||
| public function getDescription(): string | |||
| { | |||
| return ''; | |||
| } | |||
| public function up(Schema $schema): void | |||
| { | |||
| // this up() migration is auto-generated, please modify it to your needs | |||
| $this->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'); | |||
| } | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @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; | |||
| } | |||
| @@ -90,6 +90,7 @@ class PostingApi implements OwnerInterface | |||
| * @var $comments array<int, CommentApi> | |||
| */ | |||
| #[ApiProperty( | |||
| writable: false, | |||
| readableLink: true, | |||
| writableLink: true, | |||
| builtinTypes: [ | |||
| @@ -0,0 +1,59 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @date 25.01.24 | |||
| */ | |||
| namespace App\Controller; | |||
| use ApiPlatform\Api\IriConverterInterface; | |||
| use App\Entity\DocumentObject; | |||
| use App\Entity\Partner; | |||
| use App\Entity\Product; | |||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |||
| use Symfony\Component\HttpFoundation\Request; | |||
| use Symfony\Component\HttpKernel\Attribute\AsController; | |||
| use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| #[AsController] | |||
| final class CreateDocumentObjectAction extends AbstractController | |||
| { | |||
| public function __invoke( | |||
| Request $request, | |||
| IriConverterInterface $iriConverter, | |||
| MicroMapperInterface $microMapper | |||
| ): DocumentObject { | |||
| $uploadedFile = $request->files->get('file'); | |||
| if (!$uploadedFile) { | |||
| throw new BadRequestHttpException('"file" is required'); | |||
| } | |||
| $documentObject = new DocumentObject(); | |||
| $documentObject->file = $uploadedFile; | |||
| $partnerIri = $request->request->get('partner'); | |||
| if ($partnerIri !== null) { | |||
| try { | |||
| $partnerApi = $iriConverter->getResourceFromIri($partnerIri); | |||
| } catch (\Exception $exception) { | |||
| throw new BadRequestHttpException('invalid partner iri'); | |||
| } | |||
| $documentObject->setPartner($microMapper->map($partnerApi, Partner::class)); | |||
| } | |||
| $productIri = $request->request->get('product'); | |||
| if ($productIri !== null) { | |||
| try { | |||
| $productApi = $iriConverter->getResourceFromIri($productIri); | |||
| } catch (\Exception $exception) { | |||
| throw new BadRequestHttpException('invalid product iri'); | |||
| } | |||
| $documentObject->setProduct($microMapper->map($productApi, Product::class)); | |||
| } | |||
| return $documentObject; | |||
| } | |||
| } | |||
| @@ -4,11 +4,13 @@ namespace App\DataFixtures; | |||
| use App\Factory\CommentFactory; | |||
| use App\Factory\ContactFactory; | |||
| use App\Factory\DocumentObjectFactory; | |||
| use App\Factory\MediaObjectLogoFactory; | |||
| 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; | |||
| @@ -73,5 +75,7 @@ class AppFixtures extends Fixture | |||
| MediaObjectProductFactory::createMany(50); | |||
| TaskFactory::createMany(50); | |||
| TaskNoteFactory::createMany(100); | |||
| DocumentObjectFactory::createMany(50); | |||
| PartnerFollowFactory::createMany(100); | |||
| } | |||
| } | |||
| @@ -0,0 +1,152 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @date 25.01.24 | |||
| */ | |||
| 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; | |||
| use ApiPlatform\Metadata\Get; | |||
| use ApiPlatform\Metadata\GetCollection; | |||
| use ApiPlatform\Metadata\Post; | |||
| use ApiPlatform\OpenApi\Model; | |||
| use App\Controller\CreateDocumentObjectAction; | |||
| use Doctrine\ORM\Mapping as ORM; | |||
| use Symfony\Component\HttpFoundation\File\File; | |||
| use Symfony\Component\Serializer\Annotation\Groups; | |||
| use Symfony\Component\Validator\Constraints as Assert; | |||
| use Vich\UploaderBundle\Mapping\Annotation as Vich; | |||
| #[Vich\Uploadable] | |||
| #[ORM\Entity] | |||
| #[ApiResource( | |||
| shortName: 'Document', | |||
| types: ['https://schema.org/MediaObject'], | |||
| operations: [ | |||
| new Get(), | |||
| new GetCollection(), | |||
| new Post( | |||
| controller: CreateDocumentObjectAction::class, | |||
| openapi: new Model\Operation( | |||
| requestBody: new Model\RequestBody( | |||
| content: new \ArrayObject([ | |||
| 'multipart/form-data' => [ | |||
| 'schema' => [ | |||
| 'type' => 'object', | |||
| 'properties' => [ | |||
| 'file' => [ | |||
| 'type' => 'string', | |||
| 'format' => 'binary' | |||
| ], | |||
| 'partner' => [ | |||
| 'type' => 'string', | |||
| 'format' => 'iri_reference', | |||
| 'example' => 'https://example.com/' | |||
| ], | |||
| 'product' => [ | |||
| 'type' => 'string', | |||
| 'format' => 'iri_reference', | |||
| 'example' => 'https://example.com/' | |||
| ] | |||
| ], | |||
| ], | |||
| ], | |||
| ]) | |||
| ) | |||
| ), | |||
| validationContext: ['groups' => ['Default', 'document_object_create']], | |||
| deserialize: false | |||
| ), | |||
| new Delete( | |||
| // controller: DeleteMediaObjectAction::class | |||
| ), | |||
| ], | |||
| 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] | |||
| private ?int $id = null; | |||
| #[ApiProperty(types: ['https://schema.org/contentUrl'])] | |||
| #[Groups(['document_object:read'])] | |||
| public ?string $contentUrl = null; | |||
| #[Vich\UploadableField(mapping: 'document_object', fileNameProperty: 'filePath')] | |||
| #[Assert\NotNull(groups: ['document_object_create'])] | |||
| public ?File $file = null; | |||
| #[ORM\Column(nullable: true)] | |||
| public ?string $filePath = null; | |||
| #[ORM\Column] | |||
| private ?\DateTimeImmutable $createdAt = null; | |||
| #[ORM\ManyToOne(inversedBy: 'documentObjects')] | |||
| private ?Partner $partner = null; | |||
| #[ORM\ManyToOne(inversedBy: 'documentObjects')] | |||
| private ?Product $product = null; | |||
| public function __construct() | |||
| { | |||
| $this->createdAt = new \DateTimeImmutable(); | |||
| } | |||
| public function getId(): ?int | |||
| { | |||
| return $this->id; | |||
| } | |||
| public function getContentUrl(): ?string | |||
| { | |||
| return $this->contentUrl; | |||
| } | |||
| public function getFile(): ?File | |||
| { | |||
| return $this->file; | |||
| } | |||
| public function getFilePath(): ?string | |||
| { | |||
| return $this->filePath; | |||
| } | |||
| public function getCreatedAt(): ?\DateTimeImmutable | |||
| { | |||
| return $this->createdAt; | |||
| } | |||
| public function getPartner(): ?Partner | |||
| { | |||
| return $this->partner; | |||
| } | |||
| public function setPartner(?Partner $partner): static | |||
| { | |||
| $this->partner = $partner; | |||
| return $this; | |||
| } | |||
| public function getProduct(): ?Product | |||
| { | |||
| return $this->product; | |||
| } | |||
| public function setProduct(?Product $product): static | |||
| { | |||
| $this->product = $product; | |||
| return $this; | |||
| } | |||
| } | |||
| @@ -57,12 +57,20 @@ class Partner | |||
| #[ORM\OneToMany(mappedBy: 'partner', targetEntity: Sale::class)] | |||
| private Collection $sales; | |||
| #[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(); | |||
| $this->contacts = new ArrayCollection(); | |||
| $this->postings = new ArrayCollection(); | |||
| $this->sales = new ArrayCollection(); | |||
| $this->documentObjects = new ArrayCollection(); | |||
| $this->partnerFollows = new ArrayCollection(); | |||
| } | |||
| public function getId(): ?int | |||
| @@ -240,4 +248,64 @@ class Partner | |||
| return $this; | |||
| } | |||
| /** | |||
| * @return Collection<int, DocumentObject> | |||
| */ | |||
| public function getDocumentObjects(): Collection | |||
| { | |||
| return $this->documentObjects; | |||
| } | |||
| public function addDocumentObject(DocumentObject $documentObject): static | |||
| { | |||
| if (!$this->documentObjects->contains($documentObject)) { | |||
| $this->documentObjects->add($documentObject); | |||
| $documentObject->setPartner($this); | |||
| } | |||
| return $this; | |||
| } | |||
| public function removeDocumentObject(DocumentObject $documentObject): static | |||
| { | |||
| if ($this->documentObjects->removeElement($documentObject)) { | |||
| // set the owning side to null (unless already changed) | |||
| if ($documentObject->getPartner() === $this) { | |||
| $documentObject->setPartner(null); | |||
| } | |||
| } | |||
| return $this; | |||
| } | |||
| /** | |||
| * @return Collection<int, PartnerFollow> | |||
| */ | |||
| 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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| <?php | |||
| namespace App\Entity; | |||
| use App\Repository\PartnerFollowRepository; | |||
| use Doctrine\ORM\Mapping as ORM; | |||
| #[ORM\Entity(repositoryClass: PartnerFollowRepository::class)] | |||
| class PartnerFollow | |||
| { | |||
| #[ORM\Id] | |||
| #[ORM\GeneratedValue] | |||
| #[ORM\Column] | |||
| private ?int $id = null; | |||
| #[ORM\ManyToOne(inversedBy: 'partnerFollows')] | |||
| #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] | |||
| private ?User $user = null; | |||
| #[ORM\ManyToOne(inversedBy: 'partnerFollows')] | |||
| #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] | |||
| private ?Partner $partner = null; | |||
| #[ORM\Column] | |||
| private ?\DateTimeImmutable $createdAt = null; | |||
| public function __construct(User $user, Partner $partner) | |||
| { | |||
| $this->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; | |||
| } | |||
| } | |||
| @@ -32,10 +32,14 @@ class Product | |||
| #[ORM\OneToMany(mappedBy: 'product', targetEntity: Sale::class)] | |||
| private Collection $sales; | |||
| #[ORM\OneToMany(mappedBy: 'product', targetEntity: DocumentObject::class)] | |||
| private Collection $documentObjects; | |||
| public function __construct() | |||
| { | |||
| $this->createdAt = new \DateTimeImmutable(); | |||
| $this->sales = new ArrayCollection(); | |||
| $this->documentObjects = new ArrayCollection(); | |||
| } | |||
| public function getId(): ?int | |||
| @@ -113,4 +117,34 @@ class Product | |||
| return $this; | |||
| } | |||
| /** | |||
| * @return Collection<int, DocumentObject> | |||
| */ | |||
| public function getDocumentObjects(): Collection | |||
| { | |||
| return $this->documentObjects; | |||
| } | |||
| public function addDocumentObject(DocumentObject $documentObject): static | |||
| { | |||
| if (!$this->documentObjects->contains($documentObject)) { | |||
| $this->documentObjects->add($documentObject); | |||
| $documentObject->setProduct($this); | |||
| } | |||
| return $this; | |||
| } | |||
| public function removeDocumentObject(DocumentObject $documentObject): static | |||
| { | |||
| if ($this->documentObjects->removeElement($documentObject)) { | |||
| // set the owning side to null (unless already changed) | |||
| if ($documentObject->getProduct() === $this) { | |||
| $documentObject->setProduct(null); | |||
| } | |||
| } | |||
| return $this; | |||
| } | |||
| } | |||
| @@ -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<int, PartnerFollow> | |||
| */ | |||
| 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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,84 @@ | |||
| <?php | |||
| namespace App\Factory; | |||
| use App\Entity\DocumentObject; | |||
| use Doctrine\ORM\EntityRepository; | |||
| use Symfony\Component\HttpKernel\KernelInterface; | |||
| use Vich\UploaderBundle\FileAbstraction\ReplacingFile; | |||
| use Zenstruck\Foundry\ModelFactory; | |||
| use Zenstruck\Foundry\Proxy; | |||
| use Zenstruck\Foundry\RepositoryProxy; | |||
| /** | |||
| * @extends ModelFactory<DocumentObject> | |||
| * | |||
| * @method DocumentObject|Proxy create(array|callable $attributes = []) | |||
| * @method static DocumentObject|Proxy createOne(array $attributes = []) | |||
| * @method static DocumentObject|Proxy find(object|array|mixed $criteria) | |||
| * @method static DocumentObject|Proxy findOrCreate(array $attributes) | |||
| * @method static DocumentObject|Proxy first(string $sortedField = 'id') | |||
| * @method static DocumentObject|Proxy last(string $sortedField = 'id') | |||
| * @method static DocumentObject|Proxy random(array $attributes = []) | |||
| * @method static DocumentObject|Proxy randomOrCreate(array $attributes = []) | |||
| * @method static EntityRepository|RepositoryProxy repository() | |||
| * @method static DocumentObject[]|Proxy[] all() | |||
| * @method static DocumentObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) | |||
| * @method static DocumentObject[]|Proxy[] createSequence(iterable|callable $sequence) | |||
| * @method static DocumentObject[]|Proxy[] findBy(array $attributes) | |||
| * @method static DocumentObject[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) | |||
| * @method static DocumentObject[]|Proxy[] randomSet(int $number, array $attributes = []) | |||
| */ | |||
| final class DocumentObjectFactory extends ModelFactory | |||
| { | |||
| /** | |||
| * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services | |||
| * | |||
| * @todo inject services if required | |||
| */ | |||
| public function __construct( | |||
| private KernelInterface $appKernel | |||
| ) | |||
| { | |||
| parent::__construct(); | |||
| } | |||
| /** | |||
| * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories | |||
| * | |||
| * @todo add your default values here | |||
| */ | |||
| protected function getDefaults(): array | |||
| { | |||
| $projectRoot = $this->appKernel->getProjectDir(); | |||
| $folderPath = $projectRoot . '/src/DataFixtures/documents/'; | |||
| $files = glob($folderPath . '*.*'); | |||
| $randomFile = null; | |||
| if ($files !== false && count($files) > 0) { | |||
| $randomFile = $files[array_rand($files)]; | |||
| } | |||
| $randBool = (bool)random_int(0, 1); | |||
| return [ | |||
| 'file' => new ReplacingFile($randomFile), | |||
| 'partner' => $randBool ? PartnerFactory::random() : null, | |||
| 'product' => !$randBool ? ProductFactory::random() : null | |||
| ]; | |||
| } | |||
| /** | |||
| * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization | |||
| */ | |||
| protected function initialize(): self | |||
| { | |||
| return $this | |||
| // ->afterInstantiate(function(DocumentObject $documentObject): void {}) | |||
| ; | |||
| } | |||
| protected static function getClass(): string | |||
| { | |||
| return DocumentObject::class; | |||
| } | |||
| } | |||
| @@ -0,0 +1,70 @@ | |||
| <?php | |||
| namespace App\Factory; | |||
| use App\Entity\PartnerFollow; | |||
| use App\Repository\PartnerFollowRepository; | |||
| use Zenstruck\Foundry\ModelFactory; | |||
| use Zenstruck\Foundry\Proxy; | |||
| use Zenstruck\Foundry\RepositoryProxy; | |||
| /** | |||
| * @extends ModelFactory<PartnerFollow> | |||
| * | |||
| * @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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -0,0 +1,62 @@ | |||
| <?php | |||
| namespace App\Mapper; | |||
| use App\ApiResource\PartnerFollowApi; | |||
| use App\Entity\Partner; | |||
| use App\Entity\PartnerFollow; | |||
| use App\Entity\User; | |||
| use App\Repository\PartnerFollowRepository; | |||
| use Symfony\Bundle\SecurityBundle\Security; | |||
| use Symfonycasts\MicroMapper\AsMapper; | |||
| use Symfonycasts\MicroMapper\MapperInterface; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| #[AsMapper(from: PartnerFollowApi::class, to: PartnerFollow::class)] | |||
| class PartnerFollowApiToEntityMapper implements MapperInterface | |||
| { | |||
| public function __construct( | |||
| private PartnerFollowRepository $repository, | |||
| private Security $security, | |||
| private MicroMapperInterface $microMapper, | |||
| ) | |||
| { | |||
| } | |||
| public function load(object $from, string $toClass, array $context): object | |||
| { | |||
| $dto = $from; | |||
| assert($dto instanceof PartnerFollowApi); | |||
| if ($dto->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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| <?php | |||
| namespace App\Mapper; | |||
| use App\ApiResource\PartnerApi; | |||
| use App\ApiResource\PartnerFollowApi; | |||
| use App\ApiResource\UserApi; | |||
| use App\Entity\PartnerFollow; | |||
| use Symfonycasts\MicroMapper\AsMapper; | |||
| use Symfonycasts\MicroMapper\MapperInterface; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| #[AsMapper(from: PartnerFollow::class, to: PartnerFollowApi::class)] | |||
| class PartnerFollowEntityToApiMapper implements MapperInterface | |||
| { | |||
| public function __construct( | |||
| private MicroMapperInterface $microMapper | |||
| ) | |||
| { | |||
| } | |||
| public function load(object $from, string $toClass, array $context): object | |||
| { | |||
| $entity = $from; | |||
| assert($entity instanceof PartnerFollow); | |||
| $dto = new PartnerFollowApi(); | |||
| $dto->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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| <?php | |||
| namespace App\Repository; | |||
| use App\Entity\PartnerFollow; | |||
| use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |||
| use Doctrine\Persistence\ManagerRegistry; | |||
| /** | |||
| * @extends ServiceEntityRepository<PartnerFollow> | |||
| * | |||
| * @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() | |||
| // ; | |||
| // } | |||
| } | |||
| @@ -8,6 +8,7 @@ | |||
| namespace App\Serializer; | |||
| use App\Entity\DocumentObject; | |||
| use App\Entity\MediaObject; | |||
| use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | |||
| @@ -31,7 +32,7 @@ final class MediaObjectNormalizer implements NormalizerInterface, SerializerAwar | |||
| { | |||
| $context[self::ALREADY_CALLED] = true; | |||
| if ($object instanceof MediaObject) { | |||
| if ($object instanceof MediaObject || $object instanceof DocumentObject) { | |||
| // Nur für MediaObject die URI generieren | |||
| $object->contentUrl = $this->storage->resolveUri($object, 'file'); | |||
| } | |||
| @@ -0,0 +1,121 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @date 01.03.24 | |||
| */ | |||
| namespace App\Tests\Functional; | |||
| use App\Factory\DocumentObjectFactory; | |||
| use App\Factory\MediaObjectLogoFactory; | |||
| use App\Factory\MediaObjectProductFactory; | |||
| use App\Factory\PartnerFactory; | |||
| use App\Factory\ProductFactory; | |||
| use App\Factory\UserFactory; | |||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | |||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |||
| use Symfony\Component\HttpFoundation\File\UploadedFile; | |||
| use Zenstruck\Browser\Test\HasBrowser; | |||
| use Zenstruck\Foundry\Test\Factories; | |||
| use Zenstruck\Foundry\Test\ResetDatabase; | |||
| class DocumentObjectResourceTest extends KernelTestCase | |||
| { | |||
| use HasBrowser; | |||
| use ResetDatabase; | |||
| use Factories; | |||
| private JWTTokenManagerInterface $JWTManager; | |||
| private string $projectDir; | |||
| protected function setUp(): void | |||
| { | |||
| parent::setUp(); | |||
| $this->JWTManager = self::getContainer()->get('lexik_jwt_authentication.jwt_manager'); | |||
| $this->projectDir = self::getContainer()->get('kernel')->getProjectDir(); | |||
| } | |||
| public function testCreateDocumentObject(): void | |||
| { | |||
| $path = $this->projectDir . '/tests/fixtures/'; | |||
| $srcFile = $path . '1176.png'; | |||
| $dstFile = $path . '1176_upload.png'; | |||
| copy($srcFile, $dstFile); | |||
| $file = new UploadedFile($dstFile, 'image.png'); | |||
| $user = UserFactory::createOne( | |||
| [ | |||
| 'firstName' => 'Peter', | |||
| 'lastName' => 'Test', | |||
| 'password' => 'test', | |||
| 'email' => 'peter@test.de', | |||
| ] | |||
| ); | |||
| MediaObjectProductFactory::createOne(); | |||
| $partner = PartnerFactory::createOne(); | |||
| $product = ProductFactory::createOne(); | |||
| $token = $this->JWTManager->create($user->object()); | |||
| $this->browser() | |||
| ->get('/api/documents', [ | |||
| 'headers' => [ | |||
| 'Authorization' => 'Bearer ' . $token, | |||
| ], | |||
| ]) | |||
| ->assertSuccessful() | |||
| ; | |||
| $this->browser() | |||
| ->post('/api/documents', [ | |||
| 'headers' => [ | |||
| 'Authorization' => 'Bearer ' . $token, | |||
| 'Content-Type' => 'multipart/form-data' | |||
| ], | |||
| 'files' => [ | |||
| 'file' => $file, | |||
| ], | |||
| 'partner' => '/api/partners/' . $partner->getId(), | |||
| 'product' => '/api/products/' . $product->getId(), | |||
| ]) | |||
| ->assertSuccessful() | |||
| ; | |||
| $this->browser() | |||
| ->delete('/api/documents/1', | |||
| [ | |||
| 'headers' => [ | |||
| 'Authorization' => 'Bearer ' . $token, | |||
| ], | |||
| ]) | |||
| ->assertSuccessful() | |||
| ; | |||
| } | |||
| public function testDeleteDocumentObject() | |||
| { | |||
| MediaObjectProductFactory::createOne(); | |||
| $partner = PartnerFactory::createOne(); | |||
| $product = ProductFactory::createOne(); | |||
| $documentsObject = DocumentObjectFactory::createOne(); | |||
| $user = UserFactory::createOne( | |||
| [ | |||
| 'firstName' => 'Peter', | |||
| 'lastName' => 'Test', | |||
| 'password' => 'test', | |||
| 'email' => 'peter@test.de', | |||
| ] | |||
| ); | |||
| $token = $this->JWTManager->create($user->object()); | |||
| $this->browser() | |||
| ->delete('/api/documents/' . $documentsObject->getId(), [ | |||
| 'headers' => [ | |||
| 'Authorization' => 'Bearer ' . $token, | |||
| ], | |||
| ]) | |||
| ->assertSuccessful() | |||
| ; | |||
| } | |||
| } | |||
| @@ -33,7 +33,7 @@ class MediaObjectResourceTest extends KernelTestCase | |||
| $this->projectDir = self::getContainer()->get('kernel')->getProjectDir(); | |||
| } | |||
| public function testCreateAMediaObject(): void | |||
| public function testCreateMediaObject(): void | |||
| { | |||
| $path = $this->projectDir . '/tests/fixtures/'; | |||
| $srcFile = $path . '1176.png'; | |||
| @@ -0,0 +1,83 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @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) | |||
| ; | |||
| } | |||
| } | |||