LearningPHP

From HerzbubeWiki
Jump to: navigation, search

On this page I keep notes about my first steps with PHP. I'm starting this "project" because I'm planning to get an education in web programming. Other learning pages in this context are LearningJavaScript, LearningXML, LearningCSS and LearningHTML.


Contents

References

  • Einstieg in PHP 7 und MySQL. 12. Auflage, 2017. Thomas Theis. Rheinwerk Computing. ISBN 978-3-8362-4496-1. On this page I refer to this book with [EinstiegPhp7].
  • The official PHP documentation.


Glossary

PHAR 
PHP Archive. This is a file format used to put entire PHP applications into a single file, much like .jar in the Java world, for easy distribution and installation.
PHP 
A recursive acronym for "PHP: Hypertext Preprocessor".
SPL 
Standard PHP Library. A collection of interfaces and classes that are meant to solve common problems.
Superglobal variable 
A superglobal variable is a special built-in variable that is available in all scopes without the need to use the global keyword to make it available in an inner scope. PHP has several superglobal variables, a list is available in the PHP docs.


Annoyances

After merely a few days of working with PHP I have had an amazing variety of WTF moments. In my opinion PHP, despite its age, has not really matured into a well-rounded programming language / environment - there simply are too many surprises for my taste. I can only guess that in the past too much focus has been put on backwards compatibility, and not enough energy has been spent on getting rid of "old horses".

Here's a list of some of my annoyances:

  • I personally dislike the "malleability" of data types
  • String to integer conversion during type coercion: "01foo" becomes 1.
  • Using the current working directory as reference point for "." and ".." in include/require statements. This is not only an annoyance, in my opinion it's a security risk!
  • Superclass constructors and destructors are not called implicitly.
  • Namespace nesting does not work as I would expect it to. For instance, a class in the global namespace such as Exception must be fully qualified, otherwise the PHP resolver does not find the class.
  • No overloading. This is especially annoying because a class can have only one constructor. With normal methods the lack of overloading support can be worked around by giving each overloaded method its own dedicated name, but this workaround is not possible for constructors because those must have a special name (__construct()).
  • Variable names and constant names are case sensitive, but class names, function names and method names are not. I'm really impressed to what lengths the PHP language designers have gone to make their programming language look silly!


Basics

Basic working principle

PHP is an interpreted scripting language that is typically executed server-side by the web server when it is about to serve a document to the client. Just before it serves the document, the web server executes all PHP code that it can find inside the document file. The web server then replaces the PHP code it just executed by the output that the PHP code generated. The web server then serves the final result.

Actually, the web server delegates parsing and executing the PHP code to a PHP interpreter on the system. In Apache this is handled by an Apache module.

Of course, it is also possible to run PHP code with just a CLI interpreter and without any web server at all. In such a scenario PHP is no different from any other scripting language such as Python, Ruby, Perl, AWK or even a lowly shell script (for which the interpreter is a shell such as bash). For instance:

php foo.php


Embedding PHP

The web server must somehow recognize the PHP code that is embedded in the document and that it should execute. This is achieved by enclosing the PHP code like this:

<?php
  [...]
?>

Note: It is permissible to omit the closing tag, in which case everything until the end of file is treated as PHP code. In fact, if a file consist entirely of PHP code, then it is recommended to omit the closing tag to prevent accidental whitespace at the end of the file to be added to the output.


As mentioned in the previous section, the web server removes the PHP code on the fly when it executes the code and replaces it with whatever the code generates. This means that PHP code can be intermixed with normal content. Also, a file may contain several PHP code blocks.

Note:

  • <? [...] is equivalent to <?php [...]


Comments

The two usual syntaxes for comments exist:

// Single line comment

/*
 * Multi-line comment
*/

In addition, Unix shell-style comments using the hash character ("#") are also supported.


Variables & scope

Variables do not need to be declared. They simply start to exist when they are first used. Variables are used like this:

// Global variables
$a = 42;
$b = 123;

// Global variables are available in the included script.
#include "foo.inc.php"

function foo()
{
  // The function begins a new scope that hides the global scope.
  // So here $a is un-initialized.
  echo "$a";

  // Local scope still hides global variables $a and $b.
  // Assigning a value here does not change the value of
  // the global variables.
  $a = 17;
  $b = 555;

  // From now on we use the global variable $b. This hides
  // the previously established local variable $b.
  global $b;

  // $c is assigned 140 (17 + 123)
  $c = $a + $b;
  // Equivalent to the above. The $GLOBALS array is an array
  // for which the keys are the names of the global variables.
  // We can use the $GLOBALS array without using the "global"
  // keyword. $GLOBALS is a superglobal variable.
  $c = $a + $GLOBALS["b"];

  // Here we overwrite the value of the global variable $b
  $b = $a;

  // Static variables work just like in C/C++
  static $d = 0;
  $d++;
}

// Variable names can be variable, too.
// The following example echoes 42.
$hello = 42;
$variableName = "hello";
echo "$$variableName";

// Property names can be variable
$obj = ... // get an object from somewhere
echo "$obj->$variableName";

// Use braces to resolve ambiguities with variable variables.
// The following example echoes 17.
$hello = 17;
$a = array("hello");
echo "${$a[0]}";


Variable names

  • Must start with a letter
  • Can consist of numbers and letters and underscore ("_") characters
  • Are case sensitive


Variables can be referenced inside a string literal if the literal uses double quotes:

$hello = 42;
$variableName = "hello";

// The following both produce the same output
echo "hello = $hello";
echo "$variableName = ${$variableName}";


Un-initialized variables should be avoided, but this is what PHP tries to do if it happens:

// $a is first used in the context of an integer addition, so it is implicitly
// assigned integer 0 as the initial value
$a += 42;

// $b is first used in the context of a string concatenation, so it is implicitly
// assigned an empty string as the initial value
$b .= "foo";

// $c is first used without any particular context, so it is implicitly
// assigned NULL as the initial value
echo "$c";

// Etc.


Variables can be unset, and you can check whether a variable is set:

$a = 42;
if (isset($a)) { ... }
unset($a);


Constants

This is a constant declaration:

const FOO = 42;

Notes:

  • A constant is declared with the const keyword
  • The constant name must NOT be prefixed with a "$" character
  • Constant names are case sensitive. The convention is to use uppercase names for constants.
  • Constants must be declared at the top-level scope and, unlike variables, they have no scoping rules and are available everywhere


It is also possible to declare constants by using the define() function. The rules for these constants are slightly different, but since I don't see the need to use this different kind of constant declaration I'm not documenting it here. Cf. the PHP docs for details.


References

$a = 42;
$b = $a;   // copies the value
$c = &$a;  // creates a reference to the value

// Call by value
function foo($a, $b) { ... }

// Call by reference
function foo(&$a, &$b) { ... }


Types

Variables do not have a type, rather the type of the value they hold is determined at runtime. The context in which a variable is used may cause type coercion. Since PHP 7 function declarations can be written with type information:

function add(int $a, int $b) : int


Internally, PHP has four primitive types, so-called "scalar types":

  • boolean
  • integer
  • float (historically there was also double, but nowadays double should not be used and considered the same as float)
  • string

PHP in addition has four compound types:

  • array
  • object
  • callable
  • iterable

Special types:

  • NULL
  • resource

Pseudo-types:

  • void


Strict mode

Best practice is to write the following as the first line of every PHP script:

declare(strict_types=1)

TODO: And what does this mean?


boolean

The two boolean literals are TRUE and FALSE. They are case insensitive, so you could also write true or even TrUe.

If a non-boolean value is used in a boolean context, PHP automatically performs a conversion to boolean. Casts can also be used to explicitly convert a value into boolean:

$a = (bool)$b;
$a = (boolean)$b;

// Similar to casting
$a = boolval($b);

