Migration modular monolith DDD (0.1 → 3.3) (#17)
Auto Tag Develop / tag (push) Successful in 9s
Auto Tag Develop / tag (push) Successful in 9s
## Migration modular monolith DDD — Lesstime (0.1 → 3.3) Cette MR regroupe l'intégralité de la refonte en monolithe modulaire (strangler progressif, additif). Elle remplace les MR stackées de Phase 1 (#12–#16), désormais incluses ici. **Ne pas merger avant validation fonctionnelle** : branche destinée à être testée telle quelle. ### Périmètre — 9 modules sous `src/Module/` | Phase | Module | Contenu | |------|--------|---------| | 0.1 | (socle) | infrastructure modulaire, `ModuleInterface`, mapping Doctrine par module | | 0.2 | (socle front) | auto-détection des layers Nuxt sous `frontend/modules/*` | | 1.1 | **Core** | Identité (User/Auth), Notifications, Notifier | | 1.2 | Core | RBAC fin (permissions `module.resource.action`, sidebar gated) | | 1.3 | Core | Audit log (`#[Auditable]`, listener, provider DBAL) | | 2.1 | **TimeTracking** | TimeEntry + MCP + export | | 2.2 | **ProjectManagement** | cœur métier Projets/Tâches + 38 MCP tools | | 2.3 | **Absence** | demandes, soldes, policies, justificatifs | | 2.4 | **Directory** | Clients (migrés) + **Prospects** (nouveau, conversion → Client) | | 2.5 | **Mail** | intégration IMAP OVH + liens tâches | | 2.6 | **Integration** | Gitea / BookStack / Zimbra / Share | | 3.1 | **Reporting** | rapports transverses (DBAL read-only, 0 import inter-module) | | 3.2 | **ClientPortal** | portail client (ROLE_CLIENT cloisonné, tickets, notifications) | | 3.3 | (finition) | nettoyage legacy — `src/Entity` vide, app 100% modulaire | ### Architecture - Découplage inter-modules par **contrats** (`UserInterface`, `ProjectInterface`, `TaskInterface`, `TaskTagInterface`, `ClientInterface`, `ClientTicketInterface`, `LeaveProfileInterface`) + `resolve_target_entities` 100% modulaire (aucune cible legacy). - Repositories : interface `Domain/Repository` + implémentation `Infrastructure/Doctrine`, bindées. - Reporting en DBAL read-only pur (aucun import d'entité d'un autre module). - Chaque migration de module : déplacement à comportement préservé (API publique et noms d'outils MCP inchangés), migrations **additives** uniquement (zéro destructif). ### Sécurité - ROLE_CLIENT cloisonné : un utilisateur client n'accède qu'à `/portal` et à ses propres tickets (filtrés par `allowedProjects`), interdit sur toute l'API interne. - Correctif : interdiction pour un client de créer un lien vers le partage SMB (upload uniquement). ### QA non-régression (branche reconstruite from scratch) - Migrations from scratch + fixtures : OK. - Compilation dev + prod : OK. - **180 tests PHPUnit verts**, php-cs-fixer clean, ~96 routes, **66 outils MCP** tous sous `App\Module\*`. - Smoke test runtime multi-rôles (admin / ROLE_USER / ROLE_CLIENT) : 44 vérifications HTTP, **0 écart**, cloisonnement client étanche. - Build Nuxt OK, 9 layers, 0 import legacy résiduel. ### Points à arbitrer (hors périmètre de cette migration) - Durcissement MCP/IDOR pré-existant (`userId` explicite sans scoping sur certains tools TimeTracking/Absence/TaskDocument) — ticket dédié recommandé. - Validation fonctionnelle de **Prospect** et **ClientPortal** (conçus depuis les specs disque). - **Harmonisation visuelle Malio finale** (3.3) — finition esthétique inter-modules laissée au PO. --- ## ⚠️ Déploiement / migration des données — à ne pas oublier ### 1. Resynchroniser les séquences PostgreSQL après tout import/restore de dump Si la prod (ou tout environnement) est **montée depuis un dump** (`pg_restore` / `COPY`), les lignes sont chargées avec leurs `id` explicites **sans avancer les séquences** → au premier `INSERT` : `duplicate key value violates unique constraint "..._pkey"` (constaté en local sur `notification`, `task`, `time_entry`…). À lancer **juste après chaque restore/import** : ```sql DO $$ DECLARE r RECORD; maxid BIGINT; seq TEXT; BEGIN FOR r IN SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' LOOP seq := pg_get_serial_sequence(quote_ident(r.table_name), r.column_name); IF seq IS NOT NULL THEN EXECUTE format('SELECT COALESCE(MAX(%I),0) FROM %I', r.column_name, r.table_name) INTO maxid; PERFORM setval(seq, GREATEST(maxid,1), maxid > 0); END IF; END LOOP; END $$; ``` > Ne concerne **pas** une prod qui tourne déjà (séquences avancées organiquement) — uniquement le cas restore/import. Idempotent, sans risque. ### 2. Fix dénormalisation des collections typées-contrat (code, inclus dans la branche) Les relations **to-many** typées par une interface `Shared\Domain\Contract\*` (`TimeEntry::tags` → `TaskTagInterface`, `Task::collaborators` → `UserInterface`) étaient **indénormalisables par API Platform** (mono-valué OK via IRI, collection KO) → **tout POST/PATCH portant une telle collection renvoyait 400/500**. Corrigé par un dénormaliseur générique `ContractRelationDenormalizer` (réutilise `resolve_target_entities`, zéro couplage par-entité) + test fonctionnel de non-régression. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Directory\Infrastructure\Doctrine\DoctrineAddressRepository;
|
||||
use App\Shared\Domain\Attribute\Auditable;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[Auditable]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Post(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['address:read']],
|
||||
denormalizationContext: ['groups' => ['address:write']],
|
||||
order: ['id' => 'ASC'],
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['client' => 'exact', 'prospect' => 'exact'])]
|
||||
#[ORM\Entity(repositoryClass: DoctrineAddressRepository::class)]
|
||||
#[ORM\Table(name: 'directory_address')]
|
||||
class Address implements TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['address:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['address:read', 'address:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['address:read', 'address:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $street = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['address:read', 'address:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $streetComplement = null;
|
||||
|
||||
#[ORM\Column(length: 20, nullable: true)]
|
||||
#[Groups(['address:read', 'address:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $postalCode = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['address:read', 'address:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $city = null;
|
||||
|
||||
#[ORM\Column(length: 2)]
|
||||
#[Groups(['address:read', 'address:write', 'client:read', 'prospect:read'])]
|
||||
private string $country = 'FR';
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Client::class)]
|
||||
#[ORM\JoinColumn(name: 'client_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['address:read', 'address:write'])]
|
||||
private ?Client $client = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Prospect::class)]
|
||||
#[ORM\JoinColumn(name: 'prospect_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['address:read', 'address:write'])]
|
||||
private ?Prospect $prospect = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreet(): ?string
|
||||
{
|
||||
return $this->street;
|
||||
}
|
||||
|
||||
public function setStreet(?string $street): static
|
||||
{
|
||||
$this->street = $street;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreetComplement(): ?string
|
||||
{
|
||||
return $this->streetComplement;
|
||||
}
|
||||
|
||||
public function setStreetComplement(?string $streetComplement): static
|
||||
{
|
||||
$this->streetComplement = $streetComplement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPostalCode(): ?string
|
||||
{
|
||||
return $this->postalCode;
|
||||
}
|
||||
|
||||
public function setPostalCode(?string $postalCode): static
|
||||
{
|
||||
$this->postalCode = $postalCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCity(): ?string
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
public function setCity(?string $city): static
|
||||
{
|
||||
$this->city = $city;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCountry(): string
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
public function setCountry(string $country): static
|
||||
{
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClient(): ?Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function setClient(?Client $client): static
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProspect(): ?Prospect
|
||||
{
|
||||
return $this->prospect;
|
||||
}
|
||||
|
||||
public function setProspect(?Prospect $prospect): static
|
||||
{
|
||||
$this->prospect = $prospect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Directory\Infrastructure\Doctrine\DoctrineClientRepository;
|
||||
use App\Shared\Domain\Attribute\Auditable;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\ClientInterface;
|
||||
use App\Shared\Domain\Contract\ProjectInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[Auditable]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Post(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['client:read']],
|
||||
denormalizationContext: ['groups' => ['client:write']],
|
||||
order: ['name' => 'ASC'],
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: DoctrineClientRepository::class)]
|
||||
class Client implements ClientInterface, TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['client:read', 'project:read', 'user:list'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['client:read', 'client:write', 'project:read', 'user:list'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['client:read', 'client:write'])]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['client:read', 'client:write'])]
|
||||
private ?string $phone = null;
|
||||
|
||||
/** @var Collection<int, ProjectInterface> */
|
||||
#[ORM\OneToMany(targetEntity: ProjectInterface::class, mappedBy: 'client')]
|
||||
private Collection $projects;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->projects = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhone(): ?string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(?string $phone): static
|
||||
{
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, ProjectInterface> */
|
||||
public function getProjects(): Collection
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Directory\Domain\Enum\ReportType;
|
||||
use App\Module\Directory\Infrastructure\Doctrine\DoctrineCommercialReportRepository;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Post(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['commercial_report:read']],
|
||||
denormalizationContext: ['groups' => ['commercial_report:write']],
|
||||
order: ['occurredAt' => 'DESC'],
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['client' => 'exact', 'prospect' => 'exact'])]
|
||||
#[ORM\Entity(repositoryClass: DoctrineCommercialReportRepository::class)]
|
||||
#[ORM\Table(name: 'commercial_report')]
|
||||
class CommercialReport implements TimestampableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['commercial_report:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['commercial_report:read', 'commercial_report:write'])]
|
||||
private ?string $subject = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['commercial_report:read', 'commercial_report:write'])]
|
||||
private ?string $body = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[Groups(['commercial_report:read', 'commercial_report:write'])]
|
||||
private ?DateTimeImmutable $occurredAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 32, enumType: ReportType::class)]
|
||||
#[Groups(['commercial_report:read', 'commercial_report:write'])]
|
||||
private ReportType $type = ReportType::Note;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: UserInterface::class)]
|
||||
#[ORM\JoinColumn(name: 'author_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['commercial_report:read'])]
|
||||
private ?UserInterface $author = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Client::class)]
|
||||
#[ORM\JoinColumn(name: 'client_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['commercial_report:read', 'commercial_report:write'])]
|
||||
private ?Client $client = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Prospect::class)]
|
||||
#[ORM\JoinColumn(name: 'prospect_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['commercial_report:read', 'commercial_report:write'])]
|
||||
private ?Prospect $prospect = null;
|
||||
|
||||
/** @var Collection<int, ReportDocument> */
|
||||
#[ORM\OneToMany(targetEntity: ReportDocument::class, mappedBy: 'commercialReport', cascade: ['remove'])]
|
||||
#[Groups(['commercial_report:read'])]
|
||||
private Collection $documents;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->documents = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getSubject(): ?string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function setSubject(string $subject): static
|
||||
{
|
||||
$this->subject = $subject;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBody(): ?string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function setBody(?string $body): static
|
||||
{
|
||||
$this->body = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOccurredAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->occurredAt;
|
||||
}
|
||||
|
||||
public function setOccurredAt(DateTimeImmutable $occurredAt): static
|
||||
{
|
||||
$this->occurredAt = $occurredAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): ReportType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(ReportType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthor(): ?UserInterface
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(?UserInterface $author): static
|
||||
{
|
||||
$this->author = $author;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClient(): ?Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function setClient(?Client $client): static
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProspect(): ?Prospect
|
||||
{
|
||||
return $this->prospect;
|
||||
}
|
||||
|
||||
public function setProspect(?Prospect $prospect): static
|
||||
{
|
||||
$this->prospect = $prospect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, ReportDocument> */
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Directory\Infrastructure\Doctrine\DoctrineContactRepository;
|
||||
use App\Shared\Domain\Attribute\Auditable;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[Auditable]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Post(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['contact:read']],
|
||||
denormalizationContext: ['groups' => ['contact:write']],
|
||||
order: ['lastName' => 'ASC'],
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['client' => 'exact', 'prospect' => 'exact'])]
|
||||
#[ORM\Entity(repositoryClass: DoctrineContactRepository::class)]
|
||||
#[ORM\Table(name: 'directory_contact')]
|
||||
class Contact implements TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['contact:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['contact:read', 'contact:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $firstName = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['contact:read', 'contact:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $lastName = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['contact:read', 'contact:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $jobTitle = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['contact:read', 'contact:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['contact:read', 'contact:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $phonePrimary = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['contact:read', 'contact:write', 'client:read', 'prospect:read'])]
|
||||
private ?string $phoneSecondary = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Client::class)]
|
||||
#[ORM\JoinColumn(name: 'client_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['contact:read', 'contact:write'])]
|
||||
private ?Client $client = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Prospect::class)]
|
||||
#[ORM\JoinColumn(name: 'prospect_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['contact:read', 'contact:write'])]
|
||||
private ?Prospect $prospect = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getFirstName(): ?string
|
||||
{
|
||||
return $this->firstName;
|
||||
}
|
||||
|
||||
public function setFirstName(?string $firstName): static
|
||||
{
|
||||
$this->firstName = $firstName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastName(): ?string
|
||||
{
|
||||
return $this->lastName;
|
||||
}
|
||||
|
||||
public function setLastName(?string $lastName): static
|
||||
{
|
||||
$this->lastName = $lastName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJobTitle(): ?string
|
||||
{
|
||||
return $this->jobTitle;
|
||||
}
|
||||
|
||||
public function setJobTitle(?string $jobTitle): static
|
||||
{
|
||||
$this->jobTitle = $jobTitle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhonePrimary(): ?string
|
||||
{
|
||||
return $this->phonePrimary;
|
||||
}
|
||||
|
||||
public function setPhonePrimary(?string $phonePrimary): static
|
||||
{
|
||||
$this->phonePrimary = $phonePrimary;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhoneSecondary(): ?string
|
||||
{
|
||||
return $this->phoneSecondary;
|
||||
}
|
||||
|
||||
public function setPhoneSecondary(?string $phoneSecondary): static
|
||||
{
|
||||
$this->phoneSecondary = $phoneSecondary;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClient(): ?Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function setClient(?Client $client): static
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProspect(): ?Prospect
|
||||
{
|
||||
return $this->prospect;
|
||||
}
|
||||
|
||||
public function setProspect(?Prospect $prospect): static
|
||||
{
|
||||
$this->prospect = $prospect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Directory\Domain\Enum\ProspectStatus;
|
||||
use App\Module\Directory\Infrastructure\ApiPlatform\State\ConvertProspectProcessor;
|
||||
use App\Module\Directory\Infrastructure\Doctrine\DoctrineProspectRepository;
|
||||
use App\Shared\Domain\Attribute\Auditable;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\ClientInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[Auditable]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Post(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Post(
|
||||
uriTemplate: '/prospects/{id}/convert',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: ConvertProspectProcessor::class,
|
||||
),
|
||||
],
|
||||
normalizationContext: ['groups' => ['prospect:read']],
|
||||
denormalizationContext: ['groups' => ['prospect:write']],
|
||||
order: ['name' => 'ASC'],
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['status' => 'exact'])]
|
||||
#[ORM\Entity(repositoryClass: DoctrineProspectRepository::class)]
|
||||
#[ORM\Table(name: 'prospect')]
|
||||
class Prospect implements TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['prospect:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ?string $company = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ?string $phone = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 32, enumType: ProspectStatus::class)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ProspectStatus $status = ProspectStatus::New;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ?string $source = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['prospect:read', 'prospect:write'])]
|
||||
private ?string $notes = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ClientInterface::class)]
|
||||
#[ORM\JoinColumn(name: 'converted_client_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['prospect:read'])]
|
||||
private ?ClientInterface $convertedClient = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCompany(): ?string
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
public function setCompany(?string $company): static
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhone(): ?string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(?string $phone): static
|
||||
{
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ProspectStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(ProspectStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSource(): ?string
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function setSource(?string $source): static
|
||||
{
|
||||
$this->source = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNotes(): ?string
|
||||
{
|
||||
return $this->notes;
|
||||
}
|
||||
|
||||
public function setNotes(?string $notes): static
|
||||
{
|
||||
$this->notes = $notes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConvertedClient(): ?ClientInterface
|
||||
{
|
||||
return $this->convertedClient;
|
||||
}
|
||||
|
||||
public function setConvertedClient(?ClientInterface $convertedClient): static
|
||||
{
|
||||
$this->convertedClient = $convertedClient;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Directory\Infrastructure\ApiPlatform\State\ReportDocumentProcessor;
|
||||
use App\Module\Directory\Infrastructure\EventListener\ReportDocumentListener;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Post(
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: ReportDocumentProcessor::class,
|
||||
deserialize: false,
|
||||
),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['report_document:read']],
|
||||
denormalizationContext: ['groups' => ['report_document:write']],
|
||||
order: ['id' => 'DESC'],
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['commercialReport' => 'exact'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'report_document')]
|
||||
#[ORM\EntityListeners([ReportDocumentListener::class])]
|
||||
class ReportDocument
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: CommercialReport::class, inversedBy: 'documents')]
|
||||
#[ORM\JoinColumn(name: 'commercial_report_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['report_document:read', 'report_document:write'])]
|
||||
private ?CommercialReport $commercialReport = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?string $originalName = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?string $fileName = null;
|
||||
|
||||
#[ORM\Column(length: 100)]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?string $mimeType = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?int $size = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: UserInterface::class)]
|
||||
#[ORM\JoinColumn(name: 'uploaded_by_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['report_document:read', 'commercial_report:read'])]
|
||||
private ?UserInterface $uploadedBy = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCommercialReport(): ?CommercialReport
|
||||
{
|
||||
return $this->commercialReport;
|
||||
}
|
||||
|
||||
public function setCommercialReport(?CommercialReport $commercialReport): static
|
||||
{
|
||||
$this->commercialReport = $commercialReport;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOriginalName(): ?string
|
||||
{
|
||||
return $this->originalName;
|
||||
}
|
||||
|
||||
public function setOriginalName(string $originalName): static
|
||||
{
|
||||
$this->originalName = $originalName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFileName(): ?string
|
||||
{
|
||||
return $this->fileName;
|
||||
}
|
||||
|
||||
public function setFileName(?string $fileName): static
|
||||
{
|
||||
$this->fileName = $fileName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMimeType(): ?string
|
||||
{
|
||||
return $this->mimeType;
|
||||
}
|
||||
|
||||
public function setMimeType(string $mimeType): static
|
||||
{
|
||||
$this->mimeType = $mimeType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize(): ?int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function setSize(int $size): static
|
||||
{
|
||||
$this->size = $size;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUploadedBy(): ?UserInterface
|
||||
{
|
||||
return $this->uploadedBy;
|
||||
}
|
||||
|
||||
public function setUploadedBy(?UserInterface $uploadedBy): static
|
||||
{
|
||||
$this->uploadedBy = $uploadedBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Enum;
|
||||
|
||||
enum ProspectStatus: string
|
||||
{
|
||||
case New = 'new';
|
||||
case Contacted = 'contacted';
|
||||
case Qualified = 'qualified';
|
||||
case Won = 'won';
|
||||
case Lost = 'lost';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::New => 'Nouveau',
|
||||
self::Contacted => 'Contacté',
|
||||
self::Qualified => 'Qualifié',
|
||||
self::Won => 'Gagné',
|
||||
self::Lost => 'Perdu',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Enum;
|
||||
|
||||
enum ReportType: string
|
||||
{
|
||||
case Call = 'call';
|
||||
case Meeting = 'meeting';
|
||||
case Email = 'email';
|
||||
case Note = 'note';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Call => 'Appel',
|
||||
self::Meeting => 'Rendez-vous',
|
||||
self::Email => 'Email',
|
||||
self::Note => 'Note',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Repository;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Address;
|
||||
|
||||
interface AddressRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?Address;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Repository;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Client;
|
||||
|
||||
interface ClientRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?Client;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $criteria
|
||||
* @param null|array<string, string> $orderBy
|
||||
*
|
||||
* @return Client[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Repository;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\CommercialReport;
|
||||
|
||||
interface CommercialReportRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?CommercialReport;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Repository;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Contact;
|
||||
|
||||
interface ContactRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?Contact;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Repository;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\Prospect;
|
||||
|
||||
interface ProspectRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?Prospect;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $criteria
|
||||
* @param null|array<string, string> $orderBy
|
||||
*
|
||||
* @return Prospect[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Directory\Domain\Repository;
|
||||
|
||||
use App\Module\Directory\Domain\Entity\ReportDocument;
|
||||
|
||||
interface ReportDocumentRepositoryInterface
|
||||
{
|
||||
public function findById(int $id): ?ReportDocument;
|
||||
}
|
||||
Reference in New Issue
Block a user