loading...

Introducing strong native PHP types

mattkingshott profile image Matt Kingshott 👨🏻‍💻 Originally published at itnext.io on ・8 min read

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:

  1. 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.
  2. 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!
  3. 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 <?php declare(strict_types = 1); to enforce an int parameter in a method signature, there is nothing to stop you from changing it to a string within the method itself.

How to address this?

By switching to objects, or more specifically object wrappers around the native PHP data types (string, int, float and bool), PHP will prevent you from engaging in this sort of behaviour.

When you need to convert to another type, you can instead defer to a custom conversion method that has the appropriate logic to handle valid conversions, and to throw an exception when the conversion is not possible e.g. converting 'Hello World' into a bool value.

In my opinion, this is a step in the right direction.

What are the caveats?

The main “problem” with his approach, assuming you accept that it is a problem in and of itself, is performance.

Since you are now working with objects instead of simple data types, PHP has to do a bit more work / use more memory. In this respect, tasks will take slightly longer to complete.

For most applications, the impact is often negligible (milliseconds or even nanoseconds). However, for applications where performance is extremely important, using this approach may not be preferable.

Getting started

As usual, all you need to do, is pull in the package using Composer:

composer require alphametric/strong-native-types

You can then begin using the class wrappers by importing them:

use Alphametric\Strong\Types\FloatType;
use Alphametric\Strong\Types\StringType;
use Alphametric\Strong\Types\BooleanType;
use Alphametric\Strong\Types\IntegerType;

If you wish to allow null values, you’ll need to import the nullable variety:

use Alphametric\Strong\Types\NullableFloatType;
use Alphametric\Strong\Types\NullableStringType;
use Alphametric\Strong\Types\NullableBooleanType;
use Alphametric\Strong\Types\NullableIntegerType;

At this point, you can use them as you would any other object e.g. as a method parameter, class property, variable etc.

NOTE : In a perfect world, the classes would not include the ‘Type’ suffix, however in PHP 7, String and Float became reserved words. As a result, a suffix had to be added to enable compilation.

Creating instances

To create an instance, use the new keyword, or the make factory method:

$string = new StringType('hello');

$string = StringType::make('hello');

Both of the above approaches throw an exception if the supplied parameter is not of the correct type e.g.

$int = new IntegerType('hello'); // Throws exception

You may also pass an instance of the same object type. This is useful when using utility methods (e.g. adding one IntegerType to another):

$int = new IntegerType(IntegerType::make(3));

As you would expect, if you supply an incompatible strong type, PHP will throw the same exception as the example shown above:

new IntegerType(StringType::make('hello')); // Throws exception

Converting between types

If you wish to convert a different data type e.g. a native PHP float into a StringType, you should use the from factory method:

$string = StringType::from(1.5); // '1.5'

In situations where a sensible / realistic data conversion is not possible, an exception will be thrown e.g.

$bool = BooleanType::from('hello'); // Throws exception

NOTE : You cannot “convert” a nullable type to a non-nullable type. For that, you’ll need to cast the object. See “casting” below.

Addressing quirks

The underlying conversion logic of the package also attempts to address some of the unique quirks of PHP’s type coercion.

For example, consider that in PHP, converting 0 to a bool results in false, while converting a negative number e.g. -1 results in true.

You can find details on all of the various “fixes” that the package implements by examining the relevant section in the readme.

Casting & nullables

If you wish to convert a nullable type to a non-nullable type, or vice-versa, call the corresponding casting methods:

StringType::make('hello')->toNullable(); // NullableStringType

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

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

Immutability

All created types are immutable, allowing you to easily create new instances while not modifying the originals e.g.

$x = StringType::from('hello');
$y = $x->append('world');

// $x = 'hello'
// $y = 'helloworld'

NOTE: Prior to version 2 of the package, you could switch between mutability states, however after some further thinking, this feature has been removed in favor of only using immutable types.

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!


Posted on by:

mattkingshott profile

Matt Kingshott 👨🏻‍💻

@mattkingshott

Founder. Developer. Writer. Lunatic. Created Pulse, IodineJS, Axiom, and more. #PHP #Laravel #Vue #TailwindCSS

Discussion

markdown guide
 

I love the idea of strong types and appreciate the effort here! This is a similar approach to how Java handles types. But you're taking native primitive types and wrapping them in your own logic. What benefit is it to me to use this library of Types rather than just the primitive versions?

 

Thanks for the reply.

The primary motivation for using it is to prevent type coercion, enforce whether null is permitted or not, and gain access to a readable, fluent API for interacting with data (see the examples).

 

why did you choose to have the mutable option? It looks like that you can do everything you need with the immutable version, and get more type safety

 

Thanks for the reply.

I’m not sure I understand your question. When you make a type immutable, you can’t change its value. So, utility methods will trigger an exception. All you can do is change its type.

If a type is mutable, you can modify its value, as well as convert it to another type.

 

The point was not the meaning of mutability/immutability. The point was that mutability makes the whole typing weaker, and IMHO should be avoided if possible.

It would be safer to have everything immutable and just return new values (with clone) instead of modifying the current ones

After further thought, I agree and have released an updated version (2.0) that makes all utility methods return new instances, thus providing immutability.

 

Great effort! And I've just added a star from my side. :-) :-) I like that you've also made an attempt at cleaning up the mess there is.

In other words, this isn’t a PHP extension or custom version of PHP itself

And that is the best approach, in my opinion. The last thing we want is another competing flavor or an extension that no one can seem to install. :-D

(The code in this post doesn't have syntax highlighting; maybe you missed adding the "php" part to the Markdown?)

 

Thanks @ankush981 !

The post was a crosspost from Medium and it didn't add the PHP markdown. I've updated the post to use the PHP syntax highlighting.

 
 

Wow, really nice. Utility methods bought me completely. Gonna star and wish you good luck in further development. 👌