152 lines
4.1 KiB
PHP
152 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Entity;
|
|
|
|
use App\Model\TimeRange;
|
|
use App\Repository\BookingRepository;
|
|
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
/**
|
|
* One person holding one resource for a span of one day.
|
|
*
|
|
* There is deliberately NO unique constraint on (resource, date) any more: since a booking
|
|
* covers a time range, the thing to prevent is an *overlap*, which no unique index can express
|
|
* and which MariaDB has no exclusion constraint for either. BookingManager serialises instead --
|
|
* it locks the resource row FOR UPDATE and checks for overlaps inside that transaction. The
|
|
* index below is what makes that check cheap.
|
|
*/
|
|
#[ORM\Entity(repositoryClass: BookingRepository::class)]
|
|
#[ORM\Index(name: 'idx_booking_resource_date', columns: ['resource_id', 'date'])]
|
|
#[ORM\Index(name: 'idx_booking_user_date', columns: ['user_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;
|
|
|
|
/** Minutes from midnight, end-exclusive. All-day is 0 to 1440. */
|
|
#[ORM\Column]
|
|
private int $startMinute;
|
|
|
|
#[ORM\Column]
|
|
private int $endMinute;
|
|
|
|
/**
|
|
* The series this occurrence was materialised from, if any. SET NULL rather than CASCADE so
|
|
* that dropping a series never silently takes bookings with it -- ending a series removes
|
|
* its future occurrences explicitly, in BookingManager.
|
|
*/
|
|
#[ORM\ManyToOne(inversedBy: 'bookings')]
|
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
|
private ?BookingSeries $series = null;
|
|
|
|
/** True once this occurrence's times were changed away from the series' own. */
|
|
#[ORM\Column]
|
|
private bool $overridden = false;
|
|
|
|
#[ORM\Column(type: 'datetime_immutable')]
|
|
private \DateTimeImmutable $createdAt;
|
|
|
|
public function __construct(
|
|
User $user,
|
|
Resource $resource,
|
|
\DateTimeImmutable $date,
|
|
?TimeRange $time = null,
|
|
?BookingSeries $series = null,
|
|
) {
|
|
$time ??= TimeRange::wholeDay();
|
|
|
|
$this->user = $user;
|
|
$this->resource = $resource;
|
|
// Normalised to midnight so a stray time component cannot shift which day this belongs to.
|
|
$this->date = $date->setTime(0, 0);
|
|
$this->startMinute = $time->startMinute;
|
|
$this->endMinute = $time->endMinute;
|
|
$this->series = $series;
|
|
$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 getTimeRange(): TimeRange
|
|
{
|
|
return TimeRange::fromMinutes($this->startMinute, $this->endMinute);
|
|
}
|
|
|
|
/**
|
|
* Changing the times of an occurrence marks it as an override, so the UI can show that it
|
|
* no longer matches its series.
|
|
*/
|
|
public function changeTime(TimeRange $time): void
|
|
{
|
|
$this->startMinute = $time->startMinute;
|
|
$this->endMinute = $time->endMinute;
|
|
|
|
if (null !== $this->series) {
|
|
$this->overridden = !$this->series->getTimeRange()->equals($time);
|
|
}
|
|
}
|
|
|
|
public function getSeries(): ?BookingSeries
|
|
{
|
|
return $this->series;
|
|
}
|
|
|
|
public function isPartOfSeries(): bool
|
|
{
|
|
return null !== $this->series;
|
|
}
|
|
|
|
public function isOverridden(): bool
|
|
{
|
|
return $this->overridden;
|
|
}
|
|
|
|
public function getCreatedAt(): \DateTimeImmutable
|
|
{
|
|
return $this->createdAt;
|
|
}
|
|
|
|
public function belongsTo(User $user): bool
|
|
{
|
|
return $this->user->getId() === $user->getId();
|
|
}
|
|
|
|
public function overlaps(TimeRange $time): bool
|
|
{
|
|
return $this->getTimeRange()->overlaps($time);
|
|
}
|
|
}
|