PHP for the Reluctant

Brennen Bearnes

2020-08-28

So you have to read/review/debug some PHP

I know, I know, it’s gross.

Agenda

My goal is to convey:

The capsule history

The greatest hits

A whole lot of the web has been built on PHP.

How did this happen?

An astonishing percentage of the web has been built on PHP.

Why? PHP totally sucks, right?

Probably, but:

Basics

By 2020: Modern features, often implemented a little weird

About that execution model

Hello world

The simplest example in PHP is just a file containing a string literal:

Hello world.

Output:

Hello world.

Hello world, take 2

For a more traditional hello world, you’d write:

<?php
print "Hello world.\n";

Output:

Hello world.

Hello world, take 3

Or maybe:

<?php
    $name = 'world';
?>
Hello, <?= $name ?>.

Output:

Hello, world.

Variables

Variable scope

<?php
// Global:
$foo = 1;

// Local to function or method:
function bar() {
    $foo = 2;
    return $foo;
}

// Accessing a global within a function:
function baz() {
    global $foo;
    $foo = 3;
}

print "$foo\n";
print bar() . "\n";
baz();
print "$foo\n";

Output:

1
2
3

Constants

<?php
// Define a constant - all-caps by convention:
define('NAME', 'VALUE');

print NAME . "\n";

// Barewords can be risky since they're treated as string literals
// for undefined constants.  You can use constant() instead:
print constant('NAME');

Output:

VALUE
VALUE

Types

Booleans

<?php
// Literals, case-insensitive:
$are_cats_mammals = true;
$are_elephants_reptiles = false;

Numbers

<?php
// Floating point:
$foo = 1.234;

// Integer:
$foo = 1;

Numbers, fancy notation

You’ll rarely encounter any but the first syntax for these, but they do work:

<?php
// Floating point:
$foo = 1.234;
$foo = 1.2e3;
$foo = 7E-10;

# $foo = 1_234.567;

// Integer:
$foo = 1;
$foo = 0123;       // octal number (equivalent to 83 decimal)
$foo = 0x1A;       // hexadecimal number (equivalent to 26 decimal)
$foo = 0b11111111; // binary number (equivalent to 255 decimal)

# $foo = 1_234_567;  // decimal number (as of PHP 7.4.0)

Strings

<?php
$var = 'variable';

$double = "double-quoted with $var interpolation and escapes.\n";
$single = 'single-quoted';

// String concatenation, like in Perl:
print $double . $single;

Output:

double-quoted with variable interpolation and escapes.
single-quoted

Strings: Heredocs & Nowdocs

An alternative string quoting mechanism, a la shell or Perl:

<?php
$foo = <<<EOT
bar
EOT;

To avoid variable interpolation and escapes, enclose the end marker in single quotes:

<?php
$bar = <<<'EOD'
Backslashes are always treated literally, e.g. \\ and \', $blah.
EOD;

Arrays

<?php
// Old syntax:
$arr = array( 1, 2, 3 );

// Short syntax:
$arr = [ 1, 2, 3 ];

print_r($arr);

Output:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)

Arrays: The Swiss Army Knife of PHP data structures

Arrays: An ordered list

<?php
// Mixing types is perfectly legal:
$foo = [ 'one', 2, 3.0 ];

// Shorthand for pushing onto end of array:
$foo[] = 'IV';
print_r( $foo );

Output:

Array
(
    [0] => one
    [1] => 2
    [2] => 3
    [3] => IV
)

Arrays: Key / value

<?php
$relenger_nicks = [
    'dancy' => 'Ahmon Dancy',
    'brennen' => 'Brennen Bearnes',
    'liw' => 'Lars Wirzenius',
    'longma' => 'Jeena Huneidi',
];

print_r($relenger_nicks);

Output:

Array
(
    [dancy] => Ahmon Dancy
    [brennen] => Brennen Bearnes
    [liw] => Lars Wirzenius
    [longma] => Jeena Huneidi
)

Arrays: Nested data structures

