G'day:
Whilst working on my recent code implemneting a postcode look-up web service, I stumbled across Monolog/Level:
enum Level: int
{
// …
}
I was unaware of PHP having enums, and it turns out it's a recent addition: arriving in PHP 8.1 (I had moved on from PHP when 7.2 was brand new). See PHP: Enumerations for the docs.
My usage of it was I wanted to get the string "CRITICAL" back from the log level I was testing a log entry for:
public function assertLogEntryIsCorrect(
TestHandler $testHandler,
Level $expectedLogLevel,
int $statusCode,
string $expectedMessage
): void {
$logRecords = $testHandler->getRecords();
$this->assertCount(1, $logRecords);
$this->assertEquals($expectedLogLevel->getName(), $logRecords[0]["level_name"]);
Monolog/Logger has this:
public const CRITICAL = 500;
But nothing associating that const to the string "CRITICAL", yet in the log entry the level is "CRITICAL". Yeah I could have just used strtoupper, but I had suspected there was a better way, so had a look at how Monolog actually writes the log entry. I traced this back to here:
public static function getLevelName(int|Level $level): string
{
return self::toMonologLevel($level)->getName();
}
And from there found the Level enum
I wanted to see what enums in PHP can do, so I worked through the docs, and wrote myself an example of each. I did this via TDD, so I have some tests describing the functionality (see the @testdox annotations for the descriptions):
namespace adamcameron\php8\tests\Unit\System;
use adamcameron\php8\tests\Unit\System\Fixtures\MaoriNumbers as MI;
use PHPUnit\Framework\TestCase;
/** @testdox Testing enums */
class EnumTest extends TestCase
{
/** @testdox It can have static methods */
public function testStaticMethods()
{
$this->assertEquals("one", MI::asEnglish(1));
}
The underlying method in the enum is:
public static function asEnglish(int $i) : string
{
return self::EN[$i - 1];
}
/** @testdox It can have instance methods */
public function testInstanceMethods()
{
$this->assertEquals("two", MI::RUA->toEnglish());
}
The underlying method in the enum is:
public function toEnglish() : string
{
return self::EN[$this->value - 1];
}
/** @testdox Its name can be returned */
public function testGetName()
{
$this->assertEquals("TORU", MI::TORU->name);
}
/** @testdox Its value can be returned */
public function testGetValue()
{
$this->assertEquals(4, MI::WHĀ->value);
}
I purposedly used the correct accented unicode charactcer in WHÄ€ to check if PHP supported non-ASCII charatcers there. Yes.
/** @testdox It encodes to JSON OK */
public function testJsonEncode()
{
$this->assertEquals('{"rima":5}', json_encode(["rima" => MI::RIMA]));
}
/** @testdox It cannot be type-coerced */
public function testTypeCoercion()
{
$this->expectError(); // NB: not an exception; an error
$this->expectErrorMessageMatches("/.*MaoriNumbers could not be converted to int.*/");
$this->assertEquals(sprintf("ono: %d", MI::ONO), "ono: 6");
}
/** @testdox It has a from method */
public function testFrom()
{
$this->assertEquals(MI::from(7), MI::WHITU);
}
/** @testdox It has a from method that throws an exception */
public function testFromException()
{
$this->expectError(\ValueError ::class);
$this->expectErrorMessage();
MI::from(0);
}
I like how IntelliJ warns me I'm going wrong trying to use 0 there:
/** @testdox It has a tryFrom method */
public function testTryFrom()
{
$this->assertEquals(MI::tryFrom(8), MI::WARU);
}
/** @testdox It has a tryFrom method that returns null */
public function testTryFromNull()
{
$this->assertNull(MI::tryFrom(-1));
}
/** @testdox It will type-coerce the argument for from and tryFrom */
public function testTryFromTypeCoercion()
{
$this->assertEquals(MI::from("9"), MI::IWA);
$this->assertEquals(MI::tryFrom(10.0), MI::TEKAU);
}
/** @testdox It can be used in a match expression */
public function testMatch()
{
$this->assertEquals("odd", MI::TAHI->getParity());
$this->assertEquals("even", MI::RUA->getParity());
}
The underlying method in the enum is:
public function getParity() : string
{
return match($this) {
self::TAHI, self::TORU, self::RIMA, self::WHITU, self::IWA => "odd",
self::RUA, self::WHĀ, self::ONO, self::WARU, self::TEKAU => "even"
};
}
Match expressions are new to PHP 8.0, and are an expression version of switch statements. I might need to look at these some more later on too.
/** @testdox It can have consts, and supply the values for same */
public function testConsts()
{
$this->assertEquals(MI::THREE, MI::TORU);
}
/** @testdox It can use traits */
public function testTraits()
{
$this->assertEquals(MI::FOUR, MI::WHĀ);
}
/** @testdox It is an object and has a class const */
public function testClassConst()
{
$this->assertEquals(get_class(MI::RIMA), MI::CLASS);
}
/** @testdox It supports the __invoke magic method */
public function testInvoke()
{
$this->assertEquals("six", (MI::ONO)());
}
The underlying method in the enum is:
public function __invoke() : string
{
return $this->toEnglish();
}
/** @testdox It has a cases method which returns a listing */
public function testCases()
{
$someCases = array_slice(MI::cases(), 6, 4);
$this->assertEquals(
[MI::WHITU, MI::WARU, MI::IWA, MI::TEKAU],
$someCases
);
}
/** @testdox It can be serialised */
public function testSerialize()
{
$this->assertMatchesRegularExpression(
sprintf('/^E:\d+:"%s:%s";$/', preg_quote(MI::TAHI::CLASS), MI::TAHI->name ),
serialize(MI::TAHI)
);
}
}
And the whole enum:
namespace adamcameron\php8\tests\Unit\System\Fixtures;
enum MaoriNumbers : int {
case TAHI = 1;
case RUA = 2;
case TORU = 3;
case WHĀ = 4;
case RIMA = 5;
case ONO = 6;
case WHITU = 7;
case WARU = 8;
case IWA = 9;
case TEKAU = 10;
use MaoriNumbersConstsTrait;
private CONST EN = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"];
public function toEnglish() : string
{
return self::EN[$this->value - 1];
}
public static function asEnglish(int $i) : string
{
return self::EN[$i - 1];
}
public function getParity() : string
{
return match($this) {
self::TAHI, self::TORU, self::RIMA, self::WHITU, self::IWA => "odd",
self::RUA, self::WHĀ, self::ONO, self::WARU, self::TEKAU => "even"
};
}
public function __invoke() : string
{
return $this->toEnglish();
}
}
This also uses a trait:
namespace adamcameron\php8\tests\Unit\System\Fixtures;
use adamcameron\php8\tests\Unit\System\Fixtures\MaoriNumbers as MI;
trait MaoriNumbersConstsTrait {
public const ONE = MI::TAHI;
public const TWO = MI::RUA;
public const THREE = MI::TORU;
public const FOUR = MI::WHĀ;
public const FIVE = MI::RIMA;
public const SIX = MI::ONO;
public const SEVEN = MI::WHITU;
public const EIGHT = MI::WARU;
public const NINE = MI::IWA;
public const TEN = MI::TEKAU;
}
I would not normally use a trait (in pretty much any situation), but I created this for the purposes of the testing.
That's it. I just wanted to share my code of my investigations into a language feature I was previously unaware of. I find this a helpful way of learning new stuff: putting it through its paces and formally observing the results.
Righto.
--
Adam