When conversion takes place, the following values are considered to be FALSE:

  • FALSE itself
  • integer: 0 (zero)
  • float: 0.0 (zero)
  • string: "0", and also the empty string
  • array: an empty array
  • NULL (including unset variables)
  • SimpleXML objects created from empty tags (TODO: What's this?)

Every other value is considered TRUE (including any resource and NAN).


integer

Integer literals can be written in various notations:

$a = 42;
$a = -42;
$a = 042;      // octal number
$a = 0x1A;     // hexadecimal number
$a = 0b101010; // binary number

Notes:

  • PHP does not support unsigned integers
  • The range of integers is platform-dependent, although 32 bit is pretty much the minimum everywhere. Constants to check are PHP_INT_SIZE, PHP_INT_MAX and PHP_INT_MIN (the latter since PHP 7).
  • If PHP encounters a number beyond the bounds of the integer type it will silently convert it to float
  • There is no integer division - dividing an integer always yields a float


If a non-integer value is used in a integer context, PHP automatically performs a conversion to integer. Casts can also be used to explicitly convert a value into integer:

$a = (int)$b;
$a = (integer)$b;

// Similar to casting. $base is optional. If omitted and the
// scalar value is a string, the string's format determines
// the base: "0x" prefix = hex, "0" prefix = octal.
$a = intval($b, $base);

Notes:

  • FALSE is 0, TRUE is 1
  • float values are rounded towards zero (i.e. truncated)
  • The result of the conversion is undefined if a float value is beyond the bounds of the integer type; no warning or notice is generated if this happens
  • PHP 7 and newer: NaN and Infinity are 0. Before PHP 7 these were undefined.


float

float literals can be written in various notations:

$a = 4.2;
$a = -4.2;
$a = 4.2e3;
$a = 4.2E-10;

Notes:

  • The range of floats is platform-dependent, although a maximum of ~1.8e308 with a precision of roughly 14 decimal digits is a common value.
  • The usual warnings about floating point precision apply
  • Worse, however, rational numbers that are exactly representable as floating point numbers in base 10 (e.g. 0.1 or 0.7) do NOT have an exact representation as floating point numbers in base 2, which is the format PHP uses internally. For this reason, the expression floor((0.1 + 0.7) * 10) unexpectedly has the result 7! Because of this mess, comparing floats without taking special measures should be AVOIDED!
  • Some numeric operations can have the result NaN (Not A Number). The constant NAN represents this. Comparing a NaN value to any other value (including the constant NAN) always results in FALSE (the only exception is: If compared to TRUE the result is TRUE). Because of this, the only reliable way to check for NaN is the function is_nan().


If a non-float value is used in a float context, PHP automatically performs a conversion to float. There don't seem to be any casts available for explicitly converting to float. Other notes:

  • For values other than string, conversion to float always happens by first converting the value to integer, then to float
  • FALSE is 0, TRUE is 1
  • float values are rounded towards zero (i.e. truncated)
  • The result of the conversion is undefined if a float value is beyond the bounds of the integer type; no warning or notice is generated if this happens
  • PHP 7 and newer: NaN and Infinity are 0. Before PHP 7 these were undefined.


If you want to perform an explicit conversion, use the floatval() function:

$a = floatval($b);

Notes:

  • Only scalar values should be converted. Converting an object generates an error and returns 1.
  • Empty arrays return 0, non-empty arrays return 1
  • Otherwise the general rules of float casting seem to apply


string

Literals

string literals can be written in various notations:

$a = 'I said "foo"';

$a = "I said 'foo'";
$a = "I said '$foo'";    // simple variable expansion
$a = "I said '${foo}'";  // simple variable expansion, but delimit the variable name
$a = "I said '{$foo + $bar}'";  // delimit a complex expression

$a = <<<"HEREDOC"
I said "foo"
HEREDOC;

$a = <<<'NOWDOC'
I said "foo"
NOWDOC;

Notes:

  • Single or double quotes can be escaped with a backslash character ("\"). A backslash character is escaped with another backslash character.
  • A number of escape sequences are recognized within double quotes, but not within single quotes (e.g. \n for newline)
  • Variable expansion occurs within double quotes, but not within single quotes
  • HEREDOC syntax behaves just as double quoted strings do, except that double quotes do not need to be escaped. The double quotes around the HEREDOC label can be omitted.
  • NOWDOC syntax behaves just as single quoted strings do, except that single quotes do not need to be escaped. The single quotes around the NOWDOC label are mandatory to make the distinction from HEREDOC syntax.
  • Variable expansion starts when the interpreter encounters a dollar character ("$"). The interpreter greedily consumes as many tokens as possible to form a valid variable name. The variable name can be explicitly delimited with curly braces ("{}").
  • To allow for complex expressions, curly braces can be put around an entire expression. For this to work the opening brace must be immediately followed by the dollar character: "{$".


Conversion to string

If a non-string value is used in a string context, PHP automatically performs a conversion to string. Casts can also be used to explicitly convert a value into string:

$a = (string)$b;

// Similar to casting
$a = strval($b);

Notes:

  • FALSE is an empty string, TRUE is "1"
  • When converting float values, the decimal point character is taken from the current locale
  • Arrays are converted to the literal string "Array"
  • Objects can only be converted if they have a __toString method
  • Resources are converted to a string similar to this: "Resource id #42". The number 42 in this example actually is the resource number assigned to the resource at runtime. The exact format should not be relied on, but the string is unique for each resource.
  • NULL is an empty string


Converting strings to numbers

Rules for converting a string to a number:

  • Numeric content is searched for at the beginning of the string
  • If nothing resembling a number is found the result of the conversion is integer 0. For instance, "foo 42" results in integer 0.
  • If a number is found it will be evaluated until non-numeric content is found. For instance "42 foo" results in integer 42.
  • The result is an integer if 1) the number found does not contain any of the characters ".", "e" or "E"; and 2) the number fits into the integer type limits
  • In all other cases the result is a float


Useful operations

Basic stuff:

$a = "hello" . " " . "world";  // string concatenation

$result = $a[1];       // e
$result = $a[-1];      // d => possible only since PHP 7.1

$a[1] = "x";           // hxllo world => TODO: What happens if more than 1 character is assigned?

$result = strlen($a);  // 11


Substrings:

$a = "hello world";

$result = substr($a, 7, 3);   // orl
$result = substr($a, 6);      // world
$result = substr($a, -1, 3);  // rld => possible only since PHP 7.1

$result = strstr($a, "or");     // orld => everything from first occurrence of specified substring, including the substring
$result = strstr($a, "OR");     // empty string => strstr() is case sensitive
$result = stristr($a, "OR");    // orld => stristr() is case insensitive
$result = strrchr($a, "l");     // ld => everything from last occurrence of specified single character, including the character


String position:

$a = "hello world";

$result = strpos($a, "l");      // 2 => position of first occurrence of specified substring (case sensitive), FALSE if not found
$result = strpos($a, "l", 4);   // 9 => specify start position for search
$result = strpos($a, "l", -2);  // 9 => negative start position is counted from end of string, search direction is forward
$result = strrpos($a, "l");     // 9 => position of last occurrence of specified substring (case sensitive), FALSE if not found
$result = strrpos($a, "l", -3); // 4 => specify start position for search; negative position is counted from end of string and search direction is backward

// use stripos() and strripos() for case insensitive search


String transformation:

$a = "hello world";

$result = strtolower($a);  // hello world
$result = strtoupper($a);  // HELLO WORLD

$result = str_replace("hi", "hello", $a);  // hi world
$result = strtr($a, "eo", "EO");           // hEllO wOrld

$result = ucfirst($a);  // Hello world
$result = ucwords($a);  // Hello World

$result = strrev($a);   // dlrow olleh


String splitting:

$a = "hello world";

$arr = explode(" ", $a);        // ["hello", "world"]
$result = implode(";", $arr);   // hello;world

$arr = str_split($a, "3");      // ["hel", "lo ", "wor", "ld"]
$arr = str_split($a, "42");     // ["hello world"]

$pattern = "/\s+/";
$result = preg_split($pattern, $a);     // ["hello", "world"]


Regex stuff:

$a = "hello world";

$pattern = "/\s+/";
$result = preg_match($pattern, $a);     // match = result 1, no match = result 0, error = result FALSE

$pattern = "/hello/i";
$replacement = "bye";
$result = preg_replace($pattern, $replacement, $a);  // "bye world"

// Back references are specified using \\n or ${n}. The latter should be used only if the
// back reference is followed immediately by a digit. \\0 refers to the entire string
// matched. A maximum of 99 back references are possible.
$pattern = "/(hello) (world)/";
$replacement = "\\2 \\1";
$result = preg_replace($pattern, $replacement, $a);  // "world hello"
$replacement = "${2} ${1} ${0}";
$result = preg_replace($pattern, $replacement, $a);  // "world hello hello world"

// All parameters can be arrays. Each pattern is replaced by its replacement string
// counterpart. Missing replacement strings are assumed to be empty strings. If an
// an array of patterns is specified but only a single replacement string (i.e. NOT
// an array with 1 element), all patterns are replaced with the same replacement
// string. From the specs it is unclear what happens if there are more replacements
// than patterns.
$strings = array("abc111", "111abc");
$patterns = array("/a/", "/1/");
$replacements = array("z", "9"); 
$maxNumberOfReplacementsPerString = 1;    // -1 means "no limit"
$totalNumberOfReplacements = -1;          // this is an out parameter, so the initial value doesn't matter
$result = preg_replace($patterns, $replacements, $strings, $maxNumberOfReplacementsPerString, $totalNumberOfReplacements);  // ["zbc911", "911zbc"]



String comparison:

$a = "hello";
$b = "world";

$result = strcmp($a, $b);      // equal = result 0, lexically smaller = result -1, lexically greater = result 1; comparison is case sensitive
$result = strcasecmp($a, $b);  // same but comparison is case insensitive


Characters:

$result = chr(65);   // A
$result = ord("A");  // 65


String encoding

PHP strings are sequences of characters. Every character is the same as a byte. PHP therefore only supports 256-character sets and has no native Unicode support.

PHP places no restriction on the value that any of the characters in a string can have, so strings that contain Null-Bytes are possible (although they may cause problems when they are passed to underlying system functions).

The encoding of the source file determines the byte value of characters in a literal string. Exception: If Zend Multibyte is enabled. TODO: What's this?


Also see Details of the String Type section in the PHP docs for strings.


Type checking

The following functions can be used to check whether a variable has a specific type:

  • is_int()
  • is_float()
  • is_string()
  • is_bool()

One other useful function is is_numeric(). This returns true for integers and floats, but also for strings that contain a number. Note that a string must be entirely numeric - if there is only one invalid character the string is not recognized as numeric.


null

A variable is considered to have the type null if:

  • It has not been set to any value yet
  • It has been unset()
  • It has been assigned the constant NULL

The constant NULL is the only possible value of type null. The constant NULL is case insensitive, so you could also write nUlL.

The function is_null can be used to check for null. TODO: Is this recommended? Or can we also compare to NULL?


Arrays

Unlike other programming languages, arrays in PHP are "ordered maps", i.e. data structures that associates values to keys. I think of them as dictionaries or hash maps.

Arrays can be created in various notations:

$a1 = array(
  "foo" => "17",
  "bar" => "42",
};

$a2 = [
  "foo" => "17",
  "bar" => "42",
];

$a3 = array("foo", "bar", "baz");

// Accessing values in an array
$b = $a1["foo"];
$b = $a3[0];

// Changing values in an array
$a1["foo"] = 1234;
$a3[] = "foobar";  // uses key with integer value 3

// Adding a value to the end of an array
$a3[] = 42;
array_push($a3, 42);  // equivalent to the above; better readable but worse performance

// Removing a value from an array
unset($a1["foo"]);

// Delete a whole array
unset($a1);

// Get array size
$numberOfElements = count($a1);

// Check if an array contains a key
if (array_key_exists("foo", $a1)) { ... }
// Check if an array contains a value. String values are compared case-sensitive.
// If strictness is set to true then === is used for comparison
$strict = true;
if (in_array(1234, $a1, $strict)) { ... }

// Return all keys in an array, or keys which have the specified value.
// The result is an indexed array.
$arrayWithKeys = array_keys($a1);
$arrayWithKeys = array_keys($a1, 1234, $strict);
// Return all values in an array. The result is an indexed array.
$arrayWithValues = array_values($a1);

// Array operators
$b = $a1 + $a2;          // union of two arrays; key/value pairs from first array take precedence
if ($a == $a2) { ... }   // TRUE if the two arrays have the same key/value pairs
if ($a === $a2) { ... }  // TRUE if the two arrays have the same key/value pairs in the same order and of the same types

// The array remembers the "highest last used integer key"
// even if that key is removed from the array
$a3[4] = 4321;
$a3[] = 1111;   // adds the value under key 5
unset($a3[5]);  // removes the value under key 5
$a3[] = 2222;   // adds the value under key 6 because the array remembers

// The two arrays must have the same number of elements
$combinedArray = array_combine($arrayWithKeys, $arrayWithValues);

