Daniel 1 рік тому
джерело
коміт
36b12d67cd
17 змінених файлів з 458 додано та 2 видалено
  1. +1
    -1
      export/openapi.json
  2. +78
    -0
      export/openapi.yaml
  3. +35
    -0
      migrations/Version20240607125011.php
  4. +2
    -1
      src/ApiResource/ContactApi.php
  5. +3
    -0
      src/ApiResource/PartnerProductApi.php
  6. +2
    -0
      src/ApiResource/ProductApi.php
  7. +18
    -0
      src/ApiResource/TaskNoteApi.php
  8. +2
    -0
      src/ApiResource/UserApi.php
  9. +34
    -0
      src/Entity/Contact.php
  10. +17
    -0
      src/Entity/TaskNote.php
  11. +1
    -0
      src/Factory/TaskNoteFactory.php
  12. +63
    -0
      src/Filter/ContactNameFilter.php
  13. +61
    -0
      src/Filter/PartnerProductUnassignedFilter.php
  14. +61
    -0
      src/Filter/ProductUnassignedFilter.php
  15. +64
    -0
      src/Filter/UserNameFilter.php
  16. +7
    -0
      src/Mapper/TaskNoteApiToEntityMapper.php
  17. +9
    -0
      src/Mapper/TaskNoteEntityToApiMapper.php

+ 1
- 1
export/openapi.json
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 78
- 0
export/openapi.yaml Переглянути файл

@@ -586,6 +586,18 @@ paths:
style: form
explode: true
allowReserved: false
-
name: nameSearch
in: query
description: ''
required: false
deprecated: false
allowEmptyValue: true
schema:
type: string
style: form
explode: false
allowReserved: false
deprecated: false
post:
operationId: api_contacts_post
@@ -1811,6 +1823,18 @@ paths:
style: form
explode: true
allowReserved: false
-
name: productPartnerUnassigned
in: query
description: ''
required: false
deprecated: false
allowEmptyValue: true
schema:
type: string
style: form
explode: false
allowReserved: false
deprecated: false
post:
operationId: api_partner_products_post
@@ -2633,6 +2657,18 @@ paths:
style: form
explode: false
allowReserved: false
-
name: partnerIdUnassigned
in: query
description: ''
required: false
deprecated: false
allowEmptyValue: true
schema:
type: string
style: form
explode: false
allowReserved: false
-
name: 'order[name]'
in: query
@@ -4023,6 +4059,18 @@ paths:
style: form
explode: false
allowReserved: false
-
name: nameSearch
in: query
description: ''
required: false
deprecated: false
allowEmptyValue: true
schema:
type: string
style: form
explode: false
allowReserved: false
deprecated: false
post:
operationId: api_users_post
@@ -6657,6 +6705,16 @@ components:
- 'null'
format: iri-reference
example: 'https://example.com/'
contact:
readOnly: true
description: '?ContactApi'
$ref: '#/components/schemas/Contact'
contactIri:
type:
- string
- 'null'
format: iri-reference
example: 'https://example.com/'
contactType:
type: string
enum:
@@ -6701,6 +6759,16 @@ components:
- 'null'
format: iri-reference
example: 'https://example.com/'
contact:
readOnly: true
description: '?ContactApi'
$ref: '#/components/schemas/Contact.jsonhal'
contactIri:
type:
- string
- 'null'
format: iri-reference
example: 'https://example.com/'
contactType:
type: string
enum:
@@ -6759,6 +6827,16 @@ components:
- 'null'
format: iri-reference
example: 'https://example.com/'
contact:
readOnly: true
description: '?ContactApi'
$ref: '#/components/schemas/Contact.jsonld'
contactIri:
type:
- string
- 'null'
format: iri-reference
example: 'https://example.com/'
contactType:
type: string
enum:


+ 35
- 0
migrations/Version20240607125011.php Переглянути файл

@@ -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 Version20240607125011 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('ALTER TABLE task_note ADD contact_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE task_note ADD CONSTRAINT FK_BC0E6E6FE7A1254A FOREIGN KEY (contact_id) REFERENCES contact (id)');
$this->addSql('CREATE INDEX IDX_BC0E6E6FE7A1254A ON task_note (contact_id)');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE task_note DROP FOREIGN KEY FK_BC0E6E6FE7A1254A');
$this->addSql('DROP INDEX IDX_BC0E6E6FE7A1254A ON task_note');
$this->addSql('ALTER TABLE task_note DROP contact_id');
}
}

