feat: accept form submissions from the websites over an API endpoint
This commit is contained in:
1 parent
f48e53fdd0
commit
30ad5e8488
17 files changed
+1350
-4
No files matched your search
@@ -0,0 +1,347 @@
|
||||
<?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 in this application creates these
|
||||
* rows, and 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;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* Note the labels come back in MySQL's order, not the form's - a json column normalises an
|
||||
* object by sorting its keys by length. Render it in whatever order suits the reviewer; the
|
||||
* form's own order is still in $payload, which is read as one blob and never sorted by us.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user