Introducing strong native PHP types

Introducing strong native PHP typesMatt KingshottBlockedUnblockFollowFollowingApr 30Today, 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-typesOkay, with that said, let’s dig into things.

BackgroundPHP 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.

0For 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 startedAs usual, all you need to do, is pull in the package using Composer:composer require alphametric/strong-native-typesYou can then begin using the class wrappers by importing them:use AlphametricStrongTypesFloatType;use AlphametricStrongTypesStringType;use AlphametricStrongTypesBooleanType;use AlphametricStrongTypesIntegerType;If you wish to allow null values, you’ll need to import the nullable variety:use AlphametricStrongTypesNullableFloatType;use AlphametricStrongTypesNullableStringType;use AlphametricStrongTypesNullableBooleanType;use AlphametricStrongTypesNullableIntegerType;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 instancesTo 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 exceptionYou 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 exceptionConverting between typesIf 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 exceptionNOTE: You cannot “convert” a nullable type to a non-nullable type.

For that, you’ll need to cast the object.

See “casting” below.

Addressing quirksThe 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 & nullablesIf you wish to convert a nullable type to a non-nullable type, or vice-versa, call the corresponding casting methods:StringType::make('hello')->toNullable(); // NullableStringTypeNullableStringType::make('hello')->toNonNullable(); // StringTypeNullableStringType::make(null)->toNonNullable(); // Throws exceptionImmutabilityBy default, all created types are mutable.

However, you can also set them to be immutable.

Once set, any attempt to modify the underlying data within the type will result in an exception:StringType::from('hello')->toImmutable()->append('world'); // Throws exceptionYou can switch the type back to a mutable state if required:$string = StringType::from('hello')->toImmutable();$string->toMutable()->append('world'); // helloworldYou can also check the mutability status of a type using the helper methods:StringType::from('hello')->toImmutable()->isImmutable(); // trueStringType::from('hello')->toImmutable()->isMutable(); // falseNOTE: While immutable types cannot be manipulated using utility methods (see below), you can still convert them to other types using the conversion methods e.

g.

toBoolean().

Extracting valuesAt 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(); // 2StringType::from('1.

5')->toFloat(); // 1.

5StringType::from('1.

5')->toBoolean(); // trueAs 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.

5NullableStringType::from(null)->toBoolean(true); // trueNullableStringType::from(null)->toInteger(57); // 57NOTE: 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); // NullableIntegerTypeTypes 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 approachIntegerType::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 PHPrtrim(ucwords(explode('meet universe', 'hello world.

meet universe')[0]), '.

') // Hello World// Package approachStringType::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 macroStringType::macro('suffix', function($suffix) { return $this->value().

' '.

$suffix;});// Call the method as normal (after registering it)StringType::make('Hello')->suffix('World') // Hello WorldA note about overkillCommon 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 sidebarI’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.

coServer and Site Monitoring with PulseWrapping upWell, 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!.

. More details

Leave a Reply