// By default, when arrays are used as operands or parameters they are passed
// by value, not by reference. This means that a copy is made!
$b = $a;   // $b is assigned a copy of the array in $a
$b = &$a;  // $b is assigned a reference to the array in $a
foo($a);   // a copy of the array in $a is passed to function foo(), unless the function declarations
           // explicitly specifies that the parameter is to be passed by reference (ampersand prefix)

Notes:

  • The key can be an integer or string
  • The value can be of any type
  • Key values that are not integers and strings are first converted into integers according to integer conversion rules (e.g. for float keys the fractional part is truncated).
  • But even for string keys, PHP first attempts to convert the string to an integer. If it succeeds the value will be stored under the converted integer value. If it fails, e.g. because the string looks like a float value, then the string value is retained as key.
  • Arrays and objects cannot be used as keys
  • An array can have mixed integer and string type keys
  • The key can be omitted, PHP in that case uses the increment (+1) of the largest previously used integer key. If no integer key was previously used, PHP uses zero. The result is an array that resembles what you are probably used to from other programming languages.
  • Multi-dimensional arrays work as expected
  • Accessing an array with a key for which no value exists results in NULL.


Type juggling, type casting, type coercion

Type juggling is a generic term that the PHP documentation uses to describe all sorts of implicit and explicit type conversions.

The first thing that the PHP documentation describes in its "Type juggling" section is that variables in PHP are not declared with an explicit type and that a variable apparently can "change its type" in mid-flight: First it is an integer, then suddenly it becomes a float or string. In my opinion, and from what I have understood after reading the entire "Types" section of the PHP documentation, this has nothing to do with type conversion and should not be summed up under the term "type juggling". It is simply a language trait that variables do not have a type. Instead, variables are references to values, and it is the values that have a type!

Type casting is explicit "type juggling": This happens when you explicitly write code to convert a value from one type to another. Typically this is done using one of the various cast operators.

Type coercion is not mentioned under this name in the PHP documentation. Type coercion is implicit "type juggling": It happens when the PHP runtime automatically converts a value from one type to another. Type coercion typically occurs when an operator finds out at runtime that one or more of its operand values do not have a type that matches the operator's requirements. For instance:

  • The operator "*" performs numeric multiplication, so operands are converted into numbers. If that is not possible an error message is generated. If either operand is a float, then both operands are evaluated as floats, and the result will be a float. Otherwise, the operands will be interpreted as integers, and the result will also be an integer.
  • The operator "." performs string concatenation, so operands are converted into strings. This conversion always succeeds, so no error message is required.

Note: Type coercion does not change the types of the actual operand values themselves, instead it performs the type conversion on the fly and uses the temporary result as the actual operand value.


Operators

PHP has the usual operators:

  • Arithmetic operators: +, -, *, /
  • Comparison operators: ==, !=, >, >=, <, <=
  • Logical operators 1 (higher precedence): &&, ||, !
  • Logical operators 2 (lower precedence): and, or, xor
  • Conditional operator: (condition) ? value1 : value2
  • String operators: ., .=
  • Others: ++, +=, --, -=, % (modulo), ** (exponentiation), instanceof

Special cases:

  • === stands for "value and type are equal". Given that $x = 5;, then $x === "5" is false, while $x === 5 is true.
  •  !== stands for "neither value nor type are equal". Given that $x = 5;, then $x === 42 is false, while $x === 5 is true.
  • <=> (spaceship operator) results in an integer less than zero, greater than zero, or zero itself, depending on whether operand 1 is less than, greater than, or equal to operand 2. The logic behind this operator is the same as the logic for the strcmp() function.
  • `foo` executes the content inside the backticks as a shell command and returns the output. This operator may not be available, e.g. when "safe mode" is enabled.
  •  ?? (null coalescing operator) is new in PHP 7. Code example: echo $a ?? $b ?? 7; // outputs 16. This means: Use $a if it is set, otherwise use $b if it is set, otherwise use 7. There appears to be no limit to how many times ?? can be used in a row.

See the type comparison tables in the PHP documentation for an overview of how values of different types compare to each other.


Functions

Basic usage

function computeSum($term1, $term2, $term3 = 17)
{
  global $globalTerm;
  $sum = $term1 + $term2 + $term3 + $globalTerm;
  return $sum;
}

$globalTerm = 42;
$sum = computeSum(3, 4);

