Project “Drift Statistics”. Part 2. Basic Entities

Part 1 of the series – Project “Drift Statistics”. Part 1. Setup
Public page on VKontakte with new episodes without delays of release on habr – Fir DEV

The essence of the machine

The car is the drift pilot's “workhorse”. Let's pay attention to it and form an entity. What should a car have as an entity? Since it is of interest to us, then at least an identifier. Interest? Yes, this is what we will consider important to us. And what is important? This is what is involved in business logic (for example, search). And what is important has an identifier. Therefore, our car will have an identifier, a name (model and brand), and engine parameters. Why don't we separate the name into brand and model? I don't know for sure yet, it seems that this is not very relevant now. Perhaps in the future we will separate for statistics, but not for now.

Now, let's implement our entity, but before writing it, we need to implement ValueObjects for our “parts” of the “car” entity. Create directories:

- Domain
    - Car
        - Entity
        - ValueObject
            - Engine

All our ValueObjects will be similar to each other, so don't be surprised. First, let's make a VO for the ID and name of the car.

ValueObject for car

Domain\Car\ValueObject\CardId.php:

<?php

declare(strict_types=1);

namespace Domain\Car\ValueObject;

final readonly class CarId
{

    private function __construct(private int $value)
    {
    }

    public static function get(int $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self(0);
    }

    public function isNull(): bool
    {
        return $this->value === 0;
    }

    public function equals(self $id): bool
    {
        return $this->value === $id->getValue();
    }

    public function getValue(): int
    {
        return $this->value;
    }

}

What are we using here? In fact, we will create everything through statics. Why? As for me, it will be easier to work with instances that are created not outside, but inside the class. Thus, we can close the logic of creating objects. As you can see, get(int $value) creates an instance of the class with the passed value, and getNull() does the same thing as get(int $value)but “sews” inside itself the logic of creating an object “without value”/”empty” and other synonyms. Well, and additionally we do 2 checks: isNull()which checks if an object is “without value”/”empty”, and equals(self $id)which accepts the same object to compare their values ​​for coincidence.

Domain\Car\ValueObject\CardName.php:

<?php

declare(strict_types=1);

namespace Domain\Car\ValueObject;

final readonly class CarName
{

    private function __construct(private string $value)
    {
    }

    public static function get(string $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self('');
    }

    public function isNull(): bool
    {
        return $this->value === '';
    }

    public function equals(self $name): bool
    {
        return $this->value === $name->getValue();
    }

    public function getValue(): string
    {
        return $this->value;
    }

}

I think the car name class doesn't require a detailed explanation, since it is similar to the car identifier class. Now, let's create a VO for the engine. Why VO and not Entity? It seems to me that the engine is not of individual interest, like the car is. So, what we will do here is the name and power of the engine. But, it seems to me, it would be better to combine them into a VO “engine” for ease of use.

Domain\Car\ValueObject\Engine\EngineName.php:

<?php

declare(strict_types=1);

namespace Domain\Car\ValueObject\Engine;

final readonly class EngineName
{

    private function __construct(private string $value)
    {
    }

    public static function get(string $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self('');
    }

    public function isNull(): bool
    {
        return $this->value === '';
    }

    public function equals(self $name): bool
    {
        return $this->value === $name->getValue();
    }

    public function getValue(): string
    {
        return $this->value;
    }

}

Something familiar, yes? Well, of course, since such things are more or less the same throughout the project.

Domain\Car\ValueObject\Engine\EnginePower.php:

<?php

declare(strict_types=1);

namespace Domain\Car\ValueObject\Engine;

final readonly class EnginePower
{

    private function __construct(private int $value)
    {
    }

    public static function get(int $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self(0);
    }

    public function isNull(): bool
    {
        return $this->value === 0;
    }

    public function equals(self $power): bool
    {
        return $this->value === $power->getValue();
    }

    public function getValue(): int
    {
        return $this->value;
    }

}

I'll keep quiet about this… Otherwise, explaining everything every time would be “extremely useful”. Now, let's combine everything into one VO:

Domain\Car\ValueObject\Engine\Engine.php:

<?php

declare(strict_types=1);

namespace Domain\Car\ValueObject\Engine;

final readonly class Engine
{

    private function __construct(
        private EngineName  $name,
        private EnginePower $power
    )
    {
    }

    public static function get(EngineName $name, EnginePower $power): self
    {
        return new self($name, $power);
    }

    public static function getNull(): self
    {
        return new self(EngineName::getNull(), EnginePower::getNull());
    }

    public function isNull(): bool
    {
        return $this->name->isNull() || $this->power->isNull();
    }

    public function getName(): EngineName
    {
        return $this->name;
    }

    public function getPower(): EnginePower
    {
        return $this->power;
    }

}