+ 2
- 1
src/ApiResource/ContactApi.php Переглянути файл

@@ -14,9 +14,9 @@ use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Entity\Contact;
use App\Entity\MediaObject;
use App\Filter\ContactNameFilter;
use App\State\EntityClassDtoStateProcessor;
use App\State\EntityToDtoStateProvider;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
@@ -51,6 +51,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
stateOptions: new Options(entityClass: Contact::class),
)]
#[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact'])]
#[ApiFilter(ContactNameFilter::class)]
class ContactApi
{
#[ApiProperty(readable: false, writable: false, identifier: true)]


+ 3
- 0
src/ApiResource/PartnerProductApi.php Переглянути файл

@@ -13,6 +13,7 @@ use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Entity\PartnerProduct;
use App\Filter\PartnerProductUnassignedFilter;
use App\State\EntityClassDtoStateProcessor;
use App\State\EntityToDtoStateProvider;
use ApiPlatform\Metadata\Delete;
@@ -38,6 +39,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
security: 'is_granted("ROLE_USER")',
)
],
order: ['product.name' => 'ASC'],
security: 'is_granted("ROLE_USER")',
provider: EntityToDtoStateProvider::class,
processor: EntityClassDtoStateProcessor::class,
@@ -50,6 +52,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
'product.name' => 'ipartial',
'partner.type' => 'exact'
])]
#[ApiFilter(PartnerProductUnassignedFilter::class)]
class PartnerProductApi
{
#[ApiProperty(readable: false, writable: false, identifier: true)]


+ 2
- 0
src/ApiResource/ProductApi.php Переглянути файл

@@ -16,6 +16,7 @@ use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Entity\MediaObject;
use App\Entity\Product;
use App\Filter\ProductUnassignedFilter;
use App\State\EntityClassDtoStateProcessor;
use App\State\EntityToDtoStateProvider;
use ApiPlatform\Metadata\Delete;
@@ -51,6 +52,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
)]

#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial'])]
#[ApiFilter(ProductUnassignedFilter::class)]
#[ApiFilter(OrderFilter::class, properties: ['name'], arguments: ['orderParameterName' => 'order'])]
class ProductApi
{


+ 18
- 0
src/ApiResource/TaskNoteApi.php Переглянути файл

@@ -76,6 +76,24 @@ class TaskNoteApi implements OwnerInterface
#[NotBlank]
public ?TaskApi $taskIri = null;

/**
* @var $owner ?ContactApi
*/
#[ApiProperty(
writable: false,
readableLink: true,
writableLink: false,
builtinTypes: [
new Type(
'object',
class: ContactApi::class,
)
]
)]
public ?ContactApi $contact = null;

public ?ContactApi $contactIri = null;

#[NotBlank]
public TaskNoteContactType $contactType;



+ 2
- 0
src/ApiResource/UserApi.php Переглянути файл

@@ -18,6 +18,7 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Entity\MediaObject;
use App\Entity\User;
use App\Filter\UserNameFilter;
use App\Interface\AdminOrOwnerInterface;
use App\State\EntityClassDtoStateProcessor;
use App\State\EntityToDtoStateProvider;
@@ -47,6 +48,7 @@ use Symfony\Component\Validator\Constraints as Assert;

)]
#[ApiFilter(SearchFilter::class, properties: ['firstName' => 'ipartial', 'lastName' => 'ipartial'])]
#[ApiFilter(UserNameFilter::class)]
class UserApi
{
#[ApiProperty(readable: false, writable: false, identifier: true)]


+ 34
- 0
src/Entity/Contact.php Переглянути файл

@@ -54,6 +54,9 @@ class Contact
#[ORM\OneToMany(mappedBy: 'contact', targetEntity: ContactPartnerProduct::class)]
private Collection $contactPartnerProducts;

#[ORM\OneToMany(mappedBy: 'contact', targetEntity: TaskNote::class)]
private Collection $taskNotes;

public function __construct(Partner $partner, User $createdBy)
{
$this->partner = $partner;
@@ -61,6 +64,7 @@ class Contact
$this->createdAt = new \DateTimeImmutable();
$this->posts = new ArrayCollection();
$this->contactPartnerProducts = new ArrayCollection();
$this->taskNotes = new ArrayCollection();
}

public function getId(): ?int
@@ -241,4 +245,34 @@ class Contact

return $this;
}

/**
* @return Collection<int, TaskNote>
*/
public function getTaskNotes(): Collection
{
return $this->taskNotes;
}

