Better strings in PHP

Published by at 25th July 2018 9:25 pm

One of the weaknesses of PHP as a programming language is the limitations of some of the fundamental types. For instance, a string in PHP is a simple value, rather than an object, and doesn't have any methods associated with it. Instead, to manipulate a string, you have to call all manner of functions. By comparison, in Python, not only can you call methods on a string, and receive a new string as the response, making them easily chainable, but you can also iterate through a string, as in this example:

1>>> a = 'foo'
2>>> a.upper()
3'FOO'
4>>> a.lower()
5'foo'
6>>> for letter in a:
7... print(letter)
8...
9f
10o
11o

A little while back, I read Adam Wathan's excellent book Refactoring to Collections, which describes how you can use a collection implementation (such as the one included with Laravel) to replace convoluted array manipulation with simpler, chainable calls to a collection object. Using this approach, you can turn something like this:

1$result = array_filter(
2 array_map(function ($item) {
3 return $item->get('foo');
4 }, $items),
5 function ($item) {
6 return $item->bar == true;
7});

Or, even worse, this:

1$result1 = array_map(function ($item) {
2 return $item->get('foo');
3}, $items);
4$result2 = array_filter($result1, function ($item) {
5 return $item->bar == true;
6});

Into this:

1$result = Collection::make($items)
2 ->map(function ($item) {
3 return $item->get('foo');
4 })->filter(function ($item) {
5 return $item->bar == true;
6 })->toArray();

Much cleaner, more elegant, and far easier to understand.

A while back, after some frustration with PHP's native strings, I started wondering how practical it would be to produce a string implementation that was more like the string objects in languages like Python and Javascript, with inspiration from collection implementations such as that used by Laravel. I soon discovered that it was very practical, and with a bit of work it's not hard to produce your own, more elegant string class.

The most fundamental functionality required is to be able to create a string object, either by passing a string to the constructor or calling a static method. Our string class should be able to do both:

1<?php
2
3class Str
4{
5 protected $string;
6
7 public function __construct(string $string = '')
8 {
9 $this->string = $string;
10 }
11
12 public static function make(string $string)
13 {
14 return new static($string);
15 }
16}

Making it iterable

To be able to get the length of a string, it needs to implement the Countable interface:

1use Countable;
2
3class Str implements Countable
4{
5 ...
6 public function count()
7 {
8 return strlen($this->string);
9 }
10}

To access it as an array, it needs to implement the ArrayAccess interface:

1...
2use ArrayAccess;
3
4class Str implements Countable, ArrayAccess
5{
6 ...
7 public function offsetExists($offset)
8 {
9 return isset($this->string[$offset]);
10 }
11
12 public function offsetGet($offset)
13 {
14 return isset($this->string[$offset]) ? $this->string[$offset] : null;
15 }
16
17 public function offsetSet($offset, $value)
18 {
19 if (is_null($offset)) {
20 $this->string[] = $value;
21 } else {
22 $this->string[$offset] = $value;
23 }
24 }
25
26 public function offsetUnset($offset)
27 {
28 $this->string = substr_replace($this->string, '', $offset, 1);
29 }
30}

And to make it iterable, it needs to implement the Iterator interface:

1use Iterator;
2
3class Str implements Countable, ArrayAccess, Iterator
4{
5 ...
6 public function current()
7 {
8 return $this->string[$this->position];
9 }
10
11 public function key()
12 {
13 return $this->position;
14 }
15
16 public function next()
17 {
18 ++$this->position;
19 }
20
21 public function rewind()
22 {
23 $this->position = 0;
24 }
25
26 public function valid()
27 {
28 return isset($this->string[$this->position]);
29 }
30}

Making it work as a string

To be useful, it also needs to be possible to actually use it as a string - for instance, you should be able to do this:

1$foo = Str::make('I am the very model of a modern major general');
2echo $foo;

Fortunately, the __toString() magic method allows this:

1 public function __toString()
2 {
3 return $this->string;
4 }

Adding methods

With that functionality in place, you can then start adding support for the methods you need in your string objects. If you're looking to be able to use the same functionality as existing PHP methods, you can call those functions inside your methods. However, be sure to return a new instance of your string object from each method - that way, you can continually chain them:

1 public function replace($find, $replace)
2 {
3 return new static(str_replace($find, $replace, $this->string));
4 }
5
6 public function toUpper()
7 {
8 return new static(strtoupper($this->string));
9 }
10
11 public function toLower()
12 {
13 return new static(strtolower($this->string));
14 }
15
16 public function trim()
17 {
18 return new static(trim($this->string));
19 }
20
21 public function ltrim()
22 {
23 return new static(ltrim($this->string));
24 }
25
26 public function rtrim()
27 {
28 return new static(rtrim($this->string));
29 }

Now, you can write something like this:

1return Str::make('I am the very model of a modern major general ')
2 ->trim()
3 ->replace('modern major general', 'scientist Salarian')
4 ->toLower();

While you could do this with PHP's native string functions alone, it would be a lot less elegant. In addition, if you have other, more complex string manipulations that you often do in a particular application, it may make sense to write a method for that so that your string objects can encapsulate that functionality for easier reuse.

As our string objects are iterable, we can also do this:

1>>> $foo = Str::make('foo');
2>>> foreach ($foo as $letter) { echo "$letter\n"; }
3f
4o
5o

If you have an application that does some complex string manipulation, having a string utility class like this can make for much more expressive, elegant and easy-to-comprehend code than PHP's native string functions. If you want to see a working implementation for this, check out my proof of concept collection and string utility library Proper.