Here, a few clarifications. What do we consider “absence” of an engine? Ideally, when it is not in the car, but in the code – this is the absence of a name or power. Yes, this may be a little incorrect, but for now it will do.

Now let's move on to creating the machine entity.

Entity machines

First, we need to define an entity interface. Why? Because we will have both a real machine entity and an entity of “absence” of a machine (ala nullable object).

Domain\Car\Entity\ICarEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Car\Entity;

use Domain\Car\ValueObject\CarId;
use Domain\Car\ValueObject\CarName;
use Domain\Car\ValueObject\Engine\Engine;

interface ICarEntity
{

    public function getName(): CarName;
    public function getEngine(): Engine;
    public function getId(): CarId;
    public function isNull(): bool;

}

Do you need explanations? I don't think so, but I'll explain it once just in case. The interface has VO retrieval: name, engine and vehicle identifier. What is it for? isNull()? And this is just for checking: “is the machine object a Nullable Object?”, that is, its “absence”.

Now let's implement the essence of the “absence” of the machine:

Domain\Car\Entity\NullCarEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Car\Entity;

use Domain\Car\ValueObject\CarId;
use Domain\Car\ValueObject\CarName;
use Domain\Car\ValueObject\Engine\Engine;

final readonly class NullCarEntity implements ICarEntity
{

    public function getName(): CarName
    {
        return CarName::getNull();
    }

    public function getEngine(): Engine
    {
        return Engine::getNull();
    }

    public function getId(): CarId
    {
        return CarId::getNull();
    }

    public function isNull(): bool
    {
        return true;
    }

}

Simple, right? Well, that's what I think. Now let's implement the essence of the machine.

Domain\Car\Entity\CarEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Car\Entity;

use Domain\Car\ValueObject\CarId;
use Domain\Car\ValueObject\CarName;
use Domain\Car\ValueObject\Engine\Engine;

final readonly class CarEntity implements ICarEntity
{

    public function __construct(
        private CarId   $id,
        private CarName $name,
        private Engine  $engine,
    )
    {
    }

    public function getName(): CarName
    {
        return $this->name;
    }

    public function getEngine(): Engine
    {
        return $this->engine;
    }

    public function getId(): CarId
    {
        return $this->id;
    }

    public function isNull(): bool
    {
        return false;
    }

}

As you can see, here we pass the required VOs for creation through the class constructor, and then simply return them upon receipt.

We are done with the car. Now we need to give the car to someone for use. And who? To the racer, of course. And we will begin with a short introduction.

The essence of a racer

Here we can have a little fun. The racer will have: ID, full name, number (under which he competes), date of birth, country, city and car. Why so many? It seems to me that such a set can somehow successfully fit into more extended statistics, both software and intellectual (brain). For example, I would be interested in comparing two pilots, and also finding out their age, to understand what the young ones can or cannot… Yes, it is strange, but in fact even the age difference can become an interesting subject of analysis.

Create directories:

- Domain
    - Racer
        - Entity
        - ValueObject

Let's begin. I'll say right away that I won't dwell on uninteresting moments. So, read carefully and understand.

Racer ValueObject

Domain\Racer\ValueObject\RacerId.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\ValueObject;

final readonly class RacerId
{

    private function __construct(private int $value)
    {
    }

    public static function get(int $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self(0);
    }

    public function isNull(): bool
    {
        return $this->value === 0;
    }

    public function equals(self $id): bool
    {
        return $this->value === $id->getValue();
    }

    public function getValue(): int
    {
        return $this->value;
    }

}

Domain\Racer\ValueObject\RacerNumber.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\ValueObject;

final readonly class RacerNumber
{

    private function __construct(private int $value)
    {
    }

    public static function get(int $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self(0);
    }

    public function isNull(): bool
    {
        return $this->value === 0;
    }

    public function equals(self $value): bool
    {
        return $this->value === $value->getValue();
    }

    public function getValue(): int
    {
        return $this->value;
    }

}

Domain\Racer\ValueObject\RacerCity.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\ValueObject;

final readonly class RacerCity
{

    private function __construct(private string $value)
    {
    }

    public static function get(string $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self('');
    }

    public function isNull(): bool
    {
        return $this->value === '';
    }

    public function equals(self $value): bool
    {
        return $this->value === $value->getValue();
    }

    public function getValue(): string
    {
        return $this->value;
    }

}

Domain\Racer\ValueObject\RacerCountry.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\ValueObject;