public function addTaskNote(TaskNote $taskNote): static
{
if (!$this->taskNotes->contains($taskNote)) {
$this->taskNotes->add($taskNote);
$taskNote->setContact($this);
}

return $this;
}

public function removeTaskNote(TaskNote $taskNote): static
{
if ($this->taskNotes->removeElement($taskNote)) {
// set the owning side to null (unless already changed)
if ($taskNote->getContact() === $this) {
$taskNote->setContact(null);
}
}

return $this;
}
}

+ 17
- 0
src/Entity/TaskNote.php Переглянути файл

@@ -26,12 +26,17 @@ class TaskNote
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Task $task = null;

#[ORM\ManyToOne(inversedBy: 'taskNotes')]
private ?Contact $contact = null;

#[ORM\Column(type: 'string', enumType: TaskNoteContactType::class)]
private TaskNoteContactType $type;

#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;



public function __construct(User $owner, Task $task)
{
$this->owner = $owner;
@@ -101,4 +106,16 @@ class TaskNote

return $this;
}

public function getContact(): ?Contact
{
return $this->contact;
}

public function setContact(?Contact $contact): static
{
$this->contact = $contact;

return $this;
}
}

+ 1
- 0
src/Factory/TaskNoteFactory.php Переглянути файл

@@ -53,6 +53,7 @@ final class TaskNoteFactory extends ModelFactory
'owner' => UserFactory::random(),
'task' => TaskFactory::random(),
'type' => self::faker()->randomElement(TaskNoteContactType::cases()),
'contact' => ContactFactory::random()
];
}



+ 63
- 0
src/Filter/ContactNameFilter.php Переглянути файл

@@ -0,0 +1,63 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @date 10.06.24
*/


namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use ApiPlatform\Metadata\ApiFilter;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;

#[ApiFilter(ContactNameFilter::class)]
class ContactNameFilter extends AbstractFilter
{
public const FILTER_NAME = "nameSearch";

public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null)
{
parent::__construct($managerRegistry, $logger, $properties);
}

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?\ApiPlatform\Metadata\Operation $operation = null, array $context = []): void
{
if ($property !== self::FILTER_NAME) {
return;
}

if ($value) {
// Alias für User-Tabelle
$rootAlias = $queryBuilder->getRootAliases()[0];

// Case-insensitive Suche nach Vorname oder Nachname, der den Wert enthält
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->like('LOWER(' . $rootAlias . '.firstName)', ':searchTerm'),
$queryBuilder->expr()->like('LOWER(' . $rootAlias . '.lastName)', ':searchTerm')
)
)
->setParameter('searchTerm', '%' . strtolower($value) . '%');
}
}

public function getDescription(string $resourceClass): array
{
return [
self::FILTER_NAME => [
'property' => self::FILTER_NAME,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter users that have the search term in their first name or last name',
'type' => 'string',
],
],
];
}

}

+ 61
- 0
src/Filter/PartnerProductUnassignedFilter.php Переглянути файл

@@ -0,0 +1,61 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @date 10.06.24
*/


namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use ApiPlatform\Metadata\ApiFilter;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;

#[ApiFilter(PartnerProductUnassignedFilter::class)]
class PartnerProductUnassignedFilter extends AbstractFilter
{
public const FILTER_NAME = "productPartnerUnassigned";

public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null)
{
parent::__construct($managerRegistry, $logger, $properties);
}

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?\ApiPlatform\Metadata\Operation $operation = null, array $context = []): void
{
if ($property !== 'unassignedContactId') {
return;
}

if ($value) {
// Alias für das Produkt
$rootAlias = $queryBuilder->getRootAliases()[0];

// Join mit PartnerProduct
$queryBuilder->leftJoin("$rootAlias.contactPartnerProducts", 'cpp', Join::WITH, 'cpp.contact = :contact_id')
->andWhere('cpp.partnerProduct IS NULL')
->setParameter('contact_id', $value);
}
}

public function getDescription(string $resourceClass): array
{
return [
self::FILTER_NAME => [
'property' => self::FILTER_NAME,
'type' => 'boolean',
'required' => false,
'swagger' => [
'description' => 'Filter partner products that are not assigned to a given contact id',
'type' => 'integer', // Hier wird der Datentyp auf 'integer' gesetzt
'format' => 'contact_id', // Optional: Format 'partner_id' angeben, um klarzustellen, dass es sich um eine Partner-ID handelt
],
],
];
}

}

+ 61
- 0
src/Filter/ProductUnassignedFilter.php Переглянути файл

