Introducing strong native PHP types

Today, after around a month of hacking, I’m pleased to announce my attempt at addressing PHP’s often-criticised weak type system. Some things you may want to know before reading any further:

There is much debate over whether PHP should be strong vs. weakly typed. There are many advocates for a weak / dynamic approach, indeed some would argue that it’s one of PHP’s most attractive features. I am not entering this debate, rather, I’m providing an option for those who want the strong variety. If you prefer weakly-typed PHP, that’s cool with me.
This project is a very much an experiment. Despite having around 150 tests, I haven’t actually used it in production. Nor have I put it through any form of performance testing… if anyone is interested in doing that, please share the results as I’d be very curious to see the outcome!
Technically, the article title is a little misleading. While the package does enforce strong types for strings, integers etc, the package actually uses class wrappers to enforce type strength. In other words, this isn’t a PHP extension or custom version of PHP itself.

You can find the package here (as well as on Packagist): https://github.com/alphametric/strong-native-types
Okay, with that said, let’s dig into things.

Background

PHP has been moving toward a strongly-typed ethos for some time. As of PHP 7.4, we now have strongly-typed class properties, method parameters and return types. However, the main item that is missing, is types within methods. That may come in a future version, however the underlying issue of type coercion is (or at least, is for now) not being addressed.

For those unaware, type coercion is when PHP attempts to ‘massage’ a data type into another one, sometimes with strange results e.g. floatval(“a1.5") == 0.0

For example, while you can use toNullable(); // NullableStringType

NullableStringType::make(‘hello’)->toNonNullable(); // StringType

NullableStringType::make(null)->toNonNullable(); // Throws exception

Extracting values

At some point, you will likely want to retrieve the underlying value within the object. You can do this using the value method:
StringType::from(‘hello’)->value(); // ‘hello’

Each of the types also implements PHP’s magic __toString method, allowing you to skip using the value method in certain situations, e.g using commands like var_dump. In these instances, null values will be rendered as the string ‘null’, while all other values will be passed through the strval method.
If you wish to retrieve the object’s value as a different data type, you can use the to methods to perform a conversion first:
StringType::from(‘1.5’)->toString(); // ‘1.5’

StringType::from(‘1.5’)->toInteger(); // 2

StringType::from(‘1.5’)->toFloat(); // 1.5

StringType::from(‘1.5’)->toBoolean(); // true

As with the conversion methods discussed earlier, the package attempts to address some of PHP’s quirks when converting to native types. See the readme to learn more about what happens under the hood.

When working with nullable types, the object will automatically fall back to defaults when the value is null. These are pre-set to ”, 0, 0.0 or false for string, int, float and bool respectively, however you can change the default by supplying an alternate value as a method parameter:
NullableStringType::from(null)->toString(); // ”

NullableStringType::from(null)->toString(‘test’); // ‘test’

NullableStringType::from(null)->toFloat(1.5); // 1.5

NullableStringType::from(null)->toBoolean(true); // true

NullableStringType::from(null)->toInteger(57); // 57

NOTE : If you override the default, the value must be of a matching type e.g. 1.5 for a float, otherwise an exception will be thrown.

Can I get some helpers?

The package includes some useful helper methods to create instances of the types. The helpers will attempt to create the types directly using the make factories. If that fails, they will attempt to perform a conversion using the from factories. If that also fails, an exception will be thrown.
$object = string(‘test’); // StringType

$object = string(‘test’, $nullable = true); // NullableStringType

$object = float(1.4); // FloatType

$object = float(null, $nullable = true); // NullableFloatType

$object = boolean(true); // BooleanType

$object = boolean(false, $nullable = true); // NullableBooleanType

$object = integer(5); // IntegerType

$object = integer(9, $nullable = true); // NullableIntegerType

Types on steroids (utility methods)

Since the data types are now objects, behaviour can be added to them. The package adds a wide selection of methods, many of which are chainable, allowing you to use a fluent, readable API to modify the underlying data.
These utility methods will enforce the original data type / throw exceptions when the result would alter the type e.g. dividing an int by 2.3.
Here’s an example of performing some math on an int without having to enclose the operations within a nested set of brackets:
// Vanilla PHP
(((4 + 2) – 2) * 2) / 4) // 2

// Package approach
IntegerType::make(4)
->add(2)
->subtract(2)
->multiplyBy(2)
->divideBy(4)

Here’s another example that allows us to manipulate a string.
Looking at the vanilla PHP, we have to break it apart to figure out what is going on, while the package’s utility methods make the code easily readable:
// Vanilla PHP
rtrim(ucwords(explode(‘meet universe’, ‘hello world…meet universe’)[0]), ‘.’) // Hello World

// Package approach
StringType::make(‘hello world…meet universe’)
->before(‘meet universe’)
->trimRight(‘.’)
->capitalize()

Check out the readme to learn more about all of utility methods that are included. If you think some key ones are missing, let me know!

Custom utility methods (macros)

I’ve made a conscious decision not to make the utility libraries too verbose, so as to avoid it being overwhelming. However, it’s likely that you’ll eventually come to a situation where you wish the library had a method that did X. Well, you need not worry, as the package has you covered!
Each of the types also includes a trait that allows you to add your own custom utility methods without having to create a subclass. You can define these methods using a closure like so:
// Register the macro
StringType::macro(‘suffix’, function($suffix) {
return $this->value().’ ‘.$suffix;
});

// Call the method as normal (after registering it)
StringType::make(‘Hello’)->suffix(‘World’) // Hello World

A note about overkill

Common sense would dictate that if you do not intend to do any form of processing / data manipulation, then there is little point in converting a simple native type into an object purely to enforce type safety. Indeed, I actually oppose this kind of behavior.

Wherever possible, code should be kept to its simplest!

If you’re unsure what I’m driving at here, consider the following example in which a string is supplied to the constructor of an exception:
throw new Exception(‘error’);

Since it isn’t going to be manipulated, and since no processing is going to be performed upon it, there is no benefit to doing the following:
throw new Exception(StringType::make(‘error’) -> value());

A brief sidebar

I’ve recently released Pulse — a friendly, affordable server and site monitoring service designed specifically for developers. It includes all the usual hardware monitors, custom service monitors, alerting via various notification channels, logs, as well as full API support. Take a look… https://pulse.alphametric.co
Server and Site Monitoring with Pulse

Wrapping up

Well, that about covers it in terms of what the package does / what you can achieve with it. Please do check it out and see if it is a good fit for you.
I’m anxious to hear feedback on how people feel about this approach. If you love it, please say so. If you hate it, also say so, but let me know why, as I’d like to see if there’s something I may have missed in building the package.
If you’re interested in reading more of my articles, you can follow me here, or also on Twitter for the occasional coding comment.
Thanks, and happy coding!

Link: https://dev.to/mattkingshott/introducing-strong-native-php-types-2gb0