Design - Typed PHP: Stronger Types For Cleaner Code (2014)

Typed PHP: Stronger Types For Cleaner Code (2014)

Design

Which Method To Use

We’ve had a look at a number of methods and extensions which can help us to abstract away the inconsistent type handling PHP presents to us. Part of creating this abstraction is deciding on which methods and/or extensions to use.

Namespace Methods

Generally, we should avoid any method that would expose a significant amount of functions in the global scope. We don’t want to create any more clutter than there already is, and being able to use our abstraction, in addition to the standard stuff, is definitely beneficial.

We should endeavour to have all our code reside in namespaces.

Furthermore, we should implement our code in such a way that it can be used both in procedural environments and object-oriented environments. Stated differently; we should contain the business logic within functions and call those functions (so that they can be used in procedural code) from within an object-oriented framework (so that scalar types can be used like objects).

We should build our logic in functions and add those functions (as methods) to scalar type objects.

Scalar Objects / SPL Types

This means we will want to use the namespace functions we covered earlier, and that we will also want to use either the scalar objects or the SPL Types as an Object-Oriented basis for our type system.

I mentioned that the Scalar Objects and SPL Types extensions shouldn’t be used together. That’s because they do the same thing. It’s not possible to proxy function calls (in the way I’ve just described) without repeating the whole Object-Oriented side of things for each extension.

This is due to the fact that the Scalar Objects extension automatically boxes variables while the SPL Types extension expects developers to box their own variables. It’s the different between return new static(substr($this, $offset, $length)); and return substr($this, $offset, $length);

While I love the Scalar Objects extension, I am inclined to suggest with the SPL Types extension. The main reason is that it doesn’t interfere with how PHP handles scalar types. This means we will need to box variables, but we’ll also enjoy the benefits of state (object-like scalar variables) and automatic unboxing.

Zephir

As cool as Zephir is, it’s not PHP. That means any developers you want to work on your code, will need to know or learn another language. It also increases the time between coding, testing and shipping.

So, for the remainder of the book, I will show code that is built on top of SPL Types and not translated into Zephir extensions.

The appendices [will] include a PHP and a Zephir implementation. I don’t want the implementation to be a focus of the book, but I will enjoy making it!

How To Structure The Types

We’ve already decided to use namespaced functions as the basis, so we need to decide how to use these from within classes.

Resolving Types

We’ll often need to resolve types, within the type methods. Any arguments could potentially be the wrong type. It makes sense for the type resolution/conversion functions to be in their own namespace:

· Type\isString

· Type\isStringObject

· Type\isExpression

· …

· Type\toStringObject

· Type\toExpression

· …

We should proxy to the creation methods:

1 class StringObject extends SPLString

2 {

3 public function trim($mask = "\t\n\r\0\x0B")

4 {

5 $isString = Type\isString($mask);

6 $isStringObject = Type\isStringObject($mask);

7

8 if ($isString or $isStringObject) {

9 if (Type\isExpression($mask)) {

10 $raw = Type\String\trimWithExpression(

11 $this,

12 $mask

13 );

14 } else {

15 $raw = Type\String\trimWithString(

16 $this,

17 $mask

18 );

19 }

20

21 return Type\toStringObject($raw);

22 }

23

24 throw new LogicException("mask is not a string");

25 }

26 }

Chaining

One of the benefits of the object-oriented approach we’re taking is that we can chain calls on the types. You may have noticed how this is implemented (from the previous example), but in-case you didn’t:

1 return Type\toStringObject($raw);

The manipulation methods should return plain PHP types - because we want people to be able to use the types interchangeably. So it falls to the classes (proxies) to wrap plain PHP types within the SPL Types.

Combining Number Types

I’ve already alluded to my desire of a single number type, and that’s exactly what I want to achieve here. For that reason, we’ll completely ignore the SPLNumber class in favour of SPLFloat.

The reason is simple - numbers should be able to handle decimal points, without a separate set of methods or additional mental overhead. This will lead to additional casting in numeric operations, but that’s not the end of the world.

How To Test

Testing is an essential part of supplanting the native type handling system. The good news is that it will be easy to do!

PHPUnit

PHPUnit is a unit-testing library which makes the process of testing small units of code super easy:

1 class StringObjectTest extends PHPUnit_Framework_TestCase

2 {

3 /** @test */

4 public function trimWorksWithStrings()

5 {

6 $subject = Type\toStringObject("Hello World...");

7

8 $this->assertEquals(

9 "Hello World",

10 $object->trim(".")

11 );

12 }

13 }

This kind of testing can be done with a number of testing frameworks. At the end of the day, as long as you are writing tests, whichever testing framework you use is up to you.

What Should Be Tested?

The short answer is: everything.

We’re aiming to build something solid, and it’s pretty low-level. That means we need to be sure things continue to work as expected. It’s not even that hard when you consider how simple the methods are that we are going to make.

We should aim to have good coverage of the namespaced functions, and a few tests to ensure that these are correctly called from the (proxy) classes.

When Should Tests Be Written?

This is really up to you. Maybe you want to write your tests first, and follow the red-green-refactor cycle. Maybe you want to write all of your library code first, and then make sure everything works as you expected it to (by writing tests for the library code).

The important thing is to write tests.

Recommendations

I highly recommend the following books:

· Clean Code, by Robert C. Martin

· The Grumpy Programmer’s PHPUnit Cookbook, by Chris Hartjes

How To Package

If you want your code to be used (by anyone else), you’d be wise to package it in such a way that others will want to use it.

Make A Readme File

It sounds simple, but you’d be surprised how few developers actually do this! When other developers stumble across your repository (on Github/BitBucket/what-have-you), they won’t be impressed to see a bare-bones directory structure and nothing else.

Make a Readme file, and include the following…

A Few Examples

Show what your library can do. There are few things more frustrating than being enticed (to learn more about a library) by some marketing, only to have to figure it out from tests and/or source-code.

Those things are excellent places to learn, but they must be sought out, whereas a few examples (in your Readme file) are easy to find. Include examples covering the major aspects of your library.

In our case, this means an example (or two) about the procedural code underpinning the library. It means an example (or two) about the object-oriented wrappers, which provide fluent interfaces to traditionally procedural actions. It means providing an example (or two) about who to subclass the types for further customisation.

It’s not hard to make these - they can even be extracted from your unit tests…

Testing Instructions

Developers want to know how solid your code is. Sure you’re using SemVer, but how much test coverage do you have? Do your tests pass? Can I trust you?

The easiest want to answer these questions is to write the unit tests, and then provide instructions for how they can be run. You don’t require anything elaborate:

1 $ composer install && phpunit

That’s how you might be running your unit tests, and it’s easy to tell others how to run them.

Installation Instructions

Ok - you’ve convinced someone to use your [hopefully] well-tested library. The examples provide its value. So how do they install your library?

This is where you make a composer.json file and include Composer installation instructions. This will require a round-trip to Packagist, but you’ll be all the better for it.

Then just a simple set of instructions is all you need add:

1 $ composer require "vendor/library:1.0.0"

License

Include an open-source license. Something friendly like MIT will do nicely. Specify this in your Readme file, and include a clearly-named file (like LICENSE or license.md) which contains the full license.

Contribution Guidelines

This is optional, but greatly increases the chance that other developers will send compatible pull-requests. If you are fussy about code style (and you should be), then be sure to tell people how you want their submissions to look.

Conclusion

Building a new type system is only partly about code. There’s a lot of design consideration that goes into well-crafted libraries. Take the time to decide on a reasonable structure - one that allows consumers the most flexibility.

Test well. Package well. Be clear.