30 Unexpected Things in PHP
You’d think you know everything, you write functions, you use operators, and you get cool, fast, and clear code constructs. But at some point, something happens that can’t be explained: the code works the way it needs to, not the way you expected. And it all feels like it’s happening, as if, against common sense—like miracles and some kind of magic. But the code can’t be wrong—you are, because either you didn’t take something into account, or you don’t know something!
Below we’ll talk about the features of PHP: unexpected, unusual, non-standard, not obvious, strange, or special situations/cases in PHP.
Also read: PHP Syntax Versions from 5.3 to 7.3
Exact comparison: 0 == 'string'
PHP is a language without strict typing, and because of that sometimes unexpected results can occur when comparing (checking) different values...
if( 0 == 'строка' ) echo 'Неужели?'; // we'll see: 'Неужели?' // the string is converted to a number during comparison and becomes 0: var_dump( 0 == 'строка' ); //> bool(true) // but var_dump( '0' == 'строка' ); //> bool(false)
This happens because 'строка' turns into zero: intval( 'строка' ) = 0, and 0 == 0 is true.
So, for example, you can “skip” the request variable:
// $_GET['foo'] can be any string, and the check will always match...
if( $_GET['foo'] == 0 ){
echo $_GET['foo'];
}
// therefore, if possible, make the check strictly by type
if( $_GET['foo'] === '0' ){
echo $_GET['foo'];
}
All the following values are the same when compared with == (the non-strict comparison operator):
0 == false == "" == "0" == null == array()
And also:
1 == '1нечто' == true true == array(111)
in_array() is deceiving you
You’re a master of arrays in PHP. You already know everything about creating, editing, and deleting arrays. Nevertheless, the following example may surprise you.
Often when working with arrays you have to search for something in them using in_array().
$array = [ false, true, 1 ];
if( in_array( 'строка', $array ) ){
echo 'Found!';
}
What do you think this example will output: the text «Found!»? To someone, it may seem strange, but this condition will work, and the code will output «Found!».
This happens because PHP is a dynamically typed language, and in this case in_array() compares values but does not take the type into account, i.e. type juggling occurs: it uses the operator ==, not ===. And 'строка' == true gives us true. So it turns out that in_array() is lying!
To avoid this kind of “deception”, you need to specify true as the third parameter in in_array(), and now all comparisons will be done taking the type into account.
$array = [ false, true, 1 ];
if( in_array( 'строка', $array, true ) ){
echo 'Found';
}
else{
echo 'Not found'; // this branch of the condition will run
}
The difference between PHP operators OR and ||, AND and &&
PHP operators OR, AND and ||, && respectively differ in execution priority. The latter ones have higher priority, so they will execute earlier.
If you compare them to the assignment operator: =, then OR/AND will be executed AFTER the assignment operator, while || and && will be executed BEFORE the assignment operator due to their higher priority. Let’s consider this difference with an example:
OR and ||
$var = false OR true; // result: $var = false // because assignment runs earlier than OR comparison // it behaves like: ( ($var = $false) or $true ) $var2 = false || true; // result: $var2 = true // since the comparison happens first, and then the assignment var_dump( $var, $var2 ); // bool(false), bool(true)
AND and &&
// "&&" has higher priority than "and" $var = true && false; // the result of the expression (true && false) is assigned to variable $g // works like: ($var = (true && false)) $var2 = true and false; // constant true is assigned to $var2, then false is ignored // works like: (($var2 = true) and false) var_dump( $var, $var2 ); //> bool(false), bool(true)
Useful link on this topic: Operator precedence
Short-circuit operators (short-circuit)
Docs: https://www.php.net/manual/ru/language.operators.logical.php
Short-circuit operators are simply a technique (a trick) that uses the logical operators && and || in order to shorten sequential checks of boolean expressions (values).
For example:
if( $foo && some_check() ){
// do staff
}
Here && is a short-circuit operator: the first check is done, and if it’s true, then the second one is done. If it’s false, then the second check is never reached and the code runs bypassing the second check (short-circuiting).
The first check is fast, and if it doesn’t pass, there’s no point in doing the second one—that’s exactly what happens.
The code above can be written like this (but it’s long and likely less readable):
if( $foo ){
if( some_check() ){
// do staff
}
}
More examples where it’s convenient to use short-circuit operators:
// foo() will never be called // because these operators are short-circuit (short-circuit) $a = false && foo(); $b = ( false and foo() ); $c = true || foo(); $d = ( true or foo() );
When comparing with AND &&, if the first condition returns false/0/''/array(), then there’s no point in checking the following conditions, because the full if condition (expression) will be executed only if all the nested conditions return true immediately (something other than empty).
When comparing with OR ||, if at least one condition returns true or something other than empty, then there’s no point in checking the following conditions, because the full if condition (expression) runs when at least one of the sub-conditions returns true (something other than empty).
count() does not always give the expected result
var_dump( count(false) ); //> int(1)
var_dump( count(0) ); //> int(1)
var_dump( count('') ); //> int(1)
var_dump( count(array()) ); //> int(0)
// same with sizeof()
var_dump( sizeof(false) ); //> int(1)
var_dump( sizeof(0) ); //> int(1)
var_dump( sizeof('') ); //> int(1)
var_dump( sizeof(array()) ); //> int(0)
isset() and null
We’re all used to checking whether a value exists in an array using isset(). However, if the element exists but its value is null, then isset() will return false as if the element were not present.
The presence of an element with a null value can be checked with the array_key_exists() function.
$array = array('first' => null, 'second' => 4);
isset( $array['first'] ); //> false
array_key_exists( 'first', $array ); //> true
Unexpected behavior in PHP when passing foreach value by reference
$array = [ 'a', 'b', 'c' ];
foreach( $array as & $item ){ }
foreach( $array as $item ){ }
print_r( $array );
/*
Array
(
[0] => a
[1] => b
[2] => b
)
*/
We iterate through the array twice, doing nothing. So, in the end, there should be no changes. Right? Wrong!
What happened? In fact, nothing like that that we didn’t ask PHP to do ourselves. In the first loop, we declared a reference & $item which, after the loop finishes, points to the array element $array[2]. Then we iterate over the array again: on each step we assign the next value to the variable $item. Since in PHP variable scope isn’t limited to a compound operator block, the variable $item in the second loop is the same variable from the first loop. Therefore, at the same time that we set the value of variable $item, this same value is also assigned to element $array[2].
- Step 0: $item = $array[2] = $array[0] = a
- Step 1: $item = $array[2] = $array[1] = b
- Step 2: $item = $array[2] = $array[2] = b
For the full explanation see here.
To avoid bugs like this, when passing the value by reference in foreach, after the loop you must delete $val using unset( $val ):
foreach( $array as & $val ){
// operations with $val
}
unset( $val );
empty() and objects
Checking empty() on objects can behave strangely. Suppose we have an object $obj and we check whether the var property is empty, and we get the following:
if( empty( $obj->var ) ){
// the condition will run
}
if( ! $obj->var ){
// the condition will not run
}
A paradox! How can it be? empty() says the property is empty, while ! says there’s something in it. How can the same property be empty and not empty at the same time? Quantum superposition, gentlemen...
However, if you look into it, there’s nothing surprising here and it’s perfectly logical!
The thing is that the empty() construct accesses the built-in object method __isset(), while a direct request to the property $obj->var will call the object method __get().
So empty() and ! request different methods if the property is not set:
class FOO {
function __get( $name ){
if( $name == 'bar' ) return true;
}
}
$obj = new FOO;
var_dump( empty( $obj->bar ) ); //> bool(true) - the variable does not exist
var_dump( ! $obj->bar ); //> bool(false) - the variable exists
Now, let’s set the value of property bar in __isset(), and then empty() will get it:
class FOO {
function __isset( $name ){
if( $name == 'bar' ) return true;
}
function __get( $name ){
if( $name == 'bar' ) return true;
}
}
$obj = new FOO;
var_dump( empty( $obj->bar ) ); //> bool(false) - the variable exists
var_dump( ! $obj->bar ); //> bool(false) - the variable exists
Number incrementer ++
It matters a lot where to use ++ (increment).
++$i — first increases the value of $i by 1 and then returns it.
$i++ — first returns the value of $i and then increases it.
$i = 0; echo $i++; //> 0 - the number increases on the next call echo $i; //> 1 - increased echo ++$i; //> 2 - the number increases immediately // now $i = 2 // you can increase inside conditions, array indices—anywhere if( $i++ == 2 ) echo $i; //> 3 $array[ ++$i ]; //> requesting an array element with index 4 // however you need to consider the position of the incrementer—before or after the variable // in both cases, the checked number will be different... // now $i = 4 $array = array( 5 => 'foo' ); $array[ $i++ ]; //> error - the index doesn’t exist because we’re requesting 4
-- — the decrementer (decrement) works exactly the same...
Let’s repeat once again:
| Example | Name | Action |
|---|---|---|
| ++$a | pre-increment | Increases $a by 1, then returns the value of $a. |
| $a++ | post-increment | Returns the value of $a, then increases $a by 1. |
| --$a | pre-decrement | Decreases $a by 1, then returns the value of $a. |
| $a-- | post-decrement | Returns the value of $a, then decreases $a by 1. |
String increment ++
With numbers everything is pretty straightforward, but what happens if you increment strings?
$a = 'fact_2'; echo ++$a; //> fact_3 $a = '2nd_fact'; echo ++$a; //> 2nd_facu $a = 'a_fact'; echo ++$a; //> a_facu $a = 'a_fact?'; echo ++$a; //> a_fact? $a = 'Привет'; echo ++$a; //> Привет
When incrementing a string, PHP changes the last character to the next character in the alphabet. So, during increment, if the string ends with 2, then that 2 will change to 3. After t comes u. However, this operation makes no sense when the string ends with a non-alphabetic/non-numeric character (in the example above, it’s the Cyrillic character).
This is well described in the official documentation for increment/decrement operations, but many people don’t read that material because they didn’t expect to find anything special there.
Floating point inaccuracies
Calculate this arithmetic and tell me the result:
echo intval( (0.1 + 0.7) * 10 );
Does it come out as 8? But the computer gives you 7!
This happens because computers aren’t good at working with imprecise numbers—this turns out to be a big and old problem, and there’s even an article on this topic: “What Every Computer Scientist Should Know About Floating-Point Arithmetic.”
So what is the final result and where is the error?
0.1 + 0.7 = 0.79999999999 0.79999999999 * 10 = 7.9999999999 intval( 7.9 ) = 7, not 8. When a value is converted to int, PHP cuts off the fractional part.
However, if you calculate it like this, you’ll see 0.8 instead of 0.79999999999. Though this result is only rounding:
echo 0.1 + 0.7; //> 0.8
Here’s an example of serializing a fractional value:
$str = serialize( 0.43 ); //> d:0.429999999999999993338661852249060757458209991455078125; echo unserialize( $str ); //> 0.43
You can solve the serialization problem like this ini_set( 'serialize_precision', -1 );. More here.
What conclusion can you make from this example? Be very careful when it comes to fractional numbers (floating point numbers) and never trust them blindly.
For additions and subtractions of float numbers in PHP there are special functions: bcadd(), bcsub(). For example:
echo bcadd(0.1, 0.7, 1); // 0.8 echo intval( bcadd(0.1, 0.7, 1) * 10 ); // 8
Object pointers on assignment (objects by reference?)
For example, let’s look at this code where we use an object for convenience, which is created from an array:
$data = (object) [ 'my_val' => 'bar', ]; $var = $data; $var->my_val = 'new_bar'; echo $data->my_val; //> new_bar
The problem here is that when we do assignment $var = $data;, we store the object ID (a pointer to the object) in variable $var, not the object itself. Next, if we look inside the object using ->—$var->my_val = 'new_bar';—we change the “inside” of the object, meaning we work with the object itself, not with the variable that stores the pointer to this object. That’s why it doesn’t matter which variable we use—in the end we always work with the object itself.
In this example, we have two variables ($data and $var) that store where in memory this object is located.
For example, let’s do the same with an array—everything will work differently:
$data = [ 'my_val' => 'bar', ]; $var = $data; $var['my_val'] = 'new_bar'; echo $data['my_val']; //> bar
To make the first example (with an object) work the same way as the array example, we need to create a copy of the object (clone it) and store its ID in the variable. We use clone for that:
$data = (object) [ 'foo' => 'bar', ]; $var = clone $data; $var->foo = 'new_bar'; echo $data->my_val; //> bar
Note! All of this doesn’t give us the right to think that objects are passed by reference! References in PHP work differently; see the examples below read here.
Objects are NOT passed by reference!
I often hear the phrase “Objects in PHP are passed by reference”—technically that’s not true. In PHP there are references (&) and they work differently! However, from a practical standpoint, this simplification doesn’t cause problems when communicating with other developers.
Let’s look at an example. To understand what passing by reference is, let’s see how variables that are passed by reference behave:
function func( & $input ){
$input = 'bar';
}
$var = 'foo';
func( $var );
var_dump( $var ); // string(3) "bar"
Here variable $var is passed to the function by reference, and when we change variable $input inside the function, it also changes the value of $var. This happens because we pass to the function a reference to variable $var (more precisely, the function accepts a parameter that is a reference to the passed variable). In this case, we can confidently say that the variable is passed by reference, not by value. That’s how references work in PHP.
Now let’s do the same for an object, but this time we won’t specify & and we’ll see whether the code behaves the same when passing the object:
function func( $input ){
$input = 'bar';
}
$var = new stdClass();
$var->foo = 'Hello';
func( $var );
print_r( $var ); // stdClass Object( [foo] => Hello )
As we can see, the change inside the function did not change the variable $var—it still contains the object. If the object were passed by reference, then when we change $input, the value of $var should also change, but it didn’t. So technically the phrase “Objects in PHP are passed by reference” is incorrect!
But on the other hand, this code does modify the object itself:
function func( $input ){
$input->foo .= ' World!';
}
$var = new stdClass();
$var->foo = 'Hello';
func( $var );
print_r( $var ); // stdClass Object( [foo] => Hello World! )
So what’s going on? Simplifying the mechanism, we can imagine it like this: when we create an object using the new operator and assign it to a variable, the variable stores not the object itself, but some object identifier, id.
When passing the variable as an argument inside some function, we pass the value of that identifier—so the passing happens by value. Importantly, the value is not the object itself, but its identifier.
Thus, both outside and inside the function, since we have the same object identifier value, we work with the same object.
But what if inside the function we assign something to the variable, for example null—will it affect the object outside the function? No. We nullified the variable holding the object id inside the function, but outside the function the external variable still contains the object id and the object hasn’t gone anywhere from memory.
More details: https://5minphp.ru/episode83/
Adding arrays
When adding arrays, elements from the added array do not replace the original ones, as you often expect.
$arr1 = [ 'key1'=>'val_1', 'key2'=>'val_2' ]; $arr2 = [ 'key1'=>'val_3', 'key2'=>'val_4', 'key3'=>'val_5' ]; print_r( $arr1 + $arr2 ); /* Array [key1] => val_1 [key2] => val_2 [key3] => val_5 */
$arr1 = [ 'val_1', 'val_2' ]; $arr2 = [ 'val_3', 'val_4', 'val_5' ]; print_r( $arr1 + $arr2 ); /* Array [0] => val_1 [1] => val_2 [2] => val_5 */
Changing the data type in array keys
When creating an array index, PHP automatically converts the data type. This should be considered when working with associative arrays. For example, if you pass a number as a string ('555') into the index, then it will become a number in the index; or if you pass true, it will become the number 1, while null will turn into an empty string. See the code example below.
Excerpt from php.net — Arrays.
In an array, key can be either type integer or type string. value can be any type.
Additionally, the following conversions will be made for key key:
-
Strings containing an integer (excluding cases where the number is preceded by the + sign) will be converted to
integer. For example, a key with the value"8"will actually be stored with the value 8. On the other hand, the value"08"will not be converted because it is not a valid decimal integer. -
Floating point numbers (type
float) will also be converted tointeger, i.e. the fractional part will be discarded. For example, a key with the value 8.7 will actually be stored with the value 8. -
bool type is also converted to
integer. For example, a key with the valuetruewill be stored as 1 and a key with the valuefalsewill be stored as 0. -
Type
nullwill be converted to an empty string. For example, a key with the valuenullwill actually be stored with the value"".
Arrays (type array) and objects (type object) cannot be used as keys. If you attempt to use them, a warning will be generated: Invalid offset type (Illegal offset type). - If multiple elements with the same key are specified, then only the last one will be used and all others will be overwritten.
Here’s an example:
$arr = [
'555' => 'val-1', // int(555) (will be removed)
555 .'' => 'val-2', // int(555)
'bar' => 'val-3', // "bar"
false => 'val-4', // int(0) (will be removed)
true => 'val-5', // int(1) (will be removed)
null => 'val-6', // string(0) ""
0 => 'val-7', // int(0)
1 => 'val-8', // int(1)
8.7 => 'val-9', // int(8)
'08' => 'val-10', // string(2) "08"
'val-11', // int(556)
'→' => 'val-12', // "→"
];
var_dump( $arr );
/*
array(9) {
[555]=> string(5) "val-2"
["bar"]=> string(5) "val-3"
[0]=> string(5) "val-7"
[1]=> string(5) "val-8"
[""]=> string(5) "val-6"
[8]=> string(5) "val-9"
["08"]=> string(6) "val-10"
[556]=> string(6) "val-11"
["→"]=> string(6) "val-12"
}
*/
Closure::call — calling an anonymous function with a specified context
This isn’t so much an unexpected thing as an interesting feature that not many people know about.
PHP closures (anonymous functions) can be called by passing context (an object) into them. As a result, the closure can be used as a method of the passed object.
For this, the closure object has a method:
call( $that, ...$params )
- $that(object)
- Object to bind the closure to for the duration of its call.
- ...$params
- Any number of parameters that are passed to the closure.
Example of how to use this
class Value {
protected $value;
function __construct( $value ){
$this->value = $value;
}
function get_value(){
return $this->value;
}
}
$three = new Value( 3 );
$four = new Value( 4 );
$closure = function( $delta ){
echo $this->get_value() + $delta;
};
$closure->call( $three, 4 ); // 7
$closure->call( $four, 4 ); // 8
What do we see? When calling the same closure, we get different results depending on the call context (which object is passed and used inside the closure).
A recursive anonymous (lambda) function
Return by reference for function/method $var = & func()
Passing variables by reference is a clear concept; but references for calling functions/methods are not quite the same.
Such a reference points to the variable that the function returns (i.e. it binds the returned variable to the variable where the result of the function will go).
This makes sense if the function has static variables that won’t be removed after the function finishes, and to which we need to have access from outside.
Let’s consider an example:
function & test( $file = null ){
static $one = 1;
static $two = 2;
echo "$one, $two\n";
return $two;
}
$two = & test();
$two = 222;
test();
/*
1, 2
1, 222
*/
When calling a function, the reference (&) must be specified; otherwise the function will work as usual—without binding to the internal variable.
$two = & test(); // reference works $two = test(); // reference does not work
Using references of this kind to speed up code is pointless. Passing functions by reference is not related to performance (PHP handles that). Read in detail in the documentation.
Since PHP 8.1.0, returning by reference from a void function has been deprecated, because such a function is contradictory. Previously, in this situation, an error level E_NOTICE was thrown: Only variables should be returned by reference.
function & test(): void {
}Another example:
function & get(){
static $data = null;
return $data;
}
$var = & get();
var_dump( $var ); // NULL
$var = 'Hello world';
echo get(); // Hello world
As you can see, we obtained the value of the function’s static variable. And we also created a link (a reference) to this static variable. Now, when we change the external variable $var, we change the internal function variable.
NOTE: if you remove the keyword static, the reference will stop making sense, because after the first call the function’s internal variable will be cleared:
function & get(){
$data = null;
return $data;
}
$var = & get();
var_dump( $var ); // NULL
$var = 'Hello world';
echo get(); // empty string (NULL)An analogous example with an object:
class My_Class {
public $value = 42;
public function & get_value() {
return $this->value;
}
}
$class = new My_Class();
echo $class->value; // 42
$val = & $class->get_value();
$val = 2;
echo $class->value; // 2
A reference from a function can be passed to another function that expects a reference:
function & collector() {
static $collection = array();
return $collection;
}
array_push( collector(), 'myval' );
print_r( collector() );
/*
Array (
[0] => myval
)
*/
Another example of changing an array element value:
class Config {
private array $settings = [
'db_host' => 'localhost',
'db_user' => 'root',
'db_pass' => 'secret',
];
public function &getSetting(string $key) {
return $this->settings[$key];
}
}
$config = new Config();
// Get a reference to an array element through a method
$dbUser = &$config->getSetting('db_user');
$dbUser = 'admin'; // Change the value
print_r( $config );
/*
Config Object (
[settings:Config:private] => Array (
[db_host] => localhost
[db_user] => admin
[db_pass] => secret
)
)
*/
Let’s consider one more example that clearly shows the difference between calling by reference and without:
function & func(){
static $static = 0;
$static++;
return $static;
}
$var1 = & func();
echo $var1; // 1
func();
func();
echo $var1; // 3
$var2 = func(); // call without &
echo $var2; // 4
func();
func();
echo $var1; // 6
echo $var2; // 4
elseif, not else if
// Best is to write elseif, not else if
if( $a > $b ){
echo '$a > $b';
} elseif( $a == $b ){
echo '$a == $b';
}
// You can also do it like this, but it’s not recommended
if( $a > $b ){
echo '$a > $b';
} else if( $a == $b ){
echo '$a == $b';
}
// But this variant (without curly braces and separate else if)
// will cause a syntax error
if($a > $b):
echo '$a > $b';
else if($a == $b): // Parse Error: syntax error, unexpected 'if' (T_IF)
echo '$a == $b';
endif;
isset() is 2 times faster than in_array()
The speeds are very fast, but if large arrays are processed, it makes sense to use array_flip() and search for the value via isset():
$arr = array( 5, 6, 7, 8, 9, 10, 11, 12, 13 );
$arr2 = array_flip( $arr ); // [5] => 0 [6] => 1 [7] => 2 [8] => 3 [9] => 4 [10] => 5 [11] => 6 [12] => 7 [13] => 8
for( $i = 1; $i < 500000; $i++ ){
in_array( 5, $arr ); //> 0.03150 sec.
isset( $arr2[5] ); //> 0.01552 sec.
}
Static methods in traits can be used directly
trait FOO {
static function foo(){
echo 'bar';
}
}
FOO::foo(); // bar
However, remember that creating traits without using them (e.g. as a storage for static methods) is bad practice.
Type declarations: strict typing is not that strict
Everything below concerns code where declare( strict_types=1 ); is not used.
With this declaration, all types must be exactly the ones specified! There is only one exception—an integer (int) can be passed to a function that expects a float.
Type declarations convert types:
function input( bool $val ) {
var_dump( $val ); // bool(true)
}
input( 1 ); // bool(true)
input( 'string' ); // bool(true)
input( '' ); // bool(false)
input( '0' ); // bool(false)
input( [] ); // Uncaught TypeError: Argument 1 passed to input() must be of the type bool, array given
function input( int $val ) {
var_dump( $val ); // bool(true)
}
input( '1' ); // int(1)
input( '0' ); // int(0)
input( '2.7' ); // int(2)
input( 'string' ); // Uncaught TypeError: Argument 1 passed to input() must be of the type int, string given
input( '' ); // Uncaught TypeError: Argument 1 passed to input() must be of the type int, string given
input( [] ); // Uncaught TypeError: Argument 1 passed to input() must be of the type int, array given
function output( $return ): bool {
return $return;
}
var_dump( output( 1 ) ); // bool(true)
var_dump( output( 'string' ) ); // bool(true)
var_dump( output( '' ) ); // bool(false)
var_dump( output( '0' ) ); // bool(false)
var_dump( output( [] ) ); // Uncaught TypeError: Return value of output() must be of the type bool, array returned
empty() includes isset()
Very often I see code written like this:
isset( $vars[1] ) && !empty( $vars[1] ) // or isset( $vars ) && !empty( $vars[1] )
The first check is redundant! You can simply use !empty( $vars[1] ).
empty()is equivalent to!isset($foo) || !$foo!empty()is equivalent toisset($foo) && $foo.
So, empty does the same thing as isset, plus it additionally checks for the presence of a value.
So, empty is the same as !$foo, but it does not output a warning if the variable doesn’t exist.
That’s the main purpose of this function: perform the comparison without worrying that the variable might not exist.
Moreover, when you use isset, all nested properties are checked as well; for example, the following code will not output any warnings:
$foo = null; var_dump( empty( $foo->bar->baz ) ); // true $foo2 = 777; var_dump( empty( $foo2->bar->baz ) ); // true
Instead of null you can have anything: an array, a number, a string, an object. In other words, empty simply suppresses any runtime warnings.
But of course it doesn’t work with parsing errors (syntax errors), because when there is a syntax error, the script never starts. For example:
empty( null->bar->baz ); // Parse error
In good code, empty() signals that the variable might not be there and that it’s acceptable. But if the variable is required to be present, then checking such a variable using empty (as is very often done) will simply suppress a logical error in the code if the variable gets renamed or removed during refactoring. And you won’t see the warning ahead of time—you’ll catch the bug later and then have to find the cause manually! A static analyzer will likely not complain either, because empty assumes the absence of a variable is allowed.
The same applies to isset()
$foo = null; var_dump( isset( $foo->foo->bar ) ); // false
Don’t bloat the code—write it short and readable!
static for Closure
Why it’s important to specify static when declaring anonymous functions. Why make callbacks in a sorting function (usort) static?
From the documentation:
When declared in the context of a class, the current class is automatically bound to the Closure, making $this available inside the Closure. If you do NOT need such binding to the current class, use static anonymous functions.
So, when a Closure is declared in the context of a class, the class is automatically bound to the closure and $this is available inside the anonymous function.
An example that shows this problem:
class LargeObject {
protected $array;
public function __construct() {
$this->array = array_fill( 0, 2000, 15 );
}
public function getItemProcessor(): Closure {
return function () {
return 1 + 2;
};
}
}
$processors = [];
for( $i = 0; $i < 2000; $i++ ){
$processors[] = ( new LargeObject() )->getItemProcessor();
}
echo sprintf( '%.2f MiB', memory_get_peak_usage() / 1024 / 1024 ); // 138.86 MiB
As we can see, the process consumed 138.86 MB of memory, essentially out of nowhere.
This happens because in $processors[] we keep accumulating an array in which there are Closures bound to the object; therefore, the object can’t be removed by the garbage collector until the variable $processors is deleted.
Now let’s add static:
return static function () {
return 1 + 2;
};
And now memory usage will be the reasonable 5.86 MB, not 138.86 MB.
Conclusion
In short: anonymous functions without static should be used if you need to bind an object to the function execution scope. In all other cases, you can and should use static—at least to avoid accidentally shooting yourself in the foot.
Getting values of private properties or methods
Sometimes you need to get the value of a class property that has private (private/protected) visibility.
Usually this is done via reflection new ReflectionClass().
However, you can also do it via a closure to which you can pass context:
class Test {
protected $protect = 'Protected variable.';
private $priv = 'Private variable.';
protected function protected_func(){
return 'protected function.';
}
private function private_func(){
return 'Private function.';
}
}
$Test = new Test();
print_r( [
'protect' => ( fn() => $this->protect )->call( $Test ),
'priv' => ( fn() => $this->priv )->call( $Test ),
'protected_func' => ( fn() => $this->protected_func() )->call( $Test ),
'private_func' => ( fn() => $this->private_func() )->call( $Test ),
] );
Running private methods
Suppose we have a class with a private method:
class Counter {
private function is_file( $url ): bool {
// ...
}
}
Now in tests we need to run this private method. You can do it via Closure::bind():
$counter = new Counter(); $call = Closure::bind( fn( $url ) => $this->is_file( $url ), $counter, Counter::class ); $call( 'https://ex.com/file.pdf' ); // true
You can also run a static private method:
$wp_line = Closure::bind( static fn() => Bootstrap::detect_line(), null, Bootstrap::class )();
Another example of creating an instance without calling the constructor:
$call = Closure::bind( fn( $url ) => $this->is_file( $url ), ( new ReflectionClass( Counter::class ) )->newInstanceWithoutConstructor(), Counter::class ); $call( 'https://ex.com/file.pdf' ); // true
You can also use regular functions (not arrow functions):
$call = Closure::bind(
function( $url ){ return $this->is_file( $url ); },
$counter,
Counter::class
);
Or you can use the bindTo( $instance, $scope ) method:
$call = ( fn( $url ) => $this->is_file( $url ) )->bindTo( $counter, Counter::class );
How does it work?
Closure::bind( $closure, $object, $scope ) — returns a new closure where:
$thispoints to$object($counter)- And the scope is
$scope(Counter::class), so private/protected methods are available.
How is print different from echo
The difference between print and echo in PHP is minimal:
-
echo– can output multiple arguments separated by commas, does not return a value (cannot be used in expressions).echo "Hello", " World!"; var_dump( echo 'foo' ); // Parse Error: syntax error, unexpected 'echo'
print– accepts only one argument and returns 1 (can be used in expressions).var_dump( print 'bar' ); // int(1)
print can be used in ternary operators:
$term ? print $term->desc : echo_content(); // or $term ? print markdown( $term->description ) : the_content();
The goto construct
In PHP, it’s sometimes convenient to use a goto construct.
For example, we need to connect to a database, but the first time it might not work—we need to try several times with a delay. You can write this with a while loop, or you can write it like this:
$max_retries = 5;
$delay = 1;
$retries = 0;
CONNECTION_RETRY: {
try {
$client->connect( ...$arguments );
}
catch ( Exception $exception ) {
if ( ++$retries >= $max_retries ) {
throw $exception;
}
usleep( $delay * 1000 );
goto CONNECTION_RETRY;
}
}
Another simple example:
$count = 1;
CONNECTION_RETRY: {
var_dump( $count );
if( $count++ < 4 ){
sleep( 1 );
goto CONNECTION_RETRY;
}
}
/*
int(1)
int(2)
int(3)
int(4)
*/
Comparing DateTime and DateTimeImmutable
When working with DateTime or DateTimeImmutable objects, you sometimes need to compare their time (greater/less/equal).
Usually they’re compared using a UNIX timestamp obtained via the getTimestamp() method. Or you can use the diff() method, which calculates the difference between dates.
However, few people know that date objects can be compared using the regular comparison operators >, <, and ==:
$date1 = new DateTime("now");
$date2 = new DateTime("tomorrow");
var_dump( $date1 == $date2 );
var_dump( $date1 < $date2 );
var_dump( $date1 > $date2 );
An example of this is included in the documentation for the DateTimeInterface::diff() method.
Such comparison is possible because PHP, under the hood, uses the timelib library by Derick Rethans (author of Xdebug).
fn() => $var - arrow functions and external variables
While writing unit tests, I once needed to mock cache behavior. I decided to store the cache value in a local variable of the test function.
Here’s an example that demonstrates how this works:
$cache = '';
$get = function() use ( & $cache ){
return $cache;
};
$set = function( $val ) use ( & $cache ){
$cache = $val;
};
$set( 'hello' );
echo $get(); // hello
But initially, I tried to do this with arrow functions, which allegedly don’t have a context and have access to variables from the outer scope “opened” for them. However, this code doesn’t work:
$cache = ''; $get = fn() => $cache; $set = fn( $val ) => $cache = $val; $set( 'hello' ); var_dump( $get() ); // string(0) ""
Why does that happen? Let’s refer to the documentation:
Both anonymous functions and arrow functions are implemented using the Closure class.
Arrow functions work the same way as anonymous functions, except that access to variables from the parent scope is done automatically.
When an arrow function uses a variable that was defined in the parent scope, the variable is implicitly captured by value.
Arrow functions use value binding. This is roughly equivalent to executing
use($x)for each variable$xthat is used inside the arrow function. Capturing by value means that you cannot modify any values in the outer scope.
By reference you can only pass and return variables:
fn( & $x ) => $x; // passes the variable by reference, returns by value fn &( $x ) => $x; // passes by value, returns by reference
Reassigning private properties of a class
Suppose we have an abstract class with a private property and a child class with the same private property $age:
abstract class Father {
private int $age = 56;
public function __construct(
protected string $name
) {
}
}
final class Child extends Father {
private int $age = 22;
public function __construct(
protected string $name,
) {
parent::__construct( $name );
}
}
The private properties of both objects will be separate because they’re private! For example, if we now run var_dump(), we’ll see 3 properties, not 2:
print_r( new Child( 'Jhon' ) ); /* Child Object ( [age:Father:private] => 56 [name:protected] => Alex [age:Child:private] => 22 ) */
To get all this data in PHP—for example, to later cache the object—you can cast the object to an array:
print_r( (array) new Child( 'Jhon' ) ); /* Array ( [\0Father\0age] => 56 [\0*\0name] => Jhon [\0Child\0age] => 22 ) */
Where this might be useful, see this video: https://www.youtube.com/watch?v=YvJXq9aJwpQ
Libraries from the video:
A loop that will execute N times - for( $i = 2;; )
An interesting approach to run a loop a specific number of times.
for( $i = 2;; ){
var_dump( $i );
if( --$i === 0 ){
break;
}
}
It will output:
int(2) int(1)
The construct for( $i = 2;; ), used together with --$i inside the loop, is a way to limit the number of iterations the loop will perform. Despite its unusualness, it’s a valid use of the “for” loop construct in PHP.
In this example, we start with $i = 2. Then inside the loop we decrease the value by 1 each time --$i.
So, on the first pass $i = 2. On the second iteration $i = 1. Then $i = 0—then we hit the break condition.
A clearer and more readable version of the code above can be written like this:
$i = 2;
while( $i > 0 ){
var_dump( $i );
$i--;
}
NOTE: Honestly, I don’t know why you would write something confusing when you can write it more clearly, but the approach seemed interesting to me, so it’s included here.
Fatal error when creating an object before class declaration
FooTraite.php
<?php
trait FooTrait {
}
FooClass.php
<?php
new FooClass();
class FooClass {
use FooTrait;
public function __construct() {
echo 'Hello World!';
}
}
Run
run.php
<?php require_once __DIR__ . '/FooTraite.php'; require_once __DIR__ . '/FooClass.php';
We get a fatal error:
Fatal error: Uncaught Error: Class "FooClass" not found in /app/parser/FooClass.php:3
However
If you comment out the trait, everything works.
If you extend it too, it also works:
class FooClass extends FooParent {}
Why does it happen like this?
This is a PHP feature related to hoisting (class lifting).
PHP hoists (hoist) a class to the beginning of the file only if it can fully resolve its dependencies at the compilation stage.
- With
extends— the parent class is already in memory → dependencies are resolved → hoisting works - With
use Trait— traits are resolved in runtime always, regardless of whether the trait is loaded or not → hoisting is impossible →new FooClass()before the class declaration = Fatal Error
--
Used when writing:
- Personal observations
- http://php.net