<?php
$relengers = [
    [
        'nick' => 'dancy',
        'name' => 'Ahmon Dancy',
        'editors' => [ 'emacs', 'vim' ]
    ],
    [
        'nick' => 'brennen',
        'name' => 'Brennen Bearnes',
        'editors' => [ 'vim', 'nano', 'edit.com' ]
    ],
];

print $relengers[1]['name'] . ' uses ' . $relengers[1]['editors'][0];

Output:

Brennen Bearnes uses vim

Typecasting

Implicit coercion is common, and (unsurprisingly) a frequent cause of bugs. (More about that in a minute.)

Types can be explicitly cast like so:

<?php
$foo = true;

$foo_float = (float)$foo;
$foo_int   = (int)$foo;
$foo_str   = (string)$foo;
$foo_bool  = (bool)$foo;

Truth values

<?php
// Falsey things:
false, 0, -0, 0.0, -0.0, '0', '', null, []

// ...plus undefined variables.

Aside from some edge cases, everything else should be truthy.

Type comparisons

Which brings us to the type comparison table:

Type comparisons: Loose vs. strict

<?php
var_dump('monday' == 'tuesday');
var_dump('monday' == true);
var_dump('monday' == 1);
var_dump('monday' === true);
var_dump('monday' === 1);

Output:

bool(false)
bool(true)
bool(false)
bool(false)
bool(false)

Conditionals: if and friends

<?php
$day = 'monday';

if ( $day === 'monday' ) {
    print "At least it's not Tuesday.\n";
} elseif ( $day === 'tuesday' ) {
    print "Actually the worst day.\n";
} else if ( $day === 'wednesday' ) {
    print "Looking up.\n";
} else {
    print "Basically the weekend.\n";
}

Output:

At least it's not Tuesday.

Conditionals: if and friends, short version

<?php
$day = 'monday';

if ( $day === 'monday' ):
    print "At least it's not Tuesday.\n";
elseif ( $day === 'tuesday' ):
    print "Actually the worst day.\n";
elseif ( $day === 'wednesday' ):
    print "Looking up.\n";
else:
    print "Basically the weekend.\n";

Switch statements

<?php
$day = 'monday';

switch ( $day ) {
    case 'monday':
        print "At least it's not Tuesday.\n";
        break;
    case 'tuesday':
        print "Actually the worst day.\n";
        break;
    case 'wednesday':
        print "Looking up.\n";
        break;
    default:
        print "Basically the weekend.\n";
}

Output:

At least it's not Tuesday.

Major caveat: Switch statements use loose type comparison.

loops: for

C-style:

<?php
for ( $i = 0; $i < 10; $i++ ) {
    print $i;
}
print "\n";

Output:

0123456789

loops: foreach

More idiomatic than C-style iteration:

<?php
$stars = [ 1, 3, 7 ];
foreach ( $stars as $count ) {
  print str_repeat( '*', $count ) . "\n";
}

Output:

*
***
*******

loops: foreach with an associative array

<?php
$relenger_nicks = [
    'dancy' => 'Ahmon Dancy',
    'brennen' => 'Brennen Bearnes',
    'liw' => 'Lars Wirzenius',
    'longma' => 'Jeena Huneidi',
];

foreach ( $relenger_nicks as $nick => $name ) {
    print "$name goes by $nick on IRC\n";
}

Output:

Ahmon Dancy goes by dancy on IRC
Brennen Bearnes goes by brennen on IRC
Lars Wirzenius goes by liw on IRC
Jeena Huneidi goes by longma on IRC

loops: while and do-while

<?php
while ( true ) {
    print "In loop.\n";
    break;
}
print "Out of loop.\n";

// Far less common, but supported.
do {
    print "In loop.\n";
    break;
} while ( true );
print "Out of loop.\n";

Output:

In loop.
Out of loop.
In loop.
Out of loop.

Functions

A basic function

<?php
function meow( $count ) {
    $ret = '';
    for ( $i = 0; $i < $count; $i++ ) {
        $ret .= "meow\n";
    }
    return $ret;
}

