feat: add the booking domain layer

This commit is contained in:
fromme committed 2026-09-13 14:54:42 +02:00
1 parent ef8a88ed2a
commit cb407477bf
13 files changed
+693 -6

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\BookingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* One person holding one resource for one whole day.
*
* The unique constraint is the actual guard against double-booking -- checking first and then
* inserting is racy, so callers insert and catch UniqueConstraintViolationException instead.
*/
#[ORM\Entity(repositoryClass: BookingRepository::class)]
#[ORM\UniqueConstraint(name: 'uniq_booking_resource_date', columns: ['resource_id', 'date'])]
class Booking
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private User $user;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]
private Resource $resource;
#[ORM\Column(type: 'date_immutable')]
private \DateTimeImmutable $date;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
public function __construct(User $user, Resource $resource, \DateTimeImmutable $date)
{
$this->user = $user;
$this->resource = $resource;
// Normalised to midnight so the unique constraint cannot be sidestepped by a time part.
$this->date = $date->setTime(0, 0);
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getUser(): User
{
return $this->user;
}
public function getResource(): Resource
{
return $this->resource;
}
public function getDate(): \DateTimeImmutable
{
return $this->date;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function belongsTo(User $user): bool
{
return $this->user->getId() === $user->getId();
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\ResourceRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A bookable thing: a desk or a parking spot.
*/
#[ORM\Entity(repositoryClass: ResourceRepository::class)]
#[ORM\UniqueConstraint(name: 'uniq_resource_label', columns: ['type', 'label'])]
class Resource
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(enumType: ResourceType::class)]
private ResourceType $type;
#[ORM\Column(length: 50)]
#[Assert\NotBlank]
private string $label;
/**
* Id of the matching element in the hand-authored floor plan SVG. Null for anything that is
* bookable but not drawn on the plan.
*/
#[ORM\Column(length: 50, nullable: true)]
private ?string $svgElementId = null;
/**
* Retired resources stay in the table so historical bookings keep resolving; they are just
* not offered any more.
*/
#[ORM\Column]
private bool $active = true;
public function __construct(ResourceType $type, string $label, ?string $svgElementId = null)
{
$this->type = $type;
$this->label = $label;
$this->svgElementId = $svgElementId;
}
public function getId(): ?int
{
return $this->id;
}
public function getType(): ResourceType
{
return $this->type;
}
public function getLabel(): string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->label = $label;
}
public function getSvgElementId(): ?string
{
return $this->svgElementId;
}
public function setSvgElementId(?string $svgElementId): void
{
$this->svgElementId = $svgElementId;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): void
{
$this->active = $active;
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Entity;
enum ResourceType: string
{
case DESK = 'desk';
case PARKING = 'parking';
public function label(): string
{
return match ($this) {
self::DESK => 'Schreibtisch',
self::PARKING => 'Parkplatz',
};
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A local mirror of an identity held by myep.
*
* There is no password: authentication happens against myep over OAuth2, and ephub reads
* /api/userinfo once at login and then relies on its own session.
*/
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'uniq_user_email', columns: ['email'])]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180)]
#[Assert\NotBlank]
#[Assert\Email]
private string $email;
#[ORM\Column(length: 100)]
private string $name;
/**
* Always filtered through a local whitelist before being stored -- the identity provider's
* claim is never trusted verbatim.
*
* @var list<string>
*/
#[ORM\Column]
private array $roles = [];
public function __construct(string $email, string $name)
{
$this->email = $email;
$this->name = $name;
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): string
{
return $this->email;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getUserIdentifier(): string
{
return $this->email;
}
/**
* @return list<string>
*/
public function getRoles(): array
{
return array_values(array_unique([...$this->roles, 'ROLE_USER']));
}
/**
* @param list<string> $roles
*/
public function setRoles(array $roles): void
{
$this->roles = $roles;
}
public function eraseCredentials(): void
{
}
}