DEV Community

Matthew Daly
Matthew Daly

Posted on • Originally published at matthewdaly.co.uk on

Better strings in PHP

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:

>>> a = 'foo'
>>> a.upper()
'FOO'
>>> a.lower()
'foo'
>>> for letter in a:
... print(letter)
... 
f
o
o
Enter fullscreen mode Exit fullscreen mode

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:

$result = array_filter(
    array_map(function ($item) {
        return $item->get('foo');
    }, $items),
    function ($item) {
        return $item->bar == true;
});
Enter fullscreen mode Exit fullscreen mode

Or, even worse, this:

$result1 = array_map(function ($item) {
    return $item->get('foo');
}, $items);

$result2 = array_filter($result1, function ($item) {
    return $item->bar == true;
});
Enter fullscreen mode Exit fullscreen mode

Into this:

$result = Collection::make($items)
    ->map(function ($item) {
        return $item->get('foo');
    })->filter(function ($item) {
        return $item->bar == true;
    })->toArray();
Enter fullscreen mode Exit fullscreen mode

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:

<?php

class Str
{
    protected $string;

    public function __construct(string $string = '')
    {
        $this->string = $string;
    }

    public static function make(string $string)
    {
        return new static($string);
    }
}
Enter fullscreen mode Exit fullscreen mode

Making it iterable

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

use Countable;

class Str implements Countable
{
    ...
    public function count()
    {
        return strlen($this->string);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

...

use ArrayAccess;

class Str implements Countable, ArrayAccess

{
    ...

    public function offsetExists($offset)
    {
        return isset($this->string[$offset]);
    }

    public function offsetGet($offset)
    {
        return isset($this->string[$offset]) ? $this->string[$offset] : null;
    }

    public function offsetSet($offset, $value)
    {
        if (is_null($offset)) {
            $this->string[] = $value;
        } else {
            $this->string[$offset] = $value;
        }
    }

    public function offsetUnset($offset)
    {
        $this->string = substr_replace($this->string, '', $offset, 1);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

use Iterator;

class Str implements Countable, ArrayAccess, Iterator

{
    ...

    public function current()
    {
        return $this->string[$this->position];
    }

    public function key()
    {
        return $this->position;
    }

    public function next()
    {
        ++$this->position;
    }

    public function rewind()
    {
        $this->position = 0;
    }

    public function valid()
    {
        return isset($this->string[$this->position]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

echo $foo;
Enter fullscreen mode Exit fullscreen mode

Fortunately, the __toString() magic method allows this:

public function __toString()
{
    {
        return $this->string;
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

    public function replace($find, $replace)
    {
        return new static(str_replace($find, $replace, $this->string));
    }

    public function toUpper()
    {
        return new static(strtoupper($this->string));
    }

    public function toLower()
    {
        return new static(strtolower($this->string));
    }

    public function trim()
    {
        return new static(trim($this->string));
    }

    public function ltrim()
    {
        return new static(ltrim($this->string));
    }

    public function rtrim()
    {
        return new static(rtrim($this->string));
    }
Enter fullscreen mode Exit fullscreen mode

Now, you can write something like this:

return Str::make('I am the very model of a modern major general ')
    ->trim()
    ->replace('modern major general', 'scientist Salarian')
    ->toLower();
Enter fullscreen mode Exit fullscreen mode

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:

>>> $foo = Str::make('foo');

>>> foreach ($foo as $letter) { echo "$letter\n"; }

f

o

o
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (4)

Collapse
 
davidbruchmann profile image
David Bruchmann

btw: adding a previous() (or prev()) function to the iterator class can make some things a lot of easier. Don't know if it had a usable feature related to the strings here, but it's easy to implement and can save some headaches in other use-cases.

Collapse
 
aleksikauppila profile image
Aleksi Kauppila

Very nice, good job. I like the idea of implementing ArrayAccess here. Using global functions in PHP is a pain in the ass an not OOP at all. It's good to hide all of them into these sort of types.

Collapse
 
matthewbdaly profile image
Matthew Daly • Edited

Another thing I didn't mention in the post is that if you use value objects, you can use something like this as the base class for those, thereby getting all of that functionality easily. For instance, you could extend the custom string class into an Email class that validates the passed string is a valid email address when it's passed to the constructor, and typehint it to ensure that any method that receives an email gets an instance of Email and not just a generic string and it already has the various methods of the string class.

Collapse
 
adhocore profile image
Jitendra

i once wrote a simplistic and minimalist library with that was based on magic. however it was one solution for strings, numbers and arrays alike. 😊