Solution: 1

PHPUnit 10.2.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.14
Configuration: phpunit.xml

..                                                                  2 / 2 (100%)

Time: 00:00.001, Memory: 24.41 MB

Measurements
 ✔ Centimeters can be converted to int
 ✔ Meters can be converted to float

OK (2 tests, 2 assertions)
Output of phpunit --testdox
<?php declare(strict_types=1);

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[CoversClass(Meter::class)]
#[CoversClass(Centimeter::class)]
class MeasurementsTest extends TestCase
{
    #[Test]
    public function centimeters_can_be_converted_to_int(): void
    {
        $centimeters = 23;
        $measure = new Centimeter($centimeters);
        $this->assertEquals($centimeters, $measure->asInt());
    }

    #[Test]
    public function meters_can_be_converted_to_float(): void
    {
        $meters = 1.5;
        $measure = new Meter($meters);
        $this->assertEquals($meters, $measure->asFloat());
    }
}
MeasurementsTest.php

The most important insight when solving this subtask is to create individual classes for meters and centimeters. After all, a centimeter is something completely different from a meter. You don't necessarily agree?

Would we represent a meter and a kilogram together in one class? Probably not, that would be like apples and mushrooms in a bowl.

How about a meter and a yard? Although both are measures of length, the metric system and the imperial system are fundamentally different. We would do well not to mix them in one class.

But centimeters and meters are both part of the metric system, so can we put them in the same class? It is relatively likely that we will soon have to extend our solution to include millimeters or kilometers, or, depending on the application, nanometers or astronomical units - and probably everything in between. We probably agree that this would inflate a single class too much. This is a good indicator for splitting the two units into two classes right now.

Incidentally, this has nothing to do with speculative generalization. We are simply consistently implementing the single responsibility principle. One class represents centimetres, one meter. Each class is responsible for one thing.

Don't be afraid of classes that are too small. Combining is easier than splitting.

If we realize in the future that the two classes contain duplicated code, we can always merge them. This is easier than splitting a complex class. Classes that are too small are always less problematic than classes that are too large.

<?php declare(strict_types=1);

class Centimeter
{
    private readonly int $centimeters;

    public function __construct(int $centimeters)
    {
        $this->centimeters = $centimeters;
    }

    public function asInt(): int
    {
        return $this->centimeters;
    }
}
Centimeter.php

These two classes here are still very small and don't really do anything. But we are still at the very beginning.

<?php declare(strict_types=1);

class Meter
{
    private readonly float $meters;

    public function __construct(float $meters)
    {
        $this->meters = $meters;
    }

    public function asFloat(): float
    {
        return $this->meters;
    }
}
Meter.php

The tests shown here look like constructor tests or getter tests. There are people who say you don't need to test code like this because it's trivial and is usually generated by the IDE or an AI through autocomplete anyway. I am not of this opinion. In my experience, supposedly trivial code is particularly prone to errors because nobody ever looks closely at it.

What motivated us to write these tests? We didn't want to test the constructor or a getter, but we wanted to represent scalar values with value objects and needed a way to get back to the scalar values for further processing. In this sense, the methods asInt() and asFloat() are a replacement for a type conversion such as (float) $meterValueObject, which is not possible in PHP. In my opinion, this is quite different from a getter.

Incidentally, this is also the reason why I prefer asString() to the built-in interceptor function __toString(). I don't always want to convert objects into strings, but often also into other data types, especially for calculations. In fact, __toString() in PHP was originally intended to create a readable representation of an object, for example in exception messages or log files.