Why PHP is not for me

So most recently I was employed at Hologram and was therefore too busy to write. Not anymore – I was one of the employees laid off from Hologram recently.

While at Hologram, I worked mostly in PHP, which was something I actually dreaded coming into the job, but ended up not minding as much as I thought I would. PHP, for all of its terrible reputation, functions mostly like how you’d expect a programming language to function. It does what you ask of it. But there are a lot of dark corners to the language.

Quick syntax oddities

But there are some…oddities. I’ll start with the syntax. As a Rubyist, the opening sigil for declaring or referencing a variable feels super weird:

$a = 1;

I made this joke to my team: “PHP must be great for startups, because all I see are dollar signs.” Then I proceeded to tell dad jokes for 3 months until management got rid of me. I guess that one was on me.

Then there’s stabby arrow class property calls, which include functions:

class Dog {
    // This is a property
    public $fur_color = "brown";

    function woof() {
        echo "woof\n";
    }
}

$d = new Dog;

$d->fur_color;
// "brown"

$d->woof();
// woof

Another oddity is the array push syntax, which looks like an assignment

$a = [];
// []

$a[] = 1;
// [1]

But those are just some quirks! They’re relatively easy to get over. Other issues are not.

Everything is a string

In PHP, if you want to refer to a function, you can just use a string as if it were the function itself:

function say_hi() {echo "hi\n";}
'say_hi'();
hi

And if you want to reference a class, you can also use a string:

$klass = 'Dog';
$d = new $klass;
var_dump($d);
object(Dog)#2 (0) {
}

Oddly enough, using a raw string to reference a class doesn’t work:

$d = new 'Dog';
PHP Parse error:  syntax error, unexpected ''Dog'' (T_CONSTANT_ENCAPSED_STRING) in php shell code on line 1

Why does this matter? Well, PHP also really likes to do metaprogramming with string composition. So I encountered a lot of code like this:

$namespaced_klass = "\Namespace\\" . $klass;

$object = new $namespaced_klass();

The . concatenates the two strings. Let’s try this in Ruby:

klass = "Animal::" + "Dog"
=> "Animal::Dog"

d = klass.new()
Traceback (most recent call last):
        4: ...