Notes:

  • Function names are case insensitive (I'm still choking on this one - I mean, variables are case sensitive but functions are not? Oh dear...)
  • PHP does not support function overloading
  • The caller must supply values for all mandatory parameters. If less values are specified the interpreter treats this as an error.
  • The caller can supply more values than specified by the function declaration. These additional values are discarded, unless the function runs functions like func_get_args (see section further down for details).
  • Functions cannot be undefined or redefined
  • The docs state that Recursive function/method calls with over 100-200 recursion levels can smash the stack.
  • Default values must be constant expressions, i.e. you cannot specify another variable or a function call
  • By default parameters are passed by value, but they can be passed by reference by prefixing the variable name with an ampersand character ("&). Pass-by-reference parameters can have a default value. TODO: What happens if the caller specifies a constant expression or a constant itself as the pass-by-reference parameter?
  • Unsurprisingly for "flexible" PHP, the return statement can be omitted. If that is the case the function returns the default value NULL.
  • Variables declared within the function have local scope, except if they use the keywoard global. For more details see the Variables & scope] section further up on this page.


Function definition scope

Functions can be defined in any scope, not just in the global scope. For instance, functions can be defined within an if/for/while loop or even within other functions.

if ($foo)
{
  function bar() : void
  {
    // do something
  }
}

Even though in the source code a function definition can appear in a sub-scope, the PHP interpreter places the definitions into the global scope. This has the following implications:

  • Function names must be globally unique. TODO: What happens if two functions with the same name are defined in different scopes? Is it important whether program flow reaches these definitions?
  • A function can be invoked from outside the scope in which it was defined.


In the source code the definition of a globally defined function can occur after its call. If a function is defined within a sub-scope, however, the program flow must have reached the function definition for the function to become available outside the definition scope.


TODO

  • What happens if I call a function that has not been defined yet?


Type declarations

Parameter names in a function definition can be prefixed with a type declaration (before PHP 7 this was known as "type hint"). The type of the function's return value can also be specified with a type declaration. Examples:

function computeSum(int $term1, int $term2) : int
function findFooByBar(Bar $bar) : ?Foo
function doSomethingComplex(int $foo, ?Bar &$bar) : ?Foo

Notes:

  • Specifying type declarations is optional
  • Specifying the character "?" in front of the return type or a parameter type makes that type into a so-called "nullable type", i.e. instead of an actual value of that type the parameter or return value is also allowed to become null.
  • Nullable types can be very important for pass-by-reference parameters (as shown in the third example) because this is the only way how a pure "out" parameter can be reasonably simulated in PHP. If the function declaration omits the "?" character but specifies a type for a pass-by-reference parameter, then the caller is forced to supply a valid value of the specified type - but that's an "in/out" parameter, not a pure "out" parameter. With a nullable type the caller must still initialize with null the variable it passes in place of the pass-by-reference parameter, but at least the caller is freed from the duty of generating an actual value.


Valid types that can be declared:

  • bool (not boolean)
  • int (not integer)
  • float
  • string
  • array
  • callable
  • iterable
  • self (can only be used on class and instance methods; the parameter value must be an instance of the same class as the one the method is defined on)

Any other name is interpreted to be the name of a class or interface, and the parameter value must be an instance of the named class or interface.


Non-scalar types: The interpreter throws an exception if a parameter or return values does not match the type declaration.

Scalar types: By default, type coercion occurs for parameter or return values that do not match the type declaration. This can be prevented by specifying the following line at the top of the PHP source code file:

declare(strict_types=1);

Notes:

  • The strict_types declaration can only be made in PHP 7 and newer
  • When type float is specified, it is still possible to supply integer values
  • Strict typing applies only to function calls made in the source code file in which the strict_types declaration appears. This is because the strict_types declaration is seen as a preference for the code that appears in the file


Variable-length parameter lists 1

The usual ... token is used to specify variable-length parameter lists.

function sum(int ...$numbers)
{
  $total = 0;
  foreach ($numbers as $number)
    $total += $number;
  return $total;
}

echo sum(17, 42);  // prints 59

Notes:

  • The parameter values will be "packed" into and passed to the function parameter as an array
  • An (optional) type declaration forces all parameter values to have the same type.


Variable-length parameter lists 2

PHP allows the caller to supply any number of values to a function. Usually superfluous values are discarded, however the function can use certain functions to evaluate the supplied values.

Thus, a function that is declared with no parameters can be used in a similar fashion as with the ... token:

function sum()
{
  $numberOfSuppliedValues = func_num_args();

  for ($indexOfValue = 0; $indexOfValue < $numberOfSuppliedValues; ++$indexOfValue)
  {
    $value = func_get_arg($indexOfValue);
  }

  $allValues = func_get_args();
  for ($indexOfValue = 0; $indexOfValue < $numberOfSuppliedValues; ++$indexOfValue)
  {
    $value = $allValues[$indexOfValue];
  }
}


Unpacking arrays and Traversable

When calling a function, the ... token (or operator, or whatever) can be used to "unpack" an array or Traversable into the parameters required by the function.

function sum($term1, $term2) { ... }

$a = [1, 2];
echo sum(...$a);      // prints 3
echo sum(42, ...$a);  // prints 43

Notes:

  • Only those parameters that have not been explicitly specified are supplied with values from the array or Traversable


Variable functions

If you append "()" to a variable, PHP interprets this as a function call. It extracts the variable's current value and uses it as the name of the function to be called.

function foo() { ... }

$bar = "foo";
$bar();  // invokes foo()


Anonymous functions & Closures

Basic usage:

function invokeCallback($callback)
{
  callback("bar");
}

// Here we pass a reference to an anonymous function that
// we declare "on the fly" as a parameter into another
// function.
invokeCallback(function($echoVariable) {
  echo "echoVariable = $echoVariable";
});


// Here we store a reference to an anonymous function in a
// variable for later use
$foo = function($echoVariable) {
  echo "echoVariable = $echoVariable";
};


// Here we call the anonymous function directly
$foo("bar");
// And here we pass it as a parameter
invokeCallback($foo)


Anonymous functions are actually closures (implemented by the Closure class, FWIW) and can "inherit" values from the surrounding scope. This does not happen automatically, the closure must specify the variables from which it wants to inherit a value with the keyword use.

$message = "one";

$foo = function() {
  echo $message;
};
$foo();  // prints NULL and an error because of undefined variable $message

$foo = function() use($message) {
  echo $message;
};
$foo();  // prints "one"

$message = "two";
$foo();  // still prints "one"; the closure obtained the value when it was defined

$foo = function() use($message) {
  echo $message;
};
$foo();  // prints "two"


Flow control

Preliminary notes:

  • The break statement accepts a numeric parameter that specifies how many nested structures are to be broken out of. The default is 1.
  • The continue statement also accepts a numeric parameter with a similar meaning.
  • Unlike any other language I have encountered before, the continue statement has meaning when used inside a switch structure. The meaning is the same as break. So if you have a switch inside a loop, and you want to continue with the next loop iteration from somewhere within the switch, then you have to use continue 2; !
  • If the current script file was included or required, then the return statement returns control to the calling script file. If the file was included, then the include statement returns the value specified by the return statement.


if:

if (condition)
{
  // do something
}
else if (condition)
{
  // do something
}
else
{
  // do something
}


switch:

var foo = "bar";
switch(foo)
{
case "abc":  // fall-through
case "def":
  // do something
  break;
case "bar":
  // do something
  break;
default:
  // do something
  break;
}


for loop:

for ($index = 0, $found = false; $index < $numberOfItems || $found ; ++$index)
{
  if (condition)
    // do something
  else if (condition)
    continue;
  else
    break;
}

foreach ($anArray as $value)
{
  // do something
}
foreach ($anArray as $key => $value)
{
  // do something
}


while:

while (condition)
{
  // do something
}

do
{
  // do something
}
while (condition)


include/require

General syntax for including other files in the current file:

// require is the same as include, but upon failure will generate an error and
// halt script execution. include only emits a warning.
include "foo.inc.php";
require "foo.inc.php";
$fooResult = include "foo.inc.php";  // not possible for require

// The *_once variants make sure that the file is not included a second time,
// thus preventing function redefinitions etc. This is important in more
// complex programs with many reusable components where transitive includes
// are easily possible.
include_once "foo.inc.php";
require_once "foo.inc.php";


Alas, PHP monumentally messes up when it comes to handling relative paths. Let's first examine the basic include/require search rules:

  1. If the specified path is absolute, PHP will look for the file under the specified path. Under Windows, a drive letter constitutes an absolute path.
  2. If the specified path begins with "." or "..", PHP will look for the file relative to the current working directory.
  3. If neither of the above is the case, PHP will first search in all directories that are listed in include_path, then it will search in the calling script's directory, and finally it will search in the current working directory.

Important additions to the above rules:

  • include_path by default consists of just .:.
  • A path in include_path that begins with . or .. is relative to the current working directory.


The issue I have is that paths beginning with . or .. are relative to the current working directory. The current working directory is an utterly unreliable reference point! Everybody can call chdir() to change the current working directory, so it is entirely possible that include/require file lookups might suddenly fail in mid-execution. Your code is basically at the mercy of any library you use to not perform chdir(). Personally I even believe that this opens up PHP for an incalculable security risk: If someone manages to insert a chdir() into code execution, they potentially have the ability to get PHP to invoke any code located in an inconspicuous place on the local server.

Security risks aside, the unreliable current working directory reference point makes it impossible to have a setup like this:

main.php      ---> include "foo/foo.php";
foo/foo.php   ---> include "../bar/bar.php";
bar/bar.php

The problem is the relative reference to bar.php in foo.php. Because of include rule #2 from above, this reference will never work unless the current working directory just happens to be foo. Include rule #2 effectively puts a PHP script at the mercy of which current working directory is chosen by the execution environment when it starts running the PHP interpreter. This can be a PITA if you use different environments during developments (e.g. command line vs. an IDE such as PhpStorm) and/or if you want to unit-test a class or function in isolation, i.e. outside of the normal runtime environment where the entry point script starts with a fixed current working directory.


The workaround for all this mess (which I found in this SO answer) looks like this:

include(dirname(__FILE__) . "/../bar/bar.php");

The trick is that the __FILE__ constant is the absolute path of the file that contains the include or require statement. The include statement thus effectively uses an absolute path which makes it independent of the current working directory.


Object orientation

Class declarations

// Foo is abstract and cannot be instantiated
abstract class Foo
{
  // Public = Visible from everywhere
  public $aPublicProperty = "public default value";
  // Protected = Visible from the class itself and all subclasses
  protected $aProtectedProperty = "protected default value";
  // Private = Visible only from the class itself
  private $aPrivateProperty = "private " . "default " . "value";
  // Property declarations MUST include a visibility. The following
  // is illegal!
  $noVisibilityProperty = "foo";

  // Visibility for static properties is optional. Default is public.
  private static $aStaticProperty = "foo";
  static $anotherStaticProperty = "foo";

  // Constants are similar to static member variables.
  // Visibility for constant declarations is optional. Default is public.
  public const A_PUBLIC_CONSTANT = "foo";
  const ANOTHER_PUBLIC_CONSTANT = "foo";

  public function aPublicMethod()
  {
    // No dollar character after -> for non-static properties
    echo $this->aPublicProperty;
    // Dollar character after :: for static properties
    echo self::$aStaticProperty;
    // No dollar character for constants
    echo self::A_PUBLIC_CONSTANT;

    self::aStaticMethod();
  }

  // Visibility on method declarations can be omitted, it defaults to public
  function anotherPublicMethod() { ... };

  // See testOverride()
  private function aPrivateMethod()
  {
    echo "Foo::aPrivateMethod";
  }

  public function aStaticMethod() { ... };

  // Member variables and functions can have the same name.
  // Whether one or the other is used depends on the context.
  public $abc = "def";
  public function abc() { ... }

  public overridableMethod() { ... }
  public final finalMethod() { ... }

  public testOverride()
  {
    // Here we invoke the override from subclass Bar.
    $this->overridableMethod();

    // Although subclass Bar has re-declared "aPrivateMethod", here we
    // still invoke our own private method. Private methods cannot be
    // overridden.
    $this->aPrivateMethod();
  }

  // Constructors and destructors have special names, but are otherwise
  // almost like normal methods. Notes:
  // - A class can have only one constructor
  // - Constructors can have parameters just like any other method
  // - The constructor is called whenever a new instance of the class is created
  // - Destructors cannot have parameters
  // - The destructor is called as soon as there are no more references to an object,
  //   or during the interpreter's shutdown sequence (order is undefined).
  public function __construct($mandatoryParameter) { ... }
  public function __destruct() { ... }

  // Even if the class hadn't already been declared abstract, this abstract
  // method would prevent instantiation. Abstract methods cannot have an
  // implementation.
  abstract protected function anAbstractMethod($mandatoryParameter);
}

// Class "Bar" is final, i.e. it is not possible to declare any subclasses
final class Bar extends Foo
{
  // Re-declare a method with the same name and signature to override it.
  // Only public and protected methods can be overridden.
  public overridableMethod()
  {
    echo $this->aPublicProperty;
    echo parent::$aStaticProperty;
    // SURPRISE: For constants we use "self", not "parent"!
    echo self::A_PUBLIC_CONSTANT;

    // invoke the superclass implementation
    return parent::overridableMethod();
  }

  // This is NOT an override! Because we re-declarate a private method,
  // both subclass and superclass retain their own implementations.
  private function aPrivateMethod()
  {
    echo "Foo::aPrivateMethod";
  }

  // Superclass constructors and destructors are NOT called implicitly.
  // Constructor can have additional parameters as long as they are optional.
  public function __construct($mandatoryParameter, $optionalParameter = "foo")
  {
    parent::__construct();
  }
  public function __destruct()
  {
    parent::__destruct();
  }

  // Implementation of an abstract method declared in the superclass.
  // - Basically works just like overriding, i.e. name and signature must be the same
  // - The "abstract" keyword must not be present, of course
  // - Visibility can be less restricted. Here we use public, superclass used protected
  // - We can have additional parameters as long as they are optional
  public function anAbstractMethod($mandatoryParameter, $optionalParameter = "foo") { ... }
}

Notes:

  • PHP calls member variables "properties" - it has no separate concept of properties such as in C#
  • PHP does not support multiple inheritance (good)
  • PHP does not support overloading (bad). Also remember that function names are case insensitive - the same applies to methods in an OO context.
  • Default values of member variables must be either constant values, or operations with constant value operands
  • As usual, different objects that have the same type can access each other's protected and private members
  • Properties cannot be declared final
  • TODO: Is it possible to have a class hierarchy like AbstractBase : AbstractIntermediate : FinalClass, where AbstractBase has an abstract method, but AbstractIntermediate does not implement that method, only FinalClass does? The docs say that all methods marked abstract in the parent's class declaration must be defined by the child. This is againt all conventions I have seen in other languages.


Class instantiation

A class is instantiated using the keyword

new

These examples use the class declarations from the previous section.

// Illegal! Foo is abstract.
$instance = new Foo("baz");

// We omit the optional second constructor parameter
$instance = new Bar("baz");
// We specify the optional second constructor parameter
$instance = new Bar("baz1", "baz2");

// Of course, this works. too
$className = "Bar";
$instance = new $className();


The type "object" and the default class "stdClass"

When a class is instantiated, the instance has the value type object. Other values can be coerced to object using a cast. The resulting object is an instance of the default class stdClass.

Examples:

// Converting a scalar value creates an instance of built-in class "stdClass".
// The value is then available via the property "scalar".
$a = 42;
$o = (object)$a;
$b = $o->scalar;

// Converting an array creates an object whose properties correspond to the
// keys in the array.
$a = array(
  "foo" => "17",
  "bar" => "42",
};
$o = (object)$a;
$b = $o->foo;  // $b is now 17

// In PHP versions before 7.2.0, properties generated from integer keys were
// inaccessible except when the object's properties were iterated. From PHP
// 7.2.0 onwards, integer properties can be accessed with the following syntax.
$a = array("17" => "foo", "42" => "bar");
$o = (object)$a;
$b = $obj->{"17"}  // $b is now "foo"

// Converting the value NULL results in an empty instance of stdClass, i.e.
// one that has no properties. This is the same as explicitly creating an
// instance of stdClass with new.
$a = NULL;
$o = (object)$a;      // $o has no properties
$o = new stdClass();  // ditto


Interfaces

Interfaces are declared similar to classes. Some notes:

  • The keyword interface is used instead of class
  • Methods must not have a body
  • All methods must be public (if the visibility keyword is omitted, the default visibility "public" kicks in)
  • Interfaces can declare a constructor!
  • Interfaces cannot declare properties
  • Interfaces can declare constants
  • Interfaces can extend another interface
  • A class uses the keyword implement to implement (duh!) an interface
  • A class can implement several interfaces by separating their names with a comma character (",")
  • It is possible for a class to not implement all methods of an interface. In that case the class must be declared abstract.
interface Foo
{
}

interface Bar extends Foo
{
}

interface Baz
{
}

class FooBar implements Bar, Baz


Anonymous classes

Since PHP 7.0, instead of writing a formal class definition it's also possible to create an instance of an anonymous class. This is especially useful if you

function createInstanceOfAnonymousClass(string $aString)
{
  return new class($aString) {

    public function __construct(string $aString)
    {
      $this->aString = $aString;
    }

    public function printString()
    {
      echo $this->aString . "\n";
    }
  };
}

$oFoo = createInstanceOfAnonymousClass("foo");
$oBar = createInstanceOfAnonymousClass("bar");

$oFoo->printString();  // prints "foo"
$oBar->printString();  // prints "bar"

// Prints something like "class@anonymousCommand line code0x1094645c9"
echo get_class($oFoo) . "\n";

// Prints 1 (i.e. true)
echo (get_class($oFoo) === get_class($oBar)) . "\n";

Notes:

  • Not shown in the example is that an anoynmous class can extend a base class or implement interfaces, just like a normal class
  • As shown in the example, the PHP engine internally assigns a name to the anonymous class, but the exact nomenclature is an implementation detail which should not be relied upon. What can be relied upon is that two instance of the same anonymous class have the same class.


Cloning

The keyword

clone

causes an object to be cloned. In a first step, this creates a shallow copy of the object. In a second, optional step, the clone operation invokes the special function

__clone()

if it has been defined in the class that the cloned object is an instance of. The __clone function thus gets the opportunity to perform additional tasks after the shallow copy was created.

Notes:

  • No constructor is called during cloning


String representation of objects

A class can implement the special function

__toString() : string

This function will then be called whenever the object is treated as a string. A typical example is if you echo the object.

Notes:

  • __toString must not throw an exception, otherwise a fatal error occurs


Magic functions

PHP reserves all functions whose names begin with two underscores ("__") as magical. You should not define functions named like that unless it is one of the already recognized magic functions.

The previous sections have already shown some magic functions, such as __construct or __toString. There is a full list of these functions in the PHP docs, here are some of the more interesting ones:

  • __invoke is invoked when code attempts to "call" the object like a function
  • __debugInfo is invoked by the var_dump function to find out which properties of the object should be dumped. The return value must be an array with the actual key/value pairs to dump. If the magic function does not exist, var_dump dumps all properties, regardless of their visibility.


Special stuff

Use the special class constant to get the fully-qualified name (i.e. including namespaces) of a class:

namespace Foo
{
  class Bar { ... }
}

$className = Foo::Bar::class;  // result is "Foo\Bar"


Namespaces

Declaring namespaces

A namespace is declared using the keyword

namespace


A file can contain 0-n namespaces. If a file contains at least one namespace, the namespace keyword must appear either as the very first statement in a file, or after a declare statement if one exists. No non-PHP code may appear before the namespace declaration!


Namespaces can be declared in one of two ways:

  • Without braces. In this case the namespace extends to either the beginning of the next namespace, or the end of the file.
  • With braces. In this case the namespace extends to the closing brace.


Example:

---------- a.php ----------
namespace Foo;

class FooClass { ... }

namespace Bar;

class BarClass { ... }


---------- b.php ----------
namespace Foo
{
  class FooClass { ... }
}

namespace Bar
{
  class BarClass { ... }
}


Nested namespaces (or "sub-namespaces", as PHP calls them) are declared with the backslash ('\') character as the separator. A namespace without name places the content into the global namespace.

namespace Foo\Bar\Baz
{
  [...]
}

namespace
{
  [...]
}


Using namespaces

class AClass { ... }

class BClass { ... }
function bFunction { ... };
const B_CONST = ... ;

namespace Foo
{
  class AClass ( ... }
}

namespace Bar\Baz
{
  class AClass ( ... }
}

namespace Bar
{
  class AClass ( ... }

  $obj = new AClass();       // resolves to \Bar\AClass
  $obj = new Baz\AClass();   // resolves to \Bar\Baz\AClass
  $obj = new \AClass();      // resolves to global \AClass
  $obj = new \Foo\AClass();  // resolves to \Foo\AClass

  // Unqualified classes MUST be in the current namespace.
  // Unqualified functions and constants are looked up in the global namespace.
  $obj = new \BClass();      // resolves to global \BClass
  $obj = new BClass();       // ERROR! Resolver does NOT go looking for classes in global namespace
  bFunction();               // resolves to global \bFunction
  $var = B_CONST;            // resolves to global \bFunction

  // Namespaces can be used dynamically, too
  $className = "AClass";
  $obj = new $className();   // resolves to \Bar\AClass
  $className = "\\Foo\\AClass";
  $obj = new $className();   // resolves to \Foo\AClass
  $className = '\Foo\AClass';
  $obj = new $className();   // resolves to \Foo\AClass

  // The __NAMESPACE__ constant resolves to the current fully qualified namespace name.
  // In a global context, the constant resolves to an empty string.
  $className = __NAMESPACE__ . '\AClass';
  $obj = new $className();   // resolves to \Bar\AClass

  // The namespace keyword resolves to the current namespace
  $obj = new namespace\AClass();  // resolves to \Bar\AClass
}


Importing/aliasing

A namespace, class, interface, function or constant can be imported or aliased with the keyword

use

Note that use must not appear within a function or other scope.


Examples:

// Importing/aliasing an entire namespace. All members of this namespace can now
// be used without using the namespace name to qualify them.
use \Baz;

// AClass, AnInterface, aFunction and A_CONST can now be used without specifying
// the namespace
use \Foo\Bar\AClass;
use \Foo\Bar\AnInterface;
use function \Foo\Bar\aFunction;
use const \Foo\Bar\A_CONST

// AClass is now known as BClass
// aFunction is now known as bFunction
// Constants cannot be renamed
use \Foo\Bar\AClass as BClass;
use function \Foo\Bar\aFunction as bFunction;

// Multiple use statements are combined with the comma (',') as separator
use NamespaceOne, NamespaceTwo;

// Multiple use statements from the same namespace
use \Foo\Bar{AClass, AnInterface};

// Importing/aliasing only works at compile time
$className = "AClass";
$obj = new $className();  // error!

class AClass { ... }
$obj = new $className();  // works


Exception handling

class FooException extends Exception ( ... }
class BarException extends Exception ( ... }

try
{
  throw new FooException("message");
}
catch (MyException $exception)
{
}
catch (YourException $exception)
{
}
catch (FooException | BarException $exception)
{
  // A common handler for two unrelated exception types
}
catch(Exception $exception)
{
  echo "Caught exception: " . $exception->getMessage();
  throw $exception;  // re-throw
}
catch(Error $error)
{
  echo "Caught fatal error exception: " . $error->getMessage();
  throw $exception;  // re-throw
}
catch(Throwable $throwable)
{
  // Throwable is an interface, and we can catch interface types, too"
}
finally
{
  // do something
}

Notes:

  • The base class of the normal exception type hierarchy is Exception
  • The base class of fatal error exceptions thrown by the interpreter is Error
  • Exception and Error both implement the interface Throwable but are otherwise unrelated
  • User-defined exceptions are prohibited from implementing Throwable - they must extend either Exception or Error
  • Exceptions that extend Error should be used only for grave and generally unrecoverable coding errors
  • The order in which catch() clauses appear is important: More specific exception types must appear in the beginning, less specific exception types at the end
  • A global exception handler can be set with the function set_exception_handler()

The SPL provides a useful set of standard exceptions. Check out the list.

The most common error types are

  • TypeError: Is thrown when a function argument or return value does not match a type declaration.
  • ParseError: Is thrown when an included/required file or eval()'d code contains a syntax error.
  • ArithmeticError
    • DivisionByZeroError
  • AssertionError


Advanced concepts

iterable

The iterable pseudo-type was introduced in PHP 7.1. It can be used as a parameter type or return type in a function declaration. Typed function declarations are available since PHP 7.

The following things are iterable:

  • Arrays
  • Objects that implement the Traversable marker interface
  • Generators that use yield can declare their return type as iterable


An iterable can be looped over using foreach.


Generators

A function that returns a sequence of values could do so by packaging all values into an array and returning the array. If the array has many elements such a function could consume a large amount of memory to produce the array, although the caller may need the values only one by one. Also the caller might need only the array's first 10 values.

As a memory-efficient solution, PHP provides so-called "generator" functions that "generate" values only on demand. An example might look like this:

// Specifying types is optional, but we do so here to show that
// the return type is "iterable"
function fibonacciGenerator(int $maxNumbers) : iterable
{
  if ($maxNumbers <= 0)
    throw new Exception("At least 1 number must be requested");

  $numbersGenerated = 0;
  $last = 0;
  $current = 1;

  // First number is a special case
  $numbersGenerated++;
  yield 1;

  while ($numbersGenerated < $maxNumbers)
  {
    $temp = $current;
    $current += $last;
    $last = $temp;

    $numbersGenerated++;
    yield $current;
  }
}

// Generates 42 Fibonacci numbers
foreach (fibonacciGenerator(42) as $fibonacciNumber)
{
  // do something
}

Notes:

  • PHP recognizes a function as a generator when the function uses the yield keyword at least once to return a value
  • A generator function can have multiple yield statements
  • A generator function provides only forward-going iterators - once the iteration has started it must follow through to the end (or the generator needs to be "rebuilt" by calling the generator function again)


When a generator function is called, an object of the internal Generator class is returned. This object implements the Iterator interface and provides methods that can be called to manipulate the state of the generator, including sending values to and returning values from it. The following example shows how a manual iteration can be achieved, using the same Fibonacci number generator from the previous example.

// When we invoke the generator function the Generator object is created
// and stored in our variable, but the function itself is not yet invoked.
$generator = fibonacciGenerator(42);

// This executes the generator function until the first yield statement
// is encountered
$generator->next();

// Here we fetch the current value of the generator function, in this
// case the value that the first yield statement returned
echo $generator->current();  // result is 1

// Executes the generator function until the second yield statement is
// encountered, then fetches the value of that yield statement
$generator->next();
echo $generator->current();  // result is 1

// Etc.
[...]


Finally, a generator can yield key/value pairs instead of just single values. Example:

function foo()
{
  [...]
  yield $key => $value;
  [...]
}

$generator = foo();
$generator->next();

$key = $generator->key();
$value = $generator->current();


Object serialization

Serialization of an object results in a string representation of that object which includes

  • The class name
  • The data of the object, i.e. the values of all properties


An object is serialized and unserialized with the global functions serialize and unserialize. Example:

---------- a.php ----------
require_once "Foo.inc.php"

$obj = new Foo()
$objAsString = serialize($obj);

// Now do something with the serialized object, e.g. store
// it in a file, or in a database

---------- b.php ----------
require_once "Foo.inc.php"

// Get the serialized content from somewhere, e.g. a file
// or a database
$objAsString = ...

$obj = deserialize($objAsString);

// The object is now fully functional
$obj->doSomething();

Notes:

  • Static properties are not serialized
  • Some sort of include is necessary before the deserialize function is invoked so that the function knows the definition of the class it is supposed to deserialize.
  • A class can hook into the serialization/desearialization process by implementing the magic functions __sleep and __wakeup. For instance, the class could make sure that only some of its properties are serialized.


Traits

TODO write this section. Traits are about code reuse.

Date/time

PHP core offers extensive support for dealing with dates and time. A large number of functions and classe are available, which it is impossible to cover here. Refer to the docs for the full details.

Date/time string representations play an important part because apparently it is impossible to create a DateTime object with a simple time_t value. Again, the docs have the details about the acceptable formats.


A few examples just for starters:

$secondsSinceTheEpoch = time();                                 // a time_t values
$then = mktime(hour, minute, second, month, day, year, isDST);  // another time_t value

$nowInAssociativeArray = getdate();        // keys correspond to the usual tm struct: seconds, minutes, hours, mday,
$thenInAssociativeArray = getdate($then);  // wday, mon, year, yday, weekday, month; key "0" holds the time_t value

$now = strtotime($"now");                      // strtotime parses English textual datetime descriptions and returns a time_t value
$then = strtotime("2018-04-21 17:30:15 CET");  // using "/" instead of "-" in the date reverses the order of month and day
$then = strtotime("2018-04-21");               // omitting the time and timezone defaults to 00:00:00 and GMT
$then = strtotime("17:30:15 GMT+1");           // omitting the date defaults to today
$then = strtotime("+17 hours", time());        // a large number of expressions are possible to specify a relative date/time

$formattedDate = date("d.m.Y H:i:s", time());  // example output: 01.01.2018 14:01:01 (i.e. prefixed with 0, 4-digit year, 24 hour format)
$formattedDate = date("d.m.Y H:i:s");          // defaults to time() if time_t value is omitted

$now = new DateTime();
$then = new DateTime($dateTimeString);  // see above for examples; there may be slight differences between strtotime() and DateTime constructor
$then = new DateTime("@1524330589");    // construct a DateTime from a time_t value

$timeTValue = $then->getTimestamp();    // get the time_t value that best matches the DateTime
$formattedDate = $then->format("d.m.Y H:i:s");


Runtime environments

Command line

This runs an inline PHP script on the command line:

php -r 'echo "hello world\r\n";'

This runs a PHP script stored in a file on the command line:

php foo.php

It's also possible to set the executable bit on a script file and use an internal shebang:

#!/usr/bin/php
<?php
  echo "hello world\r\n";
?>


Built-in web server

PHP has a built-in web server. The following serves files from the current working directory:

php -S localhost:8000

The following serves files from the specified document root:

php -S localhost:8000 -t /path/to/docroot-folder


Interactive mode

This runs PHP in interactive mode where you can enter commands and PHP immediately executes them:

php -a


Web server integration

TODO: Write about how to integrate PHP into a web server such as Apache.


php.ini

TODO: Write about the configuration file php.ini. Some of the more important settings, do the different runtime environments use the file differently?


Diagnostics

Override php.ini values

If you don't want to modify php.ini (e.g. because you are on a production system), or cannot because you are not the sysadmin, your code can override the values defined in php.ini with

ini_set("option-name", value);

To read the values of options in php.ini, use

ini_get("option-name")

The function returns a string value. "On" values are returned as "1", "Off" values are returned either as empty string or "0" (the docs are not clear about this). The function returns false for options that are not set.


error_reporting

The option error_reporting in php.ini contains flags that indicate which types of error conditions should be reported. Note that this does not define where this error reporting takes place - see the next section for defining the output options.

The following flags exist:

  • E_ERROR (1)
  • E_WARNING (2)
  • E_NOTICE (8)
  • E_DEPRECATED (8192)
  • E_STRICT (2048)

The special E_ALL flag is a combination of all existing flags.

Short reminder how to combine the various flags with bitwise operators:

E_ERROR | E_WARNING     >>> both errors and warnings
E_ALL & ~E_WARNING      >>> everything except warnings


display_errors, log_errors and error_log

The option display_errors can have the values "On" or "Off". If on, any errors enabled by the option error_reporting are printed to the PHP output.


The option log_errors can have the values "On" or "Off". If on, any errors enabled by the option error_reporting are written to a log file. The option error_log defines the path and name of the log file.


Code

// The gettype() function prints a human-readable representation of a type
$aBool = TRUE;
echo gettype($a_bool); // prints out:  boolean

// The var_dump() function prints a human-readable representation of an expression's type and value
TODO add example


Log to a file

$fileName = "/tmp/foo.txt";
$fileHandle = fopen($fileName, 'w');  // "w" creates/truncates, "a" creates/appends
$aVariable = [...]
fwrite($fileHandle, $aVariable);
fwrite($fileHandle, "\n");
fclose($fileHandle);

// file_put_contents() performs fopen(), fwrite() and fclose() all in one
$fileName = "/tmp/foo.txt";
file_put_contents($fileName, $aVariable);
file_put_contents($fileName, $aVariable, FILE_APPEND);


Recipes

Comparing float values for equality

When comparing float values for equality one must always take rounding errors into account. The following example shows how you define an upper bound on the relative error, known as the "machine epsilon", or "unit roundoff". It represents the smallest acceptable difference in calculations.

$a = 1.23456789;
$b = 1.23456780;
// Test for 5 digits of precision
$epsilon = 0.00001;

if (abs($a - $b) < $epsilon)
{
  echo "a and b appear to be equal (within the bounds of epsilon)";
}


Accessing the query string

To recap, this is how an URL with a query string looks like:

http://www.foo.com/bar.php?firstname=john&lastname=smith

Probably the easiest way to access the key/value pairs is the $_GET superglobal:

$firstname = $_GET["firstname"];
$lastname = $_GET["lastname"];

Notes:

  • Variables and values in $_GET are already URL decoded


A more do-it-yourself method is to use a combination of the parse_url() and the parse_str() functions:

$url = ...
$queryString = parse_url($url, PHP_URL_QUERY);

// This can be a more direct alternative to get the query string
$queryString = $_SERVER["QUERY_STRING"];


$queryStringArray = array();
parse_str($queryString, $queryStringArray);

Notes:

  • The parse_url() function returns the entire query string without breaking it into its component. Also the function does not URL decode the query string.
  • The parse_str() function expects an URL encoded string and an array variable that is passed by reference. It fills URL decoded key/value pairs into the array. If the query string contains the same key multiple times, the array will contain the value for the last key in the query string (I'm not sure if such a query string is valid, though). Space characters are replaced by underscore characters ("_").
  • Important: Query string data is external data that could be maliciously crafted. Always treat query strings with suspicion. For instance, never use the function parse_str() without a second parameter - the function in that case creates variables from the query string, which could be used to inject stuff into your code, similar to SQL injection.


Processing POST form input

Values from form input controls that have been submitted with POST are available via the superglobal $_POST array. For instance:

echo "Hello " . $_POST["firstname"] . " " . $_POST["lastname"];

Notes:

  • $_POST cannot be used within double quotes
  • Period (".") and space characters in variable names are replaced by underscore characters ("_")


Protection against injection

User input that will become part of the output of the program should be pre-processed by one of these functions:

  • htmlspecialchars()
  • htmlentities()


Similarly, user input that is used to form database queries should be pre-processed by the function

  • mysqli_real_escape_string()


Uploading files

The following simple form prompts the user to select a file from their local hard disk and to submit it for upload.

<form enctype="multipart/form-data" action="upload.php" method="post">
  <!--
    MAX_FILE_SIZE must precede the file input field. The value is
    measured in bytes. The client can circumvent this restriction
    quite easily, so a server-side check is always necessary.
    There is also a PHP configuration setting that limits the size
    of uploads.
  -->
  <input type="hidden" name="MAX_FILE_SIZE" value="30000" />

  <p>File: <input name="upfile" type="file" /></p>
  <p><input type="submit" /></p>
</form>

This is the corresponding PHP script:

// Only the file name
$originalFileName = $_FILES["upfile"]["name"];
// Full path
$serverFilePath = $_FILES["upfile"]["tmp_name"];
$fileSizeInBytes = $_FILES["upfile"]["size"];
// Is provided by the browser. Could be missing, or even wrong.
$mimeType = $_FILES["upfile"]["type"];
$errorCode = $_FILES["upfile"]["error"];

// Sanitize
$isUploadedFile = is_uploaded_file($serverFilePath)
if (! $isUploadedFile)
{
  // error - the uploader tries to trick us with some sort of
  // maliciously crafted file name
}


$serverDestinationFilePath = "/path/to/file";
$moveUploadedFileResult = move_uploaded_file($serverFilePath, $serverDestinationFilePath);
if (! $moveUploadedFileResult)
{
  // Either the file is not a valid uploaded file (i.e. same checks are
  // applied as for is_uploaded_file), or something went wrong during
  // the move operation.
}

Notes:

  • When the file is uploaded, the web server places it in a temporary location. This is either the web server's own temporary folder, or a folder specified in the PHP configuration.
  • When the session ends, the information on the temporary location is lost. The web server may or may not keep the temporary file, so it's best if the file is either moved or deleted as part of the processing.
  • move_uploaded_file overwrites the destination file. It also places certain restrictions on the destination path - TODO: which ones?
  • Check out the error codes specification in the PHP docs.
  • If the user does not specify a file but triggers the upload, the file size will be 0 and the file name on the server will be empty.
  • A form may contain multiple file input controls - simply use the input control's name as the key to access the $_FILES array. If the input controls all share the same name, which must have the form "inputcontrolname[]", then PHP stores each uploaded file's information in an array. To access it, you could then do something like foreach ($_FILES["inputcontrolname"]["name"] as $key => $originalFileName) { ... }. See the HTML array feature in the PHP docs.
  • The PHP docs have some suggestions for displaying an upload progress bar.


MySQL

References

The MySQL page on this wiki.


Collation

The collation to use to properly store PHP strings is

utf8_general_ci


Connecting/disconnecting

This example shows the basic procedure to connect to the server, get some information, and then disconnect again.

$hostName = "127.0.0.1";
$userName = "foo";
$password = "bar";
$databaseName = "baz";
$port = 3306;
$socketorNamedPipe = "";
$mysqli = new mysqli($hostName, $userName, $password, $databaseName, $port, $socketorNamedPipe);

if ($mysqli->connect_error)
{
  $errorNumber = $mysqli->connect_errno;
  $errorMessage = $mysqli->connect_error;
}
else
{
  $hostInfoString = $mysqli->host_info;
  $mysqli->close();
}

Notes:

  • All parameters to the mysqli constructor are optional. If omitted PHP will use the values of certain options in php.ini as default.
  • Specifying NULL or "localhost" for hostname assumes the local host. TODO: Does this mean that NULL is equivalent to "localhost" and that "localhost" is then DNS-resolved? Or does this mean that PHP "magically" looks up a valid host name or IP address for the local host? As usual, the PHP docs are not very clear.
  • Specify NULL for no password
  • TODO: What must be specified for $databaseName if I don't want a default database?
  • TODO: How does connecting to a pipe or socket work? Specifying both a hostname/port and a socket seems to be incompatible.


The following code sample is roughly equivalent to the first sample above, but it uses procedural style:

[...] // get various connection parameters
$connection = mysqli_connect($hostName, $userName, $password, $databaseName, $port, $socketorNamedPipe);

// [EinstiegPhp7] uses this style for error checking: if (! $connection)
if (mysqli_connect_error())
{
  $errorNumber = mysqli_connect_errno();
  $errorMessage = mysqli_connect_error();
}
else
{
  $hostInfoString = mysqli_get_host_info($connection);
  mysqli_close($connection);
}


Queries

$mysqli = ...;

$queryString = "select * from foo;";
// MYSQLI_STORE_RESULT is the default result mode. This mode fetches the entire result
// set from the server, immediately freeing the connection for other queries.
//
// MYSQLI_USE_RESULT is the alternative non-default result mode. In this mode the
// result set stays on the server and we fetch row by row. This is useful if a query's
// result set can be expected to be huge and we don't want to fetch it in its entirety,
// e.g. to preserve memory. The drawback is that no other server connections can be made
// until the result set has been closed.
$resultMode = MYSQLI_STORE_RESULT;
$result = $mysqli->query($queryString, $resultMode);
if ($result === FALSE)
{
  // Query failed, perform error handling
}
else
{
  // The result type depends on the query type:
  // - For successful SELECT, SHOW, DESCRIBE or EXPLAIN queries the result is an mysqli_result object
  // - For other types of successful queries the result is TRUE
  //
  // Because we ran a SELECT query we now have a mysqli_result object that we can use for
  // subsequent result processing.

  $numberOfRows = $result->num_rows;

  // For SELECT queries this is the same as the "num_rows" property above.
  // For INSERT, UPDATE, DELETE and REPLACE queries this provides the number
  // of inserted, updated, deleted or replaced rows.
  // -1 means the last query had an error.
  $numberOfAffectedRows = $mysqli->affected_rows;

  // fetch_assoc() returns an associative array
  while ($row = $result->fetch_assoc())
  {
    $columnName = "columnA";
    // All values are returned as string type, even if the DB data
    // type for the column is something else, such as int.
    $valueAsString = $row[$columnName];

    // If it's an integer value, we have no choice, we must manually
    // convert from string to int.
    $valueAsInt = intval($valueAsString)
  }

  // Aliases to close: free() and free_result()
  $result->close();
}

$mysqli->close();

Notes:

  • TODO


The following code sample is roughly equivalent to the first sample above, but it uses procedural style:

$connection = ...;

$queryString = "select * from foo;";
$resultMode = MYSQLI_STORE_RESULT;
$result = $mysqli_query($connection, $queryString);
if ($result === FALSE)
{
  // Query failed, perform error handling
}
else
{
  $numberOfRows = mysqli_num_rows($result);
  $numberOfAffectedRows = mysqli_affected_rows($connection);

  while ($row = mysqli_fetch_assoc($result))
  {
    $columnName = "columnA";
    $valueAsString = $row[$columnName];
    $valueAsInt = intval($valueAsString)
  }

  mysqli_free_result($result);
}

mysqli_close($connection);


Escaping special characters

To protect against SQL injection attacks user input must always be pre-processed before it is used in a query.

$mysqli = ...;
$userInputString = ...

$escapedUserInputString = $mysqli->real_escape_string($userInputString);

Notes:

  • The method takes into account the current character set of the connection
  • The connection is configured with the character set automatically if the character set is configured at the server level
  • The connection can also be explicitly configured with mysqli_set_charset()


Procedural style:

$connection = ...;
$userInputString = ...

$escapedUserInputString = mysqli_real_escape_string($connection, $userInputString);


Prepared statements

This is the same as "parameterized queries".

TODO: Write example


PDO

Summary

PDO is short for PHP Data Objects. PDO requires PHP 5.1.

PDO is a database abstraction API, i.e. using the PDO API you can access all sorts of database management systems (DBMSs) in a general manner without relying on a DBMS-specific API.

For instance, you can use the same API to access a MySQL database server and an SQLite database.

Coming from the .NET world, I compare PDO with ADO.NET.


Connecting/disconnecting

try
{
  $pdoDriverName = "mysql";
  $connectionString = "host=localhost;port=3306;dbname=test";     // connecting via TCP/IP
  $connectionString = "unix_socket=/path/to/socket;dbname=test";  // connecting via socket
  $dsn = $pdoDriverName . ":" . $connectionString;

  $userName = "foo";
  $password = "bar";
  $options = null;

  $pdo = new PDO($dsn, $userName, $password, $options);

  // Perform queries

  // There is no method to explicitly close the database connection!
  // The database connection remains active as long as the PDO object remains alive.
  // When the last reference to the object goes away the connection is automatically
  // closed. Here we achieve this by assigning null to the $dbh variable.
  // When the PHP script ends the connection is also automatically closed.
  $pdo = null;
}
catch (PDOException $exception)
{
  // Connection error, perform error handling
  $errorMessage = $exception->getMessage();
}

Notes:

  • DSN is short for "data source name"
  • As seen in the example, in general a DSN consists of the PDO driver name, followed by a colon, followed by the PDO driver-specific connection syntax. Other DSN syntax is possible but not shown here. Refer to the docs for details.
  • Username, password and options are all optional and can be omitted
  • Connection options are frequently driver-specific
  • It is important to handle the exception! If the exception is not handled, the PHP script is terminated and a back trace is displayed. The back trace is likely to reveal the full database connection details, including credentials!


Persistent connections

Paraphrasing from the PHP docs:

Persistent connections are not closed at the end of the PHP script, but are cached and re-used when another script requests a connection using the same credentials. The persistent connection cache allows you to avoid the overhead of establishing a new connection every time a script needs to talk to a database, resulting in a faster web application.


Example:

$dsn = ...
$userName = ...
$password = ...
$options = array(
  PDO::ATTR_PERSISTENT => true
);

$pdo = new PDO($dsn, $userName, $password, $options);

Notes:

  • The ATTR_PERSISTENT attribute must be present in the constructor. Setting the attribute later on by calling PDO::setAttribute() will have no effect.
  • Do not use persistent connections if you use an ODBC driver and that driver already supports connection pooling!


Queries

$pdo = ...

$queryString = "select * from foo;";
$pdoStatement = $pdo->query($queryString);
if ($pdoStatement === FALSE)
{
  // Query failed, perform error handling
}
else
{
  // PDOStatement implements Traversable
  $numberOfRows = 0;
  foreach ($pdoStatement as $row)
  {
    $columnName = "columnA";
    // All values are returned as string type, even if the DB data
    // type for the column is something else, such as int.
    $valueAsString = $row[$columnName];

    // If it's an integer value, we have no choice, we must manually
    // convert from string to int.
    $valueAsInt = intval($valueAsString)

    // For INSERT, UPDATE and DELETE queries you can use
    // $pdoStatement->rowCount() to get the number of rows affected by
    // the query. For SELECT queries, however, this is not reliable:
    // Some DBMS provide the desired value, others do not!
    //
    // The only two reliable ways to get the row count for a SELECT
    // queries are: 1) Count the rows yourself while you process them in
    // your code; or 2) Run a SELECT COUNT using the same WHERE clause
    // as the original SELECT query.
    $numberOfRows++;
  }

  // It's important to remove all references to the PDOStatement object.
  // If this is not done properly, the connection will not close even
  // though the PDO object itself is no longer referenced from our code.
  // The reason is that the PDOStatement object internally has a reference
  // to the PDO object.
  $pdoStatement = null;
}

// Here the connection is finally closed (if the reference to the
// PDOStatement has also been properly removed)
$pdo = null;

Notes:

  • The result of the query() method is a PDOStatement object
  • TODO: Add more details regarding fetching the entire result set or row-by-row


Error handling with exceptions

The previous section showed how to check the result of the query() method for FALSE to detect errors. This is pretty horrible, and in addition I don't know if it's possible to obtain an error message.

A much better way, in my opinion, is to work with exceptions for error handling:

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

try
{
  $sqlQuery = ...;
  $pdo->execute($sqlQuery);
}
catch (PDOException $exception)
{
  // Error in query, perform error handling
  $errorMessage = $exception->getMessage();
}


Prepared statements

Prepared statements are also known as "parameterized queries".

// Named parameters
$queryStringNamedParameters = "select * from foo where bar = :bar and baz = :baz";
$pdoStatement = $pdo->prepare($queryStringNamedParameters);
$pdoStatement->bindParam(":bar", $bar, PDO::PARAM_STR, 15);  // 15 is the maximum string length
$pdoStatement->bindParam(":baz", $baz, PDO::PARAM_INT);
$bar = 'hello';
$baz = 42;
$pdoStatement->execute();
$bar = 'world';
$baz = 17;
$pdoStatement->execute();
while ($row = $pdoStatement->fetch(PDO::FETCH_ASSOC))
{
    $columnName = "columnA";
    // All values are returned as string type, even if the DB data
    // type for the column is something else, such as int.
    $valueAsString = $row[$columnName];

    // If it's an integer value, we have no choice, we must manually
    // convert from string to int.
    $valueAsInt = intval($valueAsString)
}


// Positional parameters
$queryStringPositionalParameters = "select * from foo where bar = ? and baz = ?";
$pdoStatement = $pdo->prepare($queryStringPositionalParameters);
$pdoStatement->bindParam(1, $bar, PDO::PARAM_STR, 15);  // 15 is the maximum string length
$pdoStatement->bindParam(2, $baz, PDO::PARAM_INT);
$bar = 'hello';
$baz = 42;
$pdoStatement->execute();
[...]


// Same as above, but specify the parameters to the execute() method.
// All parameters are treated as type PDO::PARAM_STR.
$pdoStatement = $pdo->prepare($queryStringNamedParameters);
$parameters = array(":bar" => "hello", ":baz" => "42);
$pdoStatement->execute($parameters);

$pdoStatement = $pdo->prepare($queryStringPositionalParameters);
$parameters = array("world", "17);
$pdoStatement->execute($parameters);

Notes:

  • You have to specify the string length to bindParam() only if it's an out parameter. Probably this information is required to allocate an internal buffer.
  • Positional parameters use 1-based indexes
  • TODO: What happens if a string value exceeds the specified maximum length?
  • TODO: What happens if the maximum string length is omitted for a parameter of type PDO::PARAM_STR?
  • TODO: Add example that show how to specify an out parameter in the call to bindParam()
  • Use bindValue() instead of bindParam() if you don't need out parameters. Note that bindValue() does not have a fourth parameter to specify the string length.


Number of affected rows

$pdo = [...]
$updateStatement = [...]

$updateStatement->execute();

$numberOfAffectedRows = $updateStatement->rowCount();
if ($numberOfAffectedRows > 0)
{
  // At least 1 row was affected by the UPDATE
}

Notes:

  • In MySQL, if an UPDATE query does not change a row because the new value is the same as the old value, the number of affected rows remains 0 (zero). In other DBMS this may or may not be the case as well.


Tools

PHPUnit

References


Installation

PHPUnit is distributed as a single .phar file (PHP archive).


Command line, manually:

  • Download the .phar
  • Store it as phpunit (i.e. without the extension .phar)
  • Make the file executable (chmod +x phpunit)
  • Test that it works: /path/to/phpunit --version


Command line, with Homebrew (must be run as user with administrator privileges):

brew tap homebrew/homebrew-php   # only necessary if homebrew-php has not been tapped yet
brew install php72               # required because the system version of PHP is too low for current versions of PHPUnit
brew install phpunit


Run tests

Command line:

/path/to/phpunit --bootstrap src/autoload.php tests/FooTest

Notes:

  • src/autoload.php is a script that sets up autoloading for the classes that are to be tested. Such a script is commonly generated using a tool such as phpab.
  • The --bootstrap option tells PHPUnit to run the script before it runs any tests.
  • PHPUnit runs tests in FooTest.php
  • You could also just say tests to tell PHPUnit to run all tests in the folder that are located in files named *Test.php


PhpStorm:

  • When you have the unit test file open, select "Run foo-test.php" from the context menu. PhpStorm shows the output not in a browser but in its own project window.


Write tests

final class FooTest extends TestCase
{
    protected function setUpBeforeClass()
    {
      // this is invoked once before the first test method is run
    }

    protected function setUp()
    {
      // this is invoked once before every test method is run
    }

    protected function tearDownAfterClass()
    {
      // this is invoked once after the last test method is run
    }

    protected function tearDown()
    {
      // this is invoked once after every test method is run
    }

    public function testBar()
    {
        $this->assertEquals($expectedValue, $actualValue, "bla bla bla");
    }
}

Notes:

  • Test classes by convention are named with suffix "Test"
  • Test classes must extend TestCase
  • Test functions must be named with prefix "test"
  • Test functions must be public. Non-public functions cannot be test functions because the test runner has no access to them. Consequently, non-public functions must not be named with prefix "test", otherwise the test runner will try to execute them but will fail due to visibility constraints.
  • assertEquals uses the == operator


Composer

About

Composer is a widely used package and dependency manager for PHP projects, similar to npm for JavaScript. When you have a project that depends on some PHP libraries you can use Composer to fetch those libraries and make them available in your project. Furthermore, if the libraries your project depends on have dependencies of their own, Composer will automatically manage those transitive dependencies for you as well.


References


Installation

A simple and convenient method to use Composer is to install it locally into your project as a .phar archive. Download the installer script composer-setup.php from the Composer website, store it in your project's root folder, then run the script with the following command:

php composer-setup.php --install-dir=bin --filename=composer

This creates the bin folder if it does not exist yet and places the .phar archive in the folder as an executable named composer. Note that the system-provided PHP version should be sufficient to run Composer on the command line.


An interesting option - and actually the one I prefer - is to install Composer system-wide so that you have it available in all projects and on all system accounts. Probably the simplest way to achieve this is to install Composer via your favourite package manager. On Mac OS X this is Homebrew. After you have Homebrew installed, run the following commands as a user with administrator privileges:

brew tap homebrew/homebrew-php   # only necessary if homebrew-php has not been tapped yet
brew install composer

On Debian, of course, you use APT:

apt-get install composer


Configuration

In your project you need this configuration file:

composer.json

In it you declare the dependencies your project has on external libraries.

Example content:

{
    "require":
    {
        "<vendor-name>/<project-name>": "1.0.*"
    }
}

Notes:

  • The "require" key has as its value an object that maps package names to version constraints
  • In the example, no package repository was specified, therefore packages are searched for in the default package repository named "Packagist". For details about package repositories see the Composer docs.
  • Package names are a combination of a vendor name and a project name
  • Package versions can be either fixed, or - as shown in the example - include a version number range. For detailed rules about version numbers see the Composer docs.


Installing dependencies

Run Composer with the following command in your project's root folder (where you placed composer.json):

php composer.phar install   # use this if you installed Composer locally into your project
composer install            # use this if you installed Composer system-wide via Homebrew

Normally Composer downloads dependencies into the folder

vendor

Once it has installed the dependencies, Composer creates a file where it stores the names and versions of the dependencies that it actually downloaded. The file's name is

composer.lock

This file should be added to version control! If someone else clones the version control repository and runs composer install, Composer will download and install exactly those dependencies and versions that are noted down in composer.lock. In fact, Composer completely ignores the composer.json config file. This is to make sure that all collaborators on a project work with exactly the same dependency versions.


Xdebug

Website: https://xdebug.org/

Xdebug is a PHP extension for debugging.

On Mac OS X, the following will make Xdebug available system-wide via Homebrew:

brew install php72-xdebug


PhpStorm

PHP Interpreter

An important configuration setting for PhpStorm is configure a CLI PHP interpreter. This allows PhpStorm to run PHP scripts from within the editor. It's possible to configure several interpreters, but you have to choose one of them for your project.

Adding a new interpreter works like this:

  • Preferences > Languages & Frameworks > PHP
  • Click the button "..." on the line labelled "CLI Interpreter"
  • Navigate to the location of the interpreter you want to configure, then select it. On my Mac I at least configure the interpreter I installed via Homebrew, but a second alternative can also be to use the one from the MAMP installation.
  • After you've configured at least one interpreter, you must now choose one for the project. In addition you also set the following:
  • The PHP language level. This probably is the basis for what PHpStorm considers to be legal/illegal code.
  • 0-n include paths, i.e. paths in which the PHP interpreter should look for libraries
  • Various settings governing the behaviour of the PHP runtime environment. I don't know what these are...


From now on when you edit a PHP file you can open the context menu and select "Run foo.php". The script is executed and the output is displayed either in the browser or in PhpStorm's console.


Deployment

You can configure PhpStorm so that it can deploy changes in your project into a test environment. This is the configuration I used to deploy into a local Bitnami MAMP stack environment:

  • Tools > Deployment > Configuration
  • Click "+" button to add a web server
  • Enter a name (e.g. "Bitnami MAMP")
  • Enter a type (e.g. "Local or mounted folder")
  • Click "OK" to create the server
  • Select the folder to deploy to in the "Upload/download project files" (e.g. /Users/ffhs/Applications/bitnami-mampstack-7.1.13/apache2/htdocs/fswebt)
  • Enter a value for "Web server root URL" (e.g. http://localhost:8080/fswebt)
  • On the tab "Mappings" enter a deployment path (e.g. ".")


If you want deployment to take place automatically after each file change, set this menu option: Tools > Deployment > Automatic Upload (always).


PHPUnit

  • Preferences > Languages & Frameworks > PHP > Test Frameworks
  • Click button "+"
  • Select "PHPUnit local" from the popup menu
  • Under "PHPUnit library" select the option "Path to phpunit.phar"
  • You should now see a download link. If you have already downloaded phpunit.phar for a different project you can ignore the link. Otherwise click the link to download the file. Store the file somewhere in a common folder that can be used from different projects. For instance, I chose /Users/ffhs/PHP/phpunit-7.0.1/phpunit.phar. Adding the PHPUnit version number to the folder name makes sure that later you can work with different versions of PHPUnit.
  • Alternatively, if you have installed PHPUnit via Homebrew, then you can enter the following path: /usr/local/Cellar/phpunit/7.0.1/libexec/phpunit-7.0.1.phar (unfortunately the regular executable /usr/local/bin/phpunit doesn't work, PhpStorm appears to be unable to determine the PHPUnit version and then refuses to work with it).


From now on when you edit a PHP file you can open the context menu and select "Run foo-test.php". The script is executed and the output is displayed in PhpStorm's console.


Bitnami MAMP

Bitnami provides a nice MAMP stack which - unlike the widely known MAMP application bundle - does not need to be installed into the /Applications folder, so you can use this on a non-privileged account without any system-wide side-effects.

Get Bitnami's MAMP stack from here: https://bitnami.com/stack/mamp


If you're deploying PHP files from your IDE (e.g. PhpStorm) into the MAMP stack's htdocs folder then you will probably run into the problem that the MAMP stack recognizes file changes only after a certain period of time, or after you restart the Apache service. This is due to the "OPCache" caching mechanism which Bitnami has enabled by default to improve performance. To disable the cache, edit php.ini and disable the cache like this:

opcache.enable=0