I've been coding in PHP at work for the last 5 years. My org's entire backend is written in PHP—a decision made in 2007 when the company first started. It's not a language I ever imagined myself using prior to working there, but life takes you in all sorts of directions you don't expect.
PHP gets a bad rep in the industry, despite being a mature and commonly used language. But it's mostly based on out-of-date understanding of what PHP can do. Recent versions have caught up with most other languages in terms of features; by this point it's a pretty versatile general-purpose language. Certainly not just for serving HTML, as it was originally designed.
I'm no longer working at the aforementioned company, so I'm reflecting on my experience with PHP after all these years and there's some things I've always found odd about it.
And more than just odd, some of it's language features are really unintuitive and have been prone to cause bugs. This comes from personal experience and many previous headaches at work. I'll explain two of the biggest offenders in this post—in short:
- Arrays are weird and overloaded
- The type system is clunky
Arrays Are Not Really Arrays
PHP's standard library basically only has one data structure: the array. This was intentional; it was designed to be a general-purpose, flexible data structure that can cover a variety of use cases. It's technically an ordered key-value dictionary, not an array in the traditional sense.
Unfortunately, with flexibility comes complexity. If you want to create a collection of fixed-size objects in an allocated memory block, you can't really do that. PHP pretends to support them, but the illusion breaks down in unexpected ways.
Let's say I have a bunch of fruits. PHP let's me define a fruits "array" and I can do normal array things with it.
$fruits = ["apples", "oranges", "limes"];
// you can count em'
count($fruits) // 3
// you can access em'
$fruits[0] // "apples"
// you can print em'
print_r($fruits);
/*
Array
(
[0] => "apples"
[1] => "oranges"
[2] => "limes"
)
*/
Everything looks fine but you get into trouble whenever you perform a mutation on this "simple" array; it will be exposed as being a key-value store.
When you use one of PHP's built-in functions for standard array operations like sorting or filtering, it will operate on the keys AND values of your array. If it mutates the array in-place or by a return value, the key order will likely become inconsistent.
// this will mess up your array
$filteredFruits = array_filter($fruits, fn ($name) => str_contains($name, "limes"));
print_r($filteredFruits);
/*
Array
(
[2] => limes
)
*/
// getting the first element won't work anymore
print($filteredFruits[0]);
/*
PHP Warning: Undefined array key 0
*/
// element removal also messes up the array
unset($fruits[0]);
print_r($fruits);
/*
Array
(
[1] => oranges
[2] => limes
)
*/
why can't I hold all these indices???
The only way to put these arrays back into a naturally indexed state is to use the array_values() function. You just have to know that, or else you end up with subtle bugs.
$filteredFruitsFixed = array_values($filteredFruits);
print_r($filteredFruitsFixed);
/*
Array
(
[0] => limes
)
*/
$fruitsFixed = array_values($fruits);
print_r($fruitsFixed);
/*
Array
(
[0] => oranges
[1] => limes
)
*/
It's just strange to me that PHP doesn't support simple collections of objects. It's annoying to have to manage these arbitrary numeric keys when all you really want is ordinal indexing like 99% of the time. It feels like a leaky abstraction.
Class Property Types Are Confusing
In PHP5, a native type system was added to the language. It was expanded over time and by PHP7 you could define the types for your class's properties. Although PHP is a scripting language, type declarations will help catch bugs during testing, or even during development with the use of static analysis tools like PHPStan.
But PHP's type system has some quirks since it was built on an existing dynamically typed language. The rules had to be designed after the behaviour was already there. For class properties, there's a hidden uninitialized state that can pop up if you're not careful.
Let's define a Book class with three string properties:
class Book {
public $title;
public string $author;
public ?string $publisher;
}
Here, I'm illustrating all the ways of declaring the type for a string property:
- Don't
- It's a string
- It's nullable string
Before PHP7, all class properties were (1): untyped. Since the type system is optional, it has to live alongside the "legacy" behaviour which has weird consequences. For example, what do you think the values of these three properties will be after we instantiate a Book object?
$b = new Book();
print_r($b);
/*
Book Object(
[title] =>
)
*/
Trick question! Only the untyped $title property will have a value, and that value is NULL. That seems fine and is roughly in line with how I'd expect a language to use a NULL value. But the other two properties will NOT have a value because they don't exist, or rather they could exist but haven't been initialized yet.
This example exposes the "uninitialized" state that a property can be in, which is NOT the same as NULL. This distinction frustratingly comes up when you try to do a null check on these properties:
print("title is null: " . is_null($b->title));
/*
title is null: 1
*/
print("author is null: " . is_null($b->author));
/*
PHP Fatal error: Uncaught Error: Typed property Book::author must not be accessed before initialization...
*/
print("publisher is null: " . is_null($b->publisher));
/*
PHP Fatal error: Uncaught Error: Typed property Book::publisher must not be accessed before initialization...
*/
Not a warning—a FATAL error occurs if you try to access an uninitialized property. This comes up a lot in cases where you try to deserialize data into a PHP object. If a field's data isn't present you might not initialize the property at all.
ahh yes, NULL...who was that by again?
This lax behaviour for property definitions makes writing code around them harder. Especially when you take into account that any object can have properties dynamically added to them:
$b->foo = "bar";
print("foo: " . $b->foo);
/*
foo: bar
*/
So I feel like the class property type system does little to help you understand what a given object is composed of, and in some respects has made it less clear because it's introduced this new uninitialized state. As a developer, it's hard to write defensive code because you're never sure which checks to do for all these situations: is_null(), isset(), property_exists(), empty()... it's not obvious which functions cover which states.
I'd argue that uninitialized did not need to be a state at all. For nullable typed properties, just default them to null the way untyped properties are. And for non-nullable types, require them to be be defined as constructor promoted parameters OR require a default value at declaration. Similar requirements already exist for the readonly attribute, so it's certainly feasible for the PHP execution engine to enforce it.
But there's probably some nuance or historical reason I'm missing here. Let me know in the comments if you know.
Conclusions
Despite all the critiquing I've done in this article, I still think the amount of hate PHP gets is undeserved. Like any language, it has it's quirks and tradeoffs, but you can still accomplish any task using PHP that you could in another language. The more you know about a language, the better you can structure things to work "with the grain" and write more idiomatic code.
Some things I do enjoy about PHP:
- It's a scripting language, so development friction is low. Make a file change and it instantly takes effect.
- Laravel is a solid web framework with tons of extensible functionality. It's opinionated and definitely leans into the "auto-magical" framework style, but it was designed well so you don't mind.
- All the $ signs help remind you what you're doing it all for at the end of the day 🤑
Thanks for reading!