LearningPHP
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.
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
andPHP_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
andInfinity
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 constantNAN
represents this. Comparing aNaN
value to any other value (including the constantNAN
) always results inFALSE
(the only exception is: If compared toTRUE
the result isTRUE
). Because of this, the only reliable way to check forNaN
is the functionis_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
andInfinity
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 valueNULL
. - 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 thestrict_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 aswitch
structure. The meaning is the same asbreak
. So if you have aswitch
inside a loop, and you want to continue with the next loop iteration from somewhere within theswitch
, then you have to usecontinue 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 theinclude
statement returns the value specified by thereturn
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:
- 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.
- If the specified path begins with "." or "..", PHP will look for the file relative to the current working directory.
- 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 ofclass
- 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 thevar_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
andError
both implement the interfaceThrowable
but are otherwise unrelated- User-defined exceptions are prohibited from implementing
Throwable
- they must extend eitherException
orError
- 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 asiterable
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 likeforeach ($_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 inphp.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 aPDOStatement
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 ofbindParam()
if you don't need out parameters. Note thatbindValue()
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
- Website: https://phpunit.de/index.html
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
- Website: https://getcomposer.org/
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