NoMethodError (undefined method `new' for "Animal::Dog":String)

Both languages make string manipulation really easy, but in Ruby, it’s a lot harder to summon a class or a function from a string, which is actually a feature.

I would oftentimes come across a function like this in our PHP code base:

function setupClass($klass) {
    $klass = "\GlobalNamespace\\" . $klass;
    $thing = new $klass();
    $thing->doSomethingIncomprehensible();
}

…and I would have no idea where the function doSomethingIncomprehensible would come from, which would add layers of indirection. When you follow the references of setupClass, it was possible to find even more indirection:

if ($some_condition) {
    $klass = $a . $b
} else {
    $klass = $a . $c
}

setupClass($klass);

It’s very convenient for metaprogramming, but it encourages some truly convoluted code.

Also as we can see with trying to instantiate raw string 'Dog' that it’s inconsistent.

Error messages are not helpful

I am very much cheating here comparing PHP to Rust, but let’s compare what happens when you call a slightly incorrect function name in various languages. Assuming we have a woof method defined on a struct / class and we call woog() instead of woof():

PHP:

PHP Warning:  Uncaught Error: Call to undefined method Dog::woog() in php shell code:1
Stack trace:
#0 {main}
  thrown in php shell code on line 1

Ruby:

Traceback (most recent call last):
        4: from /Users/brian/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        3: from /Users/brian/.rbenv/versions/3.0.0/bin/irb:23:in `load'
        2: from /Users/brian/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        1: from (irb):37:in `<main>'
NoMethodError (undefined method `woog' for #<Dog:0x00007f8ec213e450>)
Did you mean?  woof

Rust:

error[E0599]: no method named `woog` found for struct `Dog` in the current scope
  --> src/main.rs:12:7
   |
2  | struct Dog;
   | ----------- method `woog` not found for this
...
12 |     d.woog();
   |       ^^^^ help: there is an associated function with a similar name: `woof`

For more information about this error, try `rustc --explain E0599`.

Both Ruby and Rust offer helpful suggestions. PHP does not.

And this is a fairly bog standard error message. PHP error messages get pretty cryptic. The one that stumped me the most was this one:

Serialization of mocked objects in PHP 7.4 · Issue #1038 · mockery/mockery:

Exception : Serialization of 'ReflectionClass' is not allowed

Which basically boiled down to somewhere, someone used a class that cannot be serialized by mockery, the unit test framework, and PHP absolutely could not tell me which class it was or give me a stack trace.

Global functions, many deprecated

At some point, I wanted to write a function that checks whether a string starts with another string, so I used strpos, which purports to “Find the position of the first occurrence of a substring in a string.” Which it does, but PHP’s type system allows strpos to return either a numeric index or a boolean false.

I assumed strpos returned a numeric value for the index, i.e. a string could safely be assumed to start with another string if index == 0. Sadly, PHP casts false to 0, a common aspect of a lot of languages, actually, but one that stumped me for a few days as every string seemed to report starting with every other string. No, strpos was just returning false, which was getting cast to 0.

And yes, I could have double checked with strpos($haystack, $needle) === 0 instead of strpos($haystack, $needle) == 0, but I had no idea that strpos could return a boolean.

PHP 8, thankfully, has str_starts_with and str_ends_with, but we were stuck on an earlier version of PHP.

Few functional affordances 😢

Ruby has postfix functional methods:

[1,2,3,4,5].map {|num| num.pow(2)}
=> [1, 4, 9, 16, 25]

Which means that you can chain functions that return Enumerables together to stack different effects onto the original array / Enumerable:

[1,2,3,4,5].map {|num| num.pow(2)}.map {|num| num / 2.0}
=> [0.5, 2.0, 4.5, 8.0, 12.5]

PHP does have a map function, but it’s actually a global function:

array_map(function($num) {return $num * $num;}, [1,2,3,4,5]);
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(9)
  [3]=>
  int(16)
  [4]=>
  int(25)
}

Now suddenly when you want to combine functions it gets very messy, and the order of execution is the opposite of the order you read it in:

array_map(function ($num) {return $num / 2.0;}, array_map(function($num) {return $num * $num;}, [1,2,3,4,5]));
array(5) {
  [0]=>
  float(0.5)
  [1]=>
  float(2)
  [2]=>
  float(4.5)
  [3]=>
  float(8)
  [4]=>
  float(12.5)
}

A final petty complaint

All of the above examples were generated in the PHP REPL (php -a) using the var_dump method. Basically, where Ruby’s REPL (irb) would immediately print a string representation of the result of the expression, PHP would silently perform (or not perform) the expected operation.

Ruby:

irb(main):044:0> 1 + 1
=> 2
irb(main):045:0>

PHP:

php > 1 + 1;
php >

Unless you var_dump it:

php > var_dump(1 + 1);
int(2)
php >

And if you don’t, the REPL remains unchanged, which looks frustratingly similar to if you forget to add a semicolon to the end of an expression:

php > 1 + 1
php > echo "hi";
PHP Parse error:  syntax error, unexpected 'echo' (T_ECHO) in php shell code on line 2

Parse error: syntax error, unexpected 'echo' (T_ECHO) in php shell code on line 2
php >

Note: this error actually means “you forgot a semicolon.” See what I mean about the error messages?

Now that I think of it, there’s probably a command line flag you can pass to php to get it to behave the way I want, but it should be the default.


Despite all the quirks, it wasn’t much harder to become productive in PHP than in other dynamic languages. It just happens to be full of papercuts. And, that said, the language itself is still changing and improving – PHP 8 added enums and pattern matching, for example! I was eager to get my grubby little hands on those, but it didn’t happen before my tenure at the company ended.

Overall, PHP doesn’t spark joy in me, but I respect that it’s a beloved tool for many around the world and a workhorse of a language.

Leave a comment