src/Entity/Tuk.php line 16

  1. <?php
  2. namespace App\Entity;
  3. use App\Repository\TukRepository;
  4. use Doctrine\ORM\Mapping as ORM;
  5. /**
  6.  * A tuk-tuk. This is the entity that carries a live GPS position: the driver's
  7.  * phone pings its coordinates here, and the passenger's map draws them.
  8.  *
  9.  * @ORM\Entity(repositoryClass=TukRepository::class)
  10.  * @ORM\Table(name="tuk")
  11.  * @ORM\HasLifecycleCallbacks()
  12.  */
  13. class Tuk
  14. {
  15.     public const STATUS_OFFLINE   'offline';   // driver not working
  16.     public const STATUS_AVAILABLE 'available'// free, shown to passengers
  17.     public const STATUS_BUSY      'busy';      // on a tour
  18.     public const STATUSES = [
  19.         self::STATUS_OFFLINE,
  20.         self::STATUS_AVAILABLE,
  21.         self::STATUS_BUSY,
  22.     ];
  23.     /**
  24.      * A position older than this is stale: the driver closed the app or lost
  25.      * signal, so the tuk must not be offered to passengers.
  26.      */
  27.     public const PING_TIMEOUT_SECONDS 60;
  28.     /**
  29.      * @ORM\Id
  30.      * @ORM\GeneratedValue
  31.      * @ORM\Column(type="integer")
  32.      */
  33.     private ?int $id null;
  34.     /**
  35.      * Owning side of the one-to-one. A tuk with no driver is parked.
  36.      * @ORM\OneToOne(targetEntity=Driver::class, inversedBy="tuk")
  37.      * @ORM\JoinColumn(nullable=true, unique=true, onDelete="SET NULL")
  38.      */
  39.     private ?Driver $driver null;
  40.     /**
  41.      * Fleet code shown to passengers, e.g. "TNT-01".
  42.      * @ORM\Column(type="string", length=20, unique=true)
  43.      */
  44.     private string $code;
  45.     /**
  46.      * @ORM\Column(type="string", length=20, nullable=true)
  47.      */
  48.     private ?string $plateNumber null;
  49.     /**
  50.      * Hex paint colour of the vehicle, used for its map marker.
  51.      * @ORM\Column(type="string", length=7, options={"default": "#E8A020"})
  52.      */
  53.     private string $color '#E8A020';
  54.     /**
  55.      * @ORM\Column(type="integer", options={"default": 3})
  56.      */
  57.     private int $seats 3;
  58.     /**
  59.      * @ORM\Column(type="string", length=500, nullable=true)
  60.      */
  61.     private ?string $photo null;
  62.     /**
  63.      * One of self::STATUSES.
  64.      * @ORM\Column(type="string", length=10, options={"default": "offline"})
  65.      */
  66.     private string $status self::STATUS_OFFLINE;
  67.     /**
  68.      * @ORM\Column(type="decimal", precision=10, scale=7, nullable=true)
  69.      */
  70.     private ?float $latitude null;
  71.     /**
  72.      * @ORM\Column(type="decimal", precision=10, scale=7, nullable=true)
  73.      */
  74.     private ?float $longitude null;
  75.     /**
  76.      * Direction of travel in degrees (0 = north), so the marker can point.
  77.      * @ORM\Column(type="smallint", nullable=true)
  78.      */
  79.     private ?int $heading null;
  80.     /**
  81.      * When the driver's phone last reported a position.
  82.      * @ORM\Column(type="datetime_immutable", nullable=true)
  83.      */
  84.     private ?\DateTimeImmutable $lastPingAt null;
  85.     /**
  86.      * @ORM\Column(type="boolean", options={"default": true})
  87.      */
  88.     private bool $isActive true;
  89.     /**
  90.      * @ORM\Column(type="datetime_immutable")
  91.      */
  92.     private \DateTimeImmutable $createdAt;
  93.     /**
  94.      * @ORM\Column(type="datetime_immutable", nullable=true)
  95.      */
  96.     private ?\DateTimeImmutable $updatedAt null;
  97.     public function __construct()
  98.     {
  99.         $this->createdAt = new \DateTimeImmutable();
  100.     }
  101.     /**
  102.      * @ORM\PreUpdate()
  103.      */
  104.     public function onPreUpdate(): void
  105.     {
  106.         $this->updatedAt = new \DateTimeImmutable();
  107.     }
  108.     public function getId(): ?int
  109.     {
  110.         return $this->id;
  111.     }
  112.     public function getDriver(): ?Driver
  113.     {
  114.         return $this->driver;
  115.     }
  116.     public function setDriver(?Driver $driver): self
  117.     {
  118.         $this->driver $driver;
  119.         // Keep the inverse side in sync so Driver::getTuk() works on objects
  120.         // that were changed in this request but not yet reloaded.
  121.         if ($driver !== null && $driver->getTuk() !== $this) {
  122.             $driver->setTuk($this);
  123.         }
  124.         return $this;
  125.     }
  126.     public function getCode(): string
  127.     {
  128.         return $this->code;
  129.     }
  130.     public function setCode(string $code): self
  131.     {
  132.         $this->code $code;
  133.         return $this;
  134.     }
  135.     public function getPlateNumber(): ?string
  136.     {
  137.         return $this->plateNumber;
  138.     }
  139.     public function setPlateNumber(?string $plateNumber): self
  140.     {
  141.         $this->plateNumber $plateNumber;
  142.         return $this;
  143.     }
  144.     public function getColor(): string
  145.     {
  146.         return $this->color;
  147.     }
  148.     public function setColor(string $color): self
  149.     {
  150.         $this->color $color;
  151.         return $this;
  152.     }
  153.     public function getSeats(): int
  154.     {
  155.         return $this->seats;
  156.     }
  157.     public function setSeats(int $seats): self
  158.     {
  159.         $this->seats $seats;
  160.         return $this;
  161.     }
  162.     public function getPhoto(): ?string
  163.     {
  164.         return $this->photo;
  165.     }
  166.     public function setPhoto(?string $photo): self
  167.     {
  168.         $this->photo $photo;
  169.         return $this;
  170.     }
  171.     public function getStatus(): string
  172.     {
  173.         return $this->status;
  174.     }
  175.     public function setStatus(string $status): self
  176.     {
  177.         if (!in_array($statusself::STATUSEStrue)) {
  178.             throw new \InvalidArgumentException(sprintf(
  179.                 'Invalid status "%s". Allowed: %s',
  180.                 $status,
  181.                 implode(', 'self::STATUSES)
  182.             ));
  183.         }
  184.         $this->status $status;
  185.         return $this;
  186.     }
  187.     public function getLatitude(): ?float
  188.     {
  189.         return $this->latitude;
  190.     }
  191.     public function setLatitude(?float $latitude): self
  192.     {
  193.         $this->latitude $latitude;
  194.         return $this;
  195.     }
  196.     public function getLongitude(): ?float
  197.     {
  198.         return $this->longitude;
  199.     }
  200.     public function setLongitude(?float $longitude): self
  201.     {
  202.         $this->longitude $longitude;
  203.         return $this;
  204.     }
  205.     public function getHeading(): ?int
  206.     {
  207.         return $this->heading;
  208.     }
  209.     public function setHeading(?int $heading): self
  210.     {
  211.         $this->heading $heading === null null : (($heading 360) + 360) % 360;
  212.         return $this;
  213.     }
  214.     public function getLastPingAt(): ?\DateTimeImmutable
  215.     {
  216.         return $this->lastPingAt;
  217.     }
  218.     public function setLastPingAt(?\DateTimeImmutable $lastPingAt): self
  219.     {
  220.         $this->lastPingAt $lastPingAt;
  221.         return $this;
  222.     }
  223.     /**
  224.      * Record a GPS fix from the driver's phone.
  225.      */
  226.     public function ping(float $latitudefloat $longitude, ?int $heading null): self
  227.     {
  228.         $this->latitude   $latitude;
  229.         $this->longitude  $longitude;
  230.         $this->lastPingAt = new \DateTimeImmutable();
  231.         if ($heading !== null) {
  232.             $this->setHeading($heading);
  233.         }
  234.         return $this;
  235.     }
  236.     public function isActive(): bool
  237.     {
  238.         return $this->isActive;
  239.     }
  240.     public function setIsActive(bool $isActive): self
  241.     {
  242.         $this->isActive $isActive;
  243.         return $this;
  244.     }
  245.     public function getCreatedAt(): \DateTimeImmutable
  246.     {
  247.         return $this->createdAt;
  248.     }
  249.     public function getUpdatedAt(): ?\DateTimeImmutable
  250.     {
  251.         return $this->updatedAt;
  252.     }
  253.     // ── Derived state ────────────────────────────────────────────────────
  254.     /**
  255.      * True when the last GPS ping is recent enough to trust.
  256.      */
  257.     public function isOnline(): bool
  258.     {
  259.         if ($this->status === self::STATUS_OFFLINE || $this->lastPingAt === null) {
  260.             return false;
  261.         }
  262.         $age = (new \DateTimeImmutable())->getTimestamp() - $this->lastPingAt->getTimestamp();
  263.         return $age <= self::PING_TIMEOUT_SECONDS;
  264.     }
  265.     /**
  266.      * Can a passenger book this tuk right now?
  267.      */
  268.     public function isBookable(): bool
  269.     {
  270.         return $this->isActive
  271.             && $this->status === self::STATUS_AVAILABLE
  272.             && $this->latitude !== null
  273.             && $this->longitude !== null
  274.             && $this->driver !== null
  275.             && $this->isOnline();
  276.     }
  277.     public function __toString(): string
  278.     {
  279.         return $this->code ?? '';
  280.     }
  281. }