Facepalms and WTF Moments
If you are into Star Trek and have watched The Next Generation, you will probably remember Q, an omnipotent superior being who appears on the Enterprise every one in a while, usually causing lots of trouble. One episode begins with Q showing up complaining that he has been punished by being turned into a human. This is when Captain Picard does a facepalm, allegedly for the first time in The Next Generation.
The facepalm, especially the one by Captain Picard, has become a
famous internet meme, in fact so famous that a facepalm icon has
even been added to Unicode. Try var_dump("\u{1F926}");
or better re-watch The Next Generation, it’s still fun.
To get back on topic: some things that exist in PHP, or used to exist in PHP, are definitely worth a facepalm. However, think twice before putting all the blame on the core developers: Allowing weird code constructs in a programming language is one thing, but actually using them is definitely also worth a good, long facepalm.
No More Cheating Using Parentheses
Many PHP core functions require arguments to be passed by reference, disallowing the inline use of return values from other functions. This makes a lot of sense for functions that modify the value of the argument alongside their operation:
$last = array_pop( range(0, 3) );
var_dump($last);
Because array_pop()
removes the last element from
the given array and effectively shortens it, passing a reference is
required. In previous versions of PHP not adhering to this
requirement triggered an E_STRICT
notice:
PHP Strict standards:
Only variables should be passed by reference in ...
int(3)
Unknown to many developers, wrapping the nested function call into parentheses was a hacky but working way to avoid this notice:
$last = array_pop( (range(0,3)) );
var_dump($last);
int(3)
As this hack does neither address the underlying issue nor fix
the logical problem, adding parentheses no longer affects the
behavior in PHP 7. Additionally, due to the removal of
E_STRICT
, the error level got raised to
E_NOTICE
. Executing the example above with PHP 7
produces the following output:
PHP Notice:
Only variables should be passed by reference in ...
int(3)
Identical Parameter Names are no Longer Allowed
While technically of arguable usefulness, previous versions of PHP allowed function definitions to contain the same parameter name multiple times. With PHP 7 this is no longer supported, and the following declaration is considered invalid:
function f($a, $a)
{
// ...
}
When executed, the code shown above will trigger the following error:
PHP Fatal error:
Redefinition of parameter $a in ...
Multiple Default
Blocks in switch
Statements
PHP in previous versions at least syntactically supported
multiple default
blocks in a given switch
statement:
switch ($var) {
// ...
default: { var_dump('default #1'); }
default: { var_dump('default #2'); }
}
Run with PHP 5.6, the following output would be produced,
demonstrating that only the last default
block would
get executed:
string(10) "default #2"
As of PHP 7, this technically pointless construct is considered invalid and trying to run the above example results in a fatal error:
PHP Fatal error:
Switch statements may only contain one default clause in ...
Abusing
$this
The special $this
variable is used in methods to
refer to the object on which they are operating. It was possible to
reassign this variable before PHP 7.1 thus breaking a fundamental
concept of object-oriented programming. Starting with PHP 7.1, this
variable cannot be reassigned or overwritten.
Using the $this
variable outside the scope of an
object is no longer possible:
function f()
{
var_dump($this);
}
f();
Executing the code shown above with PHP 7.0 triggered a notice
and printed NULL
:
Notice: Undefined variable: this in ...
NULL
PHP 7.1 or later triggers a runtime error instead:
Fatal error: Uncaught Error:
Using $this when not in object context in ...
Unsetting the $this
variable using
unset()
is no longer possible:
unset($this);
Starting with PHP 7.1, the code shown above triggers a runtime error :
Fatal error: Cannot unset $this in ...
Using $this
as a parameter is no longer
possible:
function f($this)
{
}
Since PHP 7.1, the code shown above triggers a compile-time error:
Fatal error: Cannot use $this as parameter in ...
Also, using $this
as a static variable is no longer
possible:
function f()
{
static $this;
}
In PHP 7.1 and newer, this is a compile-time error:
Fatal error: Cannot use $this as static variable in ...
You cannot use $this
as a global variable:
function f()
{
global $this;
}
Since PHP 7.1, any attempt to do so will be punished with a compile-time error:
Fatal error: Cannot use $this as global variable in ...
We know that things are getting weirder, but you will have to
suffer through this with us. Assigning to $this
in a
catch
statement is no longer possible since PHP
7.1:
try {
} catch (Exception $this) {
}
This does not even compile:
Fatal error: Cannot re-assign $this in ...
You can also not re-assign $this
in a
foreach
statement:
foreach ($a as $this) {
}
Fatal error: Cannot re-assign $this in ...
Furthermore, reassigning $this
by using variable
variables or indirectly through references also no longer works.
Functions such as extract()
or parse_str()
can also no longer overwrite $this
.
If you have code that uses a variable named $this
outside the scope of an object, then simply rename it.
Using
get_class()
on null
The get_class()
function returns the name of the
class of an object that is passed to it as an argument.
While it does not really make sense, PHP supports calling this
function without an argument. When get_class()
is used
without an argument in the body of a method then it returns the name
of the class the method which called get_class()
belongs to.
class SomeClass
{
public function method()
{
var_dump(get_class());
}
}
$o = new SomeClass;
$o->method();
var_dump(get_class(new stdClass));
Executing the code shown above produces the output shown below:
string(9) "SomeClass"
string(8) "stdClass"
Prior to PHP 7.2, it was possible to pass null
as an
argument to get_class()
when the function was used in
the body of a method:
class SomeClass
{
public function method()
{
var_dump(get_class(null));
}
}
$o = new SomeClass;
$o->method();
PHP 7.1 does not complain:
string(9) "SomeClass"
Since version 7.2, PHP will issue a warning:
Warning: get_class() expects parameter 1 to be object,
null given in ...
bool(false)
Prior to PHP 7.2, it was already impossible to pass
null
as an argument to get_class()
outside
the context of a class:
var_dump(get_class(null));
This is the warning that PHP 7.1 produced:
Warning: get_class() called without object from
outside a class in ...
bool(false)
In PHP 7.2, the warning message has been updated:
Warning: get_class() expects parameter 1 to be object,
null given in ...
bool(false)
Inheritance Loophole for Static Properties
Static properties are shared between inheriting classes, except for static properties that are explicitly overridden in a child class.
Consider the following example:
class ParentClass
{
public static $property = 0;
}
class ChildClass extends ParentClass
{
}
var_dump(ParentClass::$property);
var_dump(ChildClass::$property);
ParentClass::$property = 1;
var_dump(ParentClass::$property);
var_dump(ChildClass::$property);
ChildClass::$property = 2;
var_dump(ParentClass::$property);
var_dump(ChildClass::$property);
The parent class declares a public static property that is not
explicitly overridden in the child class. Any change made to this
property is visible no matter how, whether through
ParentClass::$property
or
ChildClass::$property
, it is accessed. Executing the
code shown above prints the output shown below:
int(0)
int(0)
int(1)
int(1)
int(2)
int(2)
Previous versions of PHP had a bug that made it possible to
separate ParentClass::$property
from
ChildClass::$property
by using references:
class ParentClass
{
public static $property = 0;
}
class ChildClass extends ParentClass
{
}
ChildClass::$property = &$variable;
$variable = 1;
var_dump(ParentClass::$property);
var_dump(ChildClass::$property);
Executing the code shown above printed the output shown below in previous versions of PHP:
int(0)
int(1)
The bug that allowed this loophole has now been fixed and executing the code shown above now prints the output shown below:
int(1)
int(1)
parent
and
classes without a parent
Using the parent
keyword inside a class that does
not have a parent class has been deprecated:
class C
{
public function m(): void
{
parent::m();
}
}
$o = new C;
$o->m(true);
Executing the code shown above with PHP 7.4 prints the output shown below:
PHP Deprecated: Cannot use "parent" when current class scope
has no parent in ...
PHP Fatal error: Cannot access parent:: when current class scope
has no parent in ...
The deprecation warning is triggered while the code is compiled, and the fatal error when the code is executed.
In PHP 8, the compiler will trigger a fatal error instead of just a deprecation warning:
Fatal error: Cannot use "parent" when current class scope
has no parent in ...
Static versus Non-Static Access
Methods as well as properties of a class can be declared static.
Static properties share a common value among all instances of a
certain class. Trying to access such a property dynamically will
trigger an E_STRICT
notice. To demonstrate this, in the
following example a dynamic call is used to access a statically
defined property. Contrary to common belief, it does not actually
change the value of the static property but adds a dynamic property
with the same name to the instance:
class Sample
{
public static $prop = 'default value';
}
$obj1 = new Sample;
var_dump($obj->prop);
$obj1->prop = 'value set #1';
var_dump($obj->prop);
var_dump(Sample::$prop);
When executed with PHP 5.6 and full error reporting, the following output is generated:
Strict Standards:
Accessing static property Sample::$prop as non static in ...
Notice: Undefined property: Sample::$prop in ...
NULL
Strict Standards:
Accessing static property Sample::$prop as non static in ...
Strict Standards:
Accessing static property Sample::$prop as non static in ...
string(12) "value set #1"
string(13) "default value"
Despite complaining about the wrong way of accesssing the
property, PHP returns NULL
for the first read request
to the property – indicating it does not exist – and dynamically
adds it for the write request. As can be seen by the last line in
the output, the value of the static property was not affected at
all.
In PHP 7, the same general behavior is present, yet the
E_STRICT
message has been raised to an
E_NOTICE
:
Notice:
Accessing static property Sample::$prop as non static in ...
Notice:
Undefined property: Sample::$prop in ...
NULL
Notice:
Accessing static property Sample::$prop as non static in ...
Notice:
Accessing static property Sample::$prop as non static in ...
string(12) "value set #1"
string(13) "default value"
Funny enough, trying to reverse the approach and accessing a dynamic property statically is considered a fatal mistake in PHP 5 and PHP 7:
class Sample
{
public $prop = 'default value';
}
Sample::$prop = 'value set #1';
PHP Fatal error:
Uncaught Error: Access to undeclared static property:
Sample::$prop in ...
Static calls to dynamic methods on the other hand are executed without complaint.
For historic reasons, PHP also supports the reverse, namely
calling a non-static method statically. While this is currently
still supported, static calls to non-static methods will get removed
from PHP. To reflect this change, this type of call now emits an
E_DEPRECATED
.
class Sample {
public function doSomething() {}
}
Sample::doSomething();
Executing the code shown above prints the output shown below:
Deprecated: Non-static method Sample::doSomething()
should not be called statically in ...
Only Variables should be Assigned or Passed by Reference
Using the ampersand (&
), PHP can be forced to
use a reference rather than actual values being passed around. While
trying to assign a function or method call by reference does not
make any practical sense, it still works:
$x =& rand(0, 1);
If encountered, PHP 7 will now emit an E_NOTICE
. The
same holds true for trying to pass a function by reference:
function reference(&$param)
{
}
reference(rand(0, 1));
setcookie()
and Empty
Names
The ability to set cookies from within an application is not
exactly a new treat for PHP. In previous versions, the function
setcookie()
allowed the name parameter to be
null
or an empty string:
setcookie(null, 'abc');
When executed, PHP 5.6 will – among other headers – add the following line to the output:
Set-Cookie: =abc
Since this does not actually work from the HTTP standards
perspective, PHP 7 now emits an E_WARNING
whenever an
empty name was supplied and does not add the nameless cookie to the
headers:
PHP Warning: Cookie names must not be empty in ...
Counting of Non-Countable Objects
How many elements does an array have? Just call
count()
on the array to find out:
var_dump(count([1, 3, 5, 7, 8]));
Unsurprisingly, the result is:
int(5)
When the array is wrapped into an object, make this object
implement the Countable
interface, and you can still
call count()
, which will automatically invoke the
object’s count()
method:
class Collection implements Countable
{
private $items = [1, 3, 5, 7, 8];
public function count()
{
return count($this->items);
}
}
var_dump(count(new Collection));
Again, the result is:
int(5)
Like so often, PHP behaves a bit strangely in some edge cases.
What happens, for example, when you call count()
on a
scalar value, like a string?
var_dump(count('string'));
One might expect the result to be 6
, but it is
not:
int(1)
You get the same result when trying to count an object that does
not implement the Countable
interface:
class Collection
{
private $items = [1, 3, 5, 7, 8];
}
var_dump(count(new Collection));
Since PHP 7.2, PHP will generate a warning when trying to count non-countable objects or scalars:
Warning: count(): Parameter must be an array or an object
that implements Countable in ...
The function sizeof()
is an alias to
count()
…
var_dump(sizeof('string'));
… and it behaves just like count()
, including the
warning:
Warning: sizeof(): Parameter must be an array or an object
that implements Countable in ...
int(1)
Please note that there is no change to the behaviour of
count()
in PHP 7.2. However, your code will have to
deal with the warning, which might be a problem, for example if your
application has its own error handler that bails out on
warnings.
Bareword (Unquoted) Strings
In PHP, constants are referenced using so-called bareword (or unquoted) strings:
print PHP_VERSION;
PHP tries to resolve such a string to a built-in or user-defined
constant. As PHP_VERSION
is a built-in constant,
executing the code shown above produces the output shown below:
7.4.6
From PHP 3.0 up to and including version 7.1, PHP triggers an
E_NOTICE
and uses an unquoted string as a string when
it cannot be resolved to a constant:
print NOT_A_CONSTANT;
Executing the code shown above with PHP 7.1 produces the output shown below:
Notice: Use of undefined constant NOT_A_CONSTANT -
assumed 'NOT_A_CONSTANT' in ...
NOT_A_CONSTANT
This behavior can lead to serious bugs. This is why, as of PHP
7.2, unquoted strings that do not reference a constant now trigger
an E_WARNING
instead of an E_NOTICE
. Thus
executing the code shown above with PHP 7.2 or newer and above
produces the output shown below:
Warning: Use of undefined constant NOT_A_CONSTANT -
assumed 'NOT_A_CONSTANT' (this will throw an Error in a future
version of PHP) in ...
NOT_A_CONSTANT
Using unquoted strings that do not reference a constant will trigger an error in PHP 8.
Using Resources as Keys
PHP only supports strings or integer values as a key to specify
the index position within an array. Trying to use anything else will
lead to an E_WARNING
. Or at least almost anything else.
Before PHP 7, using a resource as a key when accessing an array
element lead not to the expected warning but merely an
E_STRICT
notice. Even more confusing, the given
resource identifier is type-cast to an integer value:
$fp = fopen(__FILE__, 'r');
$array = [];
$array[$fp] = 1;
The above code, when executed with PHP 5, leads to the following
E_STRICT
notice:
PHP Strict Standards:
Resource ID#5 used as offset, casting to integer (5)
While for backwards compatibility reasons this rather surprising behavior is still present, with PHP 7 it at least triggers a regular notice:
PHP Notice:
Resource ID#5 used as offset, casting to integer (5)
Working with References
Working with explicit, PHP-level references (the
&
operator), has always been somewhat problematic
in PHP. PHP 4 forced us to use lots of &
operators,
because it would copy objects by default. The “proper” object
semantics introduced with PHP 5 made all those &
operators superfluous when working with objects, as we have
previously pointed out in the chapter Assigning the Result of
new
by Reference.
Let aside some built-in PHP functions that work with references,
for example sort()
or preg_match()
, there
is really no need to work with explicit references in PHP.
In PHP 7.3, internal changes to reference handling change PHP’s behaviour in some edge cases that might be triggered by code that you probably should not have written. The changelog gives the following example:
$arr = [1];
$ref =& $arr[0];
var_dump($arr[0] + ($arr[0] = 2));
Honestly, we would not even know what result to expect, because we would never write such code, for the simple reason that we are not clever enough to figure out what the hell we would expect the result to be.
Accoding to the change log, starting with PHP 7.3, the result is
int(3)
whereas with PHP 5 and every PHP 7 version up to PHP 7.2.31, the result was
int(4)
The change in PHP’s behaviour is that the reference is being unwrapped on access, and that you now cannot modify a reference between accessing it and using its value. The changelog continues on to say “Please note that reading and writing a value inside a single expression remains undefined behavior and may change again in the future.”
Admittedly, we tried hard to come up with a different, more realistic example, but were unable to do so. So anything “regular” you are doing with references is still safe, you should just check your code for very creative constructs like the one shown above.
As with some other facepalms and WTF moments, this one also does also not imply that the PHP developers are incompetent or making irresponsible decisions. On the contrary: it makes perfect sense to clean up PHP, and warn devlopers that obscure code constructs might behave differently. Honestly, the facepalm aspect here is more: you really, really should not write code like this.
Your ultimate litmus test is this: show a piece of code to a number of developers. If they cannot agree on what the code does, the code is not good enough and should be rewritten to express its intent more clearly.