final readonly class RacerCountry
{

    private function __construct(private string $value)
    {
    }

    public static function get(string $value): self
    {
        return new self($value);
    }

    public static function getNull(): self
    {
        return new self('');
    }

    public function isNull(): bool
    {
        return $this->value === '';
    }

    public function equals(self $value): bool
    {
        return $this->value === $value->getValue();
    }

    public function getValue(): string
    {
        return $this->value;
    }

}

Now to the interesting part. Let's start with the full name.

Domain\Racer\ValueObject\RacerFullName.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\ValueObject;

final readonly class RacerFullName
{

    private function __construct(
        private string $first_name, 
        private string $last_name, 
        private string $patronymic
    )
    {
    }

    public static function get(string $first_name, string $last_name, string $patronymic): self
    {
        return new self($first_name, $last_name, $patronymic);
    }

    public static function getNull(): self
    {
        return new self('', '', '');
    }

    public function isNull(): bool
    {
        return "{$this->first_name}{$this->last_name}{$this->patronymic}" === '';
    }

    public function getFirstName(): string
    {
        return $this->first_name;
    }

    public function getLastName(): string
    {
        return $this->last_name;
    }

    public function getValue(): string
    {
        return $this->last_name . ' ' . $this->first_name . ' ' . $this->patronymic;
    }

    public function getPatronymic(): string
    {
        return $this->patronymic;
    }

    public function equals(self $full_name): bool
    {
        return $this->getValue() === $full_name->getValue();
    }

}

We receive the last name, first name and patronymic at the input. In receiving the value getValue() we concatenate it through a space. In the comparison method equals() we just compare the values. There is a nuance here… In fact, it will be a comparison of three spaces with three spaces, if we compare two null object. In principle, nothing terrible, but not so good either.

Now let's move on to the date of birth. It's almost the same here, but there are some small differences – the data type.

Domain\Racer\ValueObject\RacerDateOfBirth.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\ValueObject;

final readonly class RacerDateOfBirth
{

    private function __construct(
        private int $day, 
        private int $month, 
        private int $year
    )
    {
    }

    public static function get(int $day, int $month, int $year): self
    {
        return new self($day, $month, $year);
    }

    public static function getNull(): self
    {
        return new self(0, 0, 0);
    }

    public function isNull(): bool
    {
        return $this->day + $this->month + $this->year === 0;
    }

    public function getDay(): int
    {
        return $this->day;
    }

    public function getMonth(): int
    {
        return $this->month;
    }

    public function getYear(): int
    {
        return $this->year;
    }

    public function equals(self $date_of_birth): bool
    {
        $current = "{$this->day}{$this->month}{$this->year}";
        $to_find = "{$date_of_birth->day}{$date_of_birth->month}{$date_of_birth->year}";

        return $current === $to_find;
    }

}

Ooooh… There's nothing here. getValue(). Why? Because there is no point in returning the date of birth value without formatting. And date formatting can be left here, but it is better to use it outside of VO, as it seems to me. Although, on the other hand, we can lay down a standard date format, and then do formatting someday. Let's do it!

public function getValue(): string
{
    return $this->year . '-' . $this->month . '-' . $this->day;
}

As we have seen, we are in the method equals(self $date_of_birth) made a strange comparison. They took the numbers, turned them into a string, and then compared them. What??? Yes, if you sum them up, you can get a combination of data, when different dates will give the same number. Therefore, it was done this way. But, we immediately decided to add getValue()… Then, comparison equals(self $date_of_birth) now you can write like this:

public function equals(self $date_of_birth): bool
{
    return $this->getValue() === $date_of_birth->getValue();
}

Much simpler and more logical. All, finally finished with VO and can move on to entities.

Racer Entity

Let's start as usual with the interface, where we will describe the methods in a similar way.

Domain\Racer\Entity\IRacerEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\Entity;

use Domain\Car\Entity\ICarEntity;
use Domain\Racer\ValueObject\RacerCity;
use Domain\Racer\ValueObject\RacerCountry;
use Domain\Racer\ValueObject\RacerDateOfBirth;
use Domain\Racer\ValueObject\RacerFullName;
use Domain\Racer\ValueObject\RacerId;
use Domain\Racer\ValueObject\RacerNumber;

interface IRacerEntity
{

    public function getId(): RacerId;
    public function getFullName(): RacerFullName;
    public function getNumber(): RacerNumber;
    public function getDateOfBirth(): RacerDateOfBirth;
    public function getCounty(): RacerCountry;
    public function getCity(): RacerCity;
    public function getCar(): ICarEntity;

}