echo meow( 5 );

Output:

meow
meow
meow
meow
meow

A basic function, fancier

We can specify parameter types and default values:

<?php
function meow( int $count = 1 ) {
    $ret = '';
    for ( $i = 0; $i < $count; $i++ ) {
        $ret .= "meow\n";
    }
    return $ret;
}

echo meow( 5.0 );

Output:

meow
meow
meow
meow
meow

…sort of fancier

…but there’s a catch. Unless strict typing mode is on, this will just result in casting $count to an integer.

<?php
declare( strict_types = 1 );
function meow( int $count = 1 ) {
    $ret = '';
    for ( $i = 0; $i < $count; $i++ ) {
        $ret .= "meow\n";
    }
    return $ret;
}

echo meow( 5.0 );

Output:

PHP Fatal error:  Uncaught TypeError: Argument 1 passed to meow() must be of the type int, float given, called in Standard input code on line 11 and defined in Standard input code:3
Stack trace:
#0 Standard input code(11): meow(5)
#1 {main}
  thrown in Standard input code on line 3

Passing by reference

<?php
function times_ten (int &$value) {
    $value = $value * 10;
}
$value = 10;
times_ten($value);
var_dump($value);

Output:

int(100)

Classes and objects

<?php
abstract class Encabulator {
    abstract public function reticulate();
}

class TurboEncabulator extends Encabulator {
    protected $_splinesReticulated = false;

    public function reticulate() {
        $this->_splinesReticulated = true;
    }

    public function splineStatus() {
        return $this->_splinesReticulated ? 'reticulated' : 'unreticulated';
    }
}

$te = new TurboEncabulator();
$te->reticulate();
print "Spline status: " . $te->splineStatus();

Output:

Spline status: reticulated

Magic methods

__construct(), __destruct(), __call(), __callStatic(), __get(), __set(),
__isset(), __unset(), __sleep(), __wakeup(), __serialize(),
__unserialize(), __toString(), __invoke(), __set_state(), __clone() and
__debugInfo()

Magic methods: Setters and getters

<?php
class ValueStash {
    private $_values;

    public function __construct( array $values ) {
        $this->_values = $values;
    }

    public function __get( $name ) {
        return $this->_values[$name];
    }

    public function __set ( $name, $value ) {
        $this->_values[ $name ] = $value;
    }
}

$editors = new ValueStash( [ 'brennen' => 'vim' ] );
print $editors->brennen;

Output:

vim

Traps for the unwary

With a basic overview of the language out of the way, let’s go over some common pitfalls and sources of recurring bugs.

Undefined array elements

<?php
$arr = [
    'foo' => 'bar',
];

$baz = $arr['fo'];

var_dump($baz);

Output:

PHP Notice:  Undefined index: fo in /tmp/phpslideoutput.php on line 7

Notice: Undefined index: fo in /tmp/phpslideoutput.php on line 7
NULL

Advice: Check with array_key_exists($key, $array) or otherwise guard array values.

Null objects

This comes up constantly.

<?php
$foo = new stdClass;

// Look, a typo:
$fo->some_method();

Output:

PHP Notice:  Undefined variable: fo in Standard input code on line 3

Notice: Undefined variable: fo in Standard input code on line 3
PHP Fatal error:  Uncaught Error: Call to a member function some_method() on null in Standard input code:3
Stack trace:
#0 {main}
  thrown in Standard input code on line 3

empty()

<?php
var_dump(empty('0'));
var_dump(empty('1'));

Output:

bool(true)
bool(false)

isset(), and is_null()

<?php
var_dump(isset($undefined));
var_dump(is_null($undefined));

$defined = null;
var_dump(isset($defined));

Output:

bool(false)
PHP Notice:  Undefined variable: undefined in /tmp/phpslideoutput.php on line 4

Notice: Undefined variable: undefined in /tmp/phpslideoutput.php on line 4
bool(true)
bool(false)

…questions / feedback?