421 lines
12 KiB
PHP
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;
|
|
}
|
|
}
|