@@ -0,0 +1,61 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @date 10.06.24
*/


namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use ApiPlatform\Metadata\ApiFilter;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;

#[ApiFilter(ProductUnassignedFilter::class)]
class ProductUnassignedFilter extends AbstractFilter
{
public const FILTER_NAME = "partnerIdUnassigned";

public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null)
{
parent::__construct($managerRegistry, $logger, $properties);
}

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?\ApiPlatform\Metadata\Operation $operation = null, array $context = []): void
{
if ($property !== 'unassignedPartnerId') {
return;
}

if ($value) {
// Alias für das Produkt
$rootAlias = $queryBuilder->getRootAliases()[0];

// Join mit PartnerProduct
$queryBuilder->leftJoin("$rootAlias.partnerProducts", 'pp', Join::WITH, 'pp.partner = :partner_id')
->andWhere('pp.product IS NULL')
->setParameter('partner_id', $value);
}
}

public function getDescription(string $resourceClass): array
{
return [
self::FILTER_NAME => [
'property' => self::FILTER_NAME,
'type' => 'boolean',
'required' => false,
'swagger' => [
'description' => 'Filter products that are not assigned to a given partner id',
'type' => 'integer', // Hier wird der Datentyp auf 'integer' gesetzt
'format' => 'partner_id', // Optional: Format 'partner_id' angeben, um klarzustellen, dass es sich um eine Partner-ID handelt
],
],
];
}

}

+ 64
- 0
src/Filter/UserNameFilter.php Переглянути файл

@@ -0,0 +1,64 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @date 10.06.24
*/


namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use ApiPlatform\Metadata\ApiFilter;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;

#[ApiFilter(UserNameFilter::class)]
class UserNameFilter extends AbstractFilter
{
public const FILTER_NAME = "nameSearch";

public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null)
{
parent::__construct($managerRegistry, $logger, $properties);
}

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?\ApiPlatform\Metadata\Operation $operation = null, array $context = []): void
{
if ($property !== self::FILTER_NAME) {
return;
}

if ($value) {
// Alias für User-Tabelle
$rootAlias = $queryBuilder->getRootAliases()[0];

// Case-insensitive Suche nach Vorname oder Nachname, der den Wert enthält
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->like('LOWER(' . $rootAlias . '.firstName)', ':searchTerm'),
$queryBuilder->expr()->like('LOWER(' . $rootAlias . '.lastName)', ':searchTerm')
)
)
->setParameter('searchTerm', '%' . strtolower($value) . '%');
}
}

public function getDescription(string $resourceClass): array
{
return [
self::FILTER_NAME => [
'property' => 'firstLastName',
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter users that have the search term in their first name or last name',
'type' => 'string',
],
],
];
}

}

+ 7
- 0
src/Mapper/TaskNoteApiToEntityMapper.php Переглянути файл

@@ -3,6 +3,7 @@
namespace App\Mapper;

use App\ApiResource\TaskNoteApi;
use App\Entity\Contact;
use App\Entity\Task;
use App\Entity\TaskNote;
use App\Entity\User;
@@ -59,6 +60,12 @@ class TaskNoteApiToEntityMapper implements MapperInterface
assert($entity instanceof TaskNote);
$entity->setMessage($dto->message);
$entity->setType($dto->contactType);
if ($dto->contactIri) {
$entity->setContact($this->microMapper->map($dto->contactIri, Contact::class, [
MicroMapperInterface::MAX_DEPTH => 1,
]));
}


return $entity;
}


+ 9
- 0
src/Mapper/TaskNoteEntityToApiMapper.php Переглянути файл

@@ -2,6 +2,7 @@

namespace App\Mapper;

use App\ApiResource\ContactApi;
use App\ApiResource\TaskApi;
use App\ApiResource\TaskNoteApi;
use App\ApiResource\UserApi;
@@ -46,6 +47,14 @@ class TaskNoteEntityToApiMapper implements MapperInterface
MicroMapperInterface::MAX_DEPTH => 1,
]);

$dto->contact = $this->microMapper->map($entity->getContact(), ContactApi::class, [
MicroMapperInterface::MAX_DEPTH => 1,
]);

$dto->contactIri = $this->microMapper->map($entity->getContact(), ContactApi::class, [
MicroMapperInterface::MAX_DEPTH => 1,
]);

$dto->contactType = $entity->getType();

$dto->createdAt = $entity->getCreatedAt();


Завантаження…
Відмінити
Зберегти