Files
myep-team/src/Entity/ApplicationFormSubmission.php
T

421 lines
12 KiB
PHP

<?php
namespace App\Entity;
use App\Entity\Traits\TimestampableEntity;
use App\Model\ApplicationFormSubmissionDto;
use App\Repository\ApplicationFormSubmissionRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
/**
* A form submitted on one of the public websites and posted here by TYPO3 - job applications
* first, other "collect it and let a human read it" forms later. Unrelated to
* App\Entity\Application, which is a teamer applying for an assignment.
*
* Deliberately neither Blameable nor SoftDeletable. Nothing here creates a row - TYPO3 does -
* and the only thing staff write are the four follow-up dates, where the interesting fact is
* that the mail went out or the call happened, not who ticked it off; who did is in the audit
* log the mutating controllers write. No SoftDeletable because the retention rule for job
* applications asks for real deletion, so this is the one entity here that gets an actual
* DELETE. A purge has to cover "details" and "payload" as well - a json column is exactly what
* a deletion job forgets.
*
* The typed columns are the mapped core agreed with the TYPO3 side and all nullable: a form is
* configured without a field as often as with it. "details" is everything else the visitor
* filled in, keyed by the German field label in the order the form asked - display only, never
* a schema, never queried on (see .ddev/plans/MYEP-APPLICATION-ENDPOINT.md §3.3).
*/
#[ORM\Entity(repositoryClass: ApplicationFormSubmissionRepository::class)]
// What a list and a dedupe check ask for: the submissions of one form, newest first.
#[ORM\Index(name: 'IDX_F47CE995B5265E72', columns: ['form', 'submitted_at'])]
class ApplicationFormSubmission implements TimestampableEntityInterface
{
use TimestampableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[ORM\Column(length: 64)]
private string $form;
#[ORM\Column(length: 255)]
private string $formLabel;
#[ORM\Column]
private \DateTimeImmutable $submittedAt;
#[ORM\Column(nullable: true)]
private ?int $pageUid = null;
#[ORM\Column(length: 2048, nullable: true)]
private ?string $pageUrl = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $firstName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $lastName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $email = null;
#[ORM\Column(length: 64, nullable: true)]
private ?string $phone = null;
#[ORM\Column(length: 64, nullable: true)]
private ?string $mobile = null;
/**
* A string, not a date: the forms use a text field with a regex rather than a date picker,
* so this is whatever the visitor typed - usually dd.mm.yyyy. Parse it for display, store
* what arrived.
*/
#[ORM\Column(length: 64, nullable: true)]
private ?string $birthDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $street = null;
#[ORM\Column(length: 32, nullable: true)]
private ?string $zipCode = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $city = null;
/**
* The follow-up a reviewer works through after a submission arrived: two mails and two
* phone calls, each stamped when it happened and null until it has. Staff state, never part
* of the payload - fromDto() must not touch them.
*
* The mails carry a time because a send time is a machine fact and stays one once the
* notification mail of §6 is automated. A held call does not: the reviewer records it
* afterwards and knows the day, so storing an invented time would only look precise.
*/
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $firstMailSentAt = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $secondMailSentAt = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $firstCallHeldAt = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $secondCallHeldAt = null;
/**
* Everything the visitor filled in beyond the mapped core, keyed by the German field label.
* Display only: the labels are staff-written and change on a reword, so nothing may query
* this, build a column from it or migrate old rows to a new spelling (§3.3).
*
* The key order is whatever the engine hands back and is not part of what this carries: on
* MariaDB (dev and production) JSON is an alias for LONGTEXT and the document survives
* verbatim, on MySQL 8 an object is normalised and its keys come back sorted by length.
* Render it in whatever order suits the reviewer rather than relying on either.
*
* @var array<string, string|bool|list<string>>
*/
#[ORM\Column(type: Types::JSON)]
private array $details = [];
/**
* The request body as received. Mapped keys this application does not know yet are ignored
* on the way in (§3.2) and the TYPO3 side widens a form's mapping without a release here,
* so without this copy those answers would be lost for good instead of backfillable.
*
* @var array<string, mixed>
*/
#[ORM\Column(type: Types::JSON)]
private array $payload = [];
public function __construct(string $form, string $formLabel, \DateTimeImmutable $submittedAt)
{
$this->uuid = Uuid::v4();
$this->form = $form;
$this->formLabel = $formLabel;
$this->submittedAt = $submittedAt;
}
/**
* The validated payload as a row. Called with a DTO that passed validation - submittedAt is
* parseable by then, which is why the constructor can insist on a date.
*
* The raw payload is passed in separately because the DTO no longer has it: the resolver
* drops every key the DTO has no property for, and those are the mapped fields the TYPO3
* side has added and this application does not know yet (§3.2). Keeping the body is what
* makes them backfillable instead of lost.
*
* @param array<string, mixed> $payload
*/
public static function fromDto(ApplicationFormSubmissionDto $dto, array $payload = []): self
{
$submission = new self($dto->form, $dto->formLabel, $dto->getSubmittedAt());
return $submission
->setPageUid($dto->getPageUid())
->setPageUrl($dto->pageUrl)
->setFirstName($dto->firstName)
->setLastName($dto->lastName)
->setEmail($dto->email)
->setPhone($dto->phone)
->setMobile($dto->mobile)
->setBirthDate($dto->birthDate)
->setStreet($dto->street)
->setZipCode($dto->zipCode)
->setCity($dto->city)
->setDetails($dto->getNormalizedDetails())
->setPayload($payload)
;
}
public function getId(): ?int
{
return $this->id;
}
public function getUuid(): string
{
return $this->uuid;
}
public function getForm(): string
{
return $this->form;
}
public function getFormLabel(): string
{
return $this->formLabel;
}
public function getSubmittedAt(): \DateTimeImmutable
{
return $this->submittedAt;
}
public function getPageUid(): ?int
{
return $this->pageUid;
}
public function setPageUid(?int $pageUid): static
{
$this->pageUid = $pageUid;
return $this;
}
public function getPageUrl(): ?string
{
return $this->pageUrl;
}
public function setPageUrl(?string $pageUrl): static
{
$this->pageUrl = $pageUrl;
return $this;
}
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 getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): static
{
// Same normalization as Contact and Embeddable\Communication: a later match against
// existing people has to compare like with like.
$this->email = null === $email ? null : mb_strtolower(trim($email));
return $this;
}
public function getPhone(): ?string
{
return $this->phone;
}
public function setPhone(?string $phone): static
{
$this->phone = $phone;
return $this;
}
public function getMobile(): ?string
{
return $this->mobile;
}
public function setMobile(?string $mobile): static
{
$this->mobile = $mobile;
return $this;
}
public function getBirthDate(): ?string
{
return $this->birthDate;
}
public function setBirthDate(?string $birthDate): static
{
$this->birthDate = $birthDate;
return $this;
}
public function getStreet(): ?string
{
return $this->street;
}
public function setStreet(?string $street): static
{
$this->street = $street;
return $this;
}
public function getZipCode(): ?string
{
return $this->zipCode;
}
public function setZipCode(?string $zipCode): static
{
$this->zipCode = $zipCode;
return $this;
}
public function getCity(): ?string
{
return $this->city;
}
public function setCity(?string $city): static
{
$this->city = $city;
return $this;
}
public function getFirstMailSentAt(): ?\DateTimeImmutable
{
return $this->firstMailSentAt;
}
public function setFirstMailSentAt(?\DateTimeImmutable $firstMailSentAt): static
{
$this->firstMailSentAt = $firstMailSentAt;
return $this;
}
public function getSecondMailSentAt(): ?\DateTimeImmutable
{
return $this->secondMailSentAt;
}
public function setSecondMailSentAt(?\DateTimeImmutable $secondMailSentAt): static
{
$this->secondMailSentAt = $secondMailSentAt;
return $this;
}
public function getFirstCallHeldAt(): ?\DateTimeImmutable
{
return $this->firstCallHeldAt;
}
public function setFirstCallHeldAt(?\DateTimeImmutable $firstCallHeldAt): static
{
$this->firstCallHeldAt = $firstCallHeldAt;
return $this;
}
public function getSecondCallHeldAt(): ?\DateTimeImmutable
{
return $this->secondCallHeldAt;
}
public function setSecondCallHeldAt(?\DateTimeImmutable $secondCallHeldAt): static
{
$this->secondCallHeldAt = $secondCallHeldAt;
return $this;
}
/**
* @return array<string, string|bool|list<string>>
*/
public function getDetails(): array
{
return $this->details;
}
/**
* @param array<string, string|bool|list<string>> $details
*/
public function setDetails(array $details): static
{
$this->details = $details;
return $this;
}
/**
* @return array<string, mixed>
*/
public function getPayload(): array
{
return $this->payload;
}
/**
* @param array<string, mixed> $payload
*/
public function setPayload(array $payload): static
{
$this->payload = $payload;
return $this;
}
}