Well, everything is simple here, but the only difference from the car entity is that the car is added to the racer entity. Thus, we have linked the racer and the car. But, there may be a problem here, that the racer may have several cars. For example, the racer changes the car every year. It turns out that when we somehow record statistics, we need to take into account the car at a specific moment. When is this necessary? When we look at detailed statistics, the car is needed, but not in the general statistics. Therefore, there is a feeling that the racer's current car is just the current car. In short, let's leave it like this for now, and when we get to statistics, perhaps we will redo this story with the racer. So far I do not know for sure and I am already a little confused myself. Ideally, looking ahead, we need some table in the database (DB), where the racer and the car will be connected. And the link to this specific connection must be indicated in the statistics. Then we will get a more logical option. Yes, it sounds logical, but to what extent the entire list of racer cars is needed – I do not know. Perhaps, to simply display this information on the driver's personal page. But, in that case, we also need the date of the car change, its “activity” and so on. Let's probably skip this property for now and come back to it when we work on the statistics. And then, believe me, it will be sooo much fun.

Domain\Racer\Entity\IRacerEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\Entity;

use Domain\Racer\ValueObject\RacerCity;
use Domain\Racer\ValueObject\RacerCountry;
use Domain\Racer\ValueObject\RacerDateOfBirth;
use Domain\Racer\ValueObject\RacerFullName;
use Domain\Racer\ValueObject\RacerId;
use Domain\Racer\ValueObject\RacerNumber;

interface IRacerEntity
{

    public function getId(): RacerId;
    public function getFullName(): RacerFullName;
    public function getNumber(): RacerNumber;
    public function getDateOfBirth(): RacerDateOfBirth;
    public function getCounty(): RacerCountry;
    public function getCity(): RacerCity;

}

Let's decide later, and now let's write the RacerEntity and NullRacerEntity entities.

Domain\Racer\Entity\NullRacerEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\Entity;

use Domain\Car\Entity\ICarEntity;
use Domain\Car\Entity\NullCarEntity;
use Domain\Racer\ValueObject\RacerCity;
use Domain\Racer\ValueObject\RacerCountry;
use Domain\Racer\ValueObject\RacerDateOfBirth;
use Domain\Racer\ValueObject\RacerFullName;
use Domain\Racer\ValueObject\RacerId;
use Domain\Racer\ValueObject\RacerNumber;
use Domain\Team\Entity\ITeamEntity;
use Domain\Team\Entity\NullTeamEntity;

final readonly class NullRacerEntity implements IRacerEntity
{

    public function getId(): RacerId
    {
        return RacerId::getNull();
    }

    public function getFullName(): RacerFullName
    {
        return RacerFullName::getNull();
    }

    public function getNumber(): RacerNumber
    {
        return RacerNumber::getNull();
    }

    public function getDateOfBirth(): RacerDateOfBirth
    {
        return RacerDateOfBirth::getNull();
    }

    public function getCounty(): RacerCountry
    {
        return RacerCountry::getNull();
    }

    public function getCity(): RacerCity
    {
        return RacerCity::getNull();
    }

}

Domain\Racer\Entity\RacerEntity.php:

<?php

declare(strict_types=1);

namespace Domain\Racer\Entity;

use Domain\Racer\ValueObject\RacerCity;
use Domain\Racer\ValueObject\RacerCountry;
use Domain\Racer\ValueObject\RacerDateOfBirth;
use Domain\Racer\ValueObject\RacerFullName;
use Domain\Racer\ValueObject\RacerId;
use Domain\Racer\ValueObject\RacerNumber;

final readonly class RacerEntity implements IRacerEntity
{

    public function __construct(
        private RacerId          $id,
        private RacerNumber      $number,
        private RacerFullName    $full_name,
        private RacerDateOfBirth $date_of_birth,
        private RacerCountry     $county,
        private RacerCity        $city,
    )
    {
    }

    public function getId(): RacerId
    {
        return $this->id;
    }

    public function getFullName(): RacerFullName
    {
        return $this->full_name;
    }

    public function getNumber(): RacerNumber
    {
        return $this->number;
    }

    public function getDateOfBirth(): RacerDateOfBirth
    {
        return $this->date_of_birth;
    }

    public function getCounty(): RacerCountry
    {
        return $this->county;
    }

    public function getCity(): RacerCity
    {
        return $this->city;
    }

}

Here we go. At this point we will finish forming the basic entities for subsequent statistics. In fact, we managed to describe the basic entities at the top level, which will appear everywhere later. Only questions of technical implementation of data storage for statistics remain, which we will talk about another time.

The original article and new episodes are in my public page on VKontakte Fir DEV

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *