Basic

Brackets

Enclosing brackets. Anything not inside the brackets will be rendered as plain text.

<?php
    //php here
?>
plain text here
You can have as many php sections in a file as you want.

You cannot nest php sections.

<?php
    <?php
        //invalid
    ?>
?>

All php sections in a file are interpreted as a single program. For instance, this is a valid if/else.

<?php if(x > 0) { ?>
    text if x is positive
<?php } else { ?>
    text otherwise
<?php } ?>

Comments

Single line

//comments

Multi-line

/*
    comments
*/

Echo

Echo will output text to standard-out.

echo "Hey";

Use . (period) for string concatenation.

echo "Hey, " . $username;

Print

Print outputs to standard-out.

print("text");
print_r($full_array);

Variables

All variable names must start with a "$" (dollar sign).

$x = 5;

That's it for declaring a variable, you don't need a "var" keyword or a type or anything.

Check if a variable has a value:

if(isset($x))
{
}
Namespacing

ignore all this, haven't gotten it working anyway


    <?php namespace Widget;
    
        class Dongle { 
            static function doThing() {
                echo 'did it';
            }  
        }
    ?>
    
    <?php namespace MyProject\Utils;
    
        function sum(a, b) {
            return a + b;
        }
    ?>

    <?php
    
        use Widget as W;
        use MyProject\Utils; //defaults to "use MyProject\Utils as Utils;"
    
        \W\Dongle::doThing(); //referencing a method in a class
        $result = Utils\sum(1, 2); //referencing a function
    ?>

    <?php
    
        use function MyProject\Utils\sum;
    
        $result = sum(1, 2); //referencing a function
    ?>

Include

Include text of another file in this one, to use functions/etc.

Note that relative paths are relative to the place you run "php" from.


include("filename.php");
include("../filename.php");
include("/absolute/system/path/filename.php");

Includes are transitive, meaning that if A.php includes B.php, and B.php includes C.php, then elements of C can be used by A.

Constants

end of line character(s)

$output = str_replace(PHP_EOL, "<br/>", $input);

Magic Constants

Magic constants are provided automatically by the php engine, provided the correct extensions are installed.


$current_line_number_in_file = __LINE__;

$current_path_and_filename = __FILE__;

$current_directory = __DIR__;
//or
$current_directory = dirname(__FILE__);

$current_function_name = __FUNCTION__;

$current_class_name_with_namespace = __CLASS__;

$current_trait_name_with_namespace = __TRAIT__;

$current_class_method_name = __METHOD__;

$current_namespace = __NAMESPACE__;

System Variables

Server

Variables that might be provided by the web server:

The full URL, including query string:

$_SERVER['REQUEST_URI']

Just the path part of the URL, excluding the home address (such as www.site.com) and excluding the query string.

parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
String

Operations

. = string concatenation

$name = $first . $last;

strlen = string length

$characterCount = strlen("apple");

trim = remove leading and trailing white-space characters from a string

$trimmedString = trim($string);

contains

if(strpos($string, $substring) !== false)
{
    echo "string contains substring";
}

explode

$array = explode("/", $filename);

split string into characters

$chars = str_split($string); //H e l l o W o r l d
$chars = str_split($string, 3); //Hel loW orl d

implode

$string = implode(",", $array);

replace

$output = str_replace("old", "new", $input);

strtoupper = convert entire string to upper-case

$upperCase = strtoupper($string);

replace

$output = str_replace("old", "new", "input string");

replace the first instance of "old"

function replace_first_instance($old, $new, $text)
{
    $i = strpos($text, $old);
    if ($i !== false) {
        $text = substr_replace($text, $new, $i, strlen($old));
    }
    return $text;
}
to replace the last instance, use "strrpos" instead of "strpos"

ord = convert a character to its ascii code (integer)

$asciiCode = ord($char);

chr = convert an ascii code (integer) to a character

$char = chr($asciiCode);

convert string to integer

if(ctype_digit($string)) {
    return intval($string);
}

Array

Init an array

$a = array();

Print full array:

print_r($myArray);

Associative Array

An associative array can be called a hash or a dictionary in other languages. Each element is a key/value pair.

$array = array( "key1"=>"value1", "key2"=>"value2" );
$value1 = $array["key1"];

contains key

if (array_key_exists('key', $search_array)) {
    //operation
}

append

$array[$key] = $value;
$array += array($key=>$value);
note that neither update is made in-place
if this array was already an element in a parent array, the parent array will still see the old version of the array

Operations

add to array

array_push($array, $element); //add to the end
aray_unshift($array, $element); //add to the beginning

remove from array

$element = array_pop($array); //get from the end
$element = aray_shift($array); //get from the beginning

count = array length

$elementCount = count($array);

contains

$isInArray = in_array($element, $array);

sort (as of php 5 at least)

sort($array);    //sort arrays in ascending order
rsort($array);   //sort arrays in descending order
natsort($array); //sort alphanumeric strings the way a human would
usort($array);   //sort arrays with a user-defined function

asort($array);   //sort associative arrays in ascending order, according to the value
ksort($array);   //sort associative arrays in ascending order, according to the key
arsort($array);  //sort associative arrays in descending order, according to the value
krsort($array);  //sort associative arrays in descending order, according to the key
Note that php sorts considers underscore "_" character to be after alphabetic characters, which is not the usual directory sorting order. Possible solution here:

function cmp($a, $b) {
    $aTemp = str_replace('_', '0', $a);
    $bTemp = str_replace('_', '0', $b);     
    return strcmp($aTemp,$bTemp);
}
usort($arr, "cmp");

map: apply a function to each element and return an array of the results

$output_array = array_map('function_name', $input_array);
specify the function name by the first string parameter

explode = split a string into an array of string, based on the delimiter

$array = explode(",", $string);

sets array's internal pointer to the first element and returns it

$firstElement = reset($array);

sets array's internal pointer to the last element and returns it

$lastElement = end($array);
Object

Basic:

class myClass
{
    function myMethod()
    {
        echo "Test"; 
    }
}

$x = new myClass;
$x->myMethod();

Store all constructor arguments as local fields:

class myClass
{
    public function __construct(array $arguments = array()) 
    {
        if (!empty($arguments)) 
        {
            foreach ($arguments as $property => $argument) 
            {
                $this->{$property} = $argument;
            }
        }
    }
}

Fields


class myClass
{
    public $myField = "default";
}

$x = new myClass;
$x->{'myField'} = "A";

$fieldName = 'myField';
$x->$fieldName = "B";

Arguments

Function arguments are required, unless you set a default value.

Static Method


class myClass
{
    public static function myStaticMethod()
    {
        echo "Static";
    }
}
myClass::myStaticMethod();
Math

square root

$x = sqrt(36);

Rand will return a random integer, with the min and max inclusive.

$coinFlip = rand(0,1);
Function


$result = myFunction($a, $b);

function myFunction($a, $b)
{
    return $a * $b;
}

Global Variables

To access outer variable within a function, either pass them in as arguments, or declare their use within the function.

$x = 5;

function myFunction()
{
    global $x;
    $x = 6;
}
Controls

"continue" and "break" work like other languages.

If Else

In curly bracket format, "elseif" and "else if" can both be used.


if($x > 0)
{
}
else if($x < 0)
{
}
else
{
}


if($x > 0)
{
}
elseif($x < 0)
{
}
else
{
}

When using colon format, only "elseif" is valid.


if($x > 0):
    //something
elseif($x < 0):
    //something else
else:
    //or this
endif;

For


for($x = 0; $x <= 10; $x++)
{
    echo $x;
}

Foreach


foreach($array as $element)
{
}
Command Line

Arguments

"argv" is an array of command line arguments.


//command line
php myScript.php a b c

//in script
$x = $argv[0]; //"myScript.php"
Xy = $argv[1]; //"a"
Regular Expressions

Get 0 or 1 for if there is at least one match in the string

$result = preg_match('/(foo)(bar)(baz)/', 'foobarbaz');

Get details of matches within string

$matches = ""
preg_match('/a(.*)d/', 'abcd', $matches);
print_r($matches);

//$matches is set to an array
//first element is the full substring that matched the pattern
//then each substring inside parentheses in the regex is returned
/*
Array
(
    [0] => abcd
    [1] => bc
)
*/

Get number of times a match is found in the string

echo preg_match_all('/(foo)(bar)(baz)/', 'foobarbaz');

Get details of matches within string

$matches = ""
preg_match_all('/a(.*)d/', 'abcd', $matches);
print_r($matches);

//$matches is set to an array
//first element is the full substring that matched the pattern
//then each substring inside parentheses in the regex is returned
/*
Array
(
    [0] => Array( [0] => abcd )
    [1] => Array( [0] => bc )
)
*/

Replace matches with new substring

$result = preg_replace('/search_text/i', 'replacement_text', 'original_text');

Modifiers

"//i" case-insensitive
"//m" the ^ and $ characters can match based on new-lines instead of just the entire string
"//s" dots (.) can match new-line characters
"//x" white-space (or anything between # symbols) in the pattern is ignored unless it is escaped or within a character class
"//A" will only match starting at the beginning of the string
"//U" quantifiers will not be greedy, unless they are immediately followed by a ?

"//sU" can match across new-lines, but will take the shortest matches possible - what you think of as "multi-line" behavior

"\" use backslash to escape special characters, so they are treated as normal characters

Character Classes

"\d" matches any digit character
"\w" matches any ASCII character (latin alphabet, digits, underscore)
"\s" matches any whitespace character (space, tab, new line, carriage return, etc)

"\D" is the inverse set of "\d"
"\W" is the inverse set of "\w"
"\S" is the inverse set of "\s"

"[abc]" for creating a custom character class (replace "abc" with whatever), matches a single char from the classes

Anchors

"^" means beginning of line (this character is called "circumflex")
"$" means end of line


File

Write


$file = fopen($fileName, 'w');
fwrite($file, $fullText);
fclose($file);

Directory


$pages = glob('../pages/*.txt');
//result is array of "../pages/a.txt", "../pages/b.txt" etc
Include

Include

Include other php or text files in the current script.

include "otherFile.php";
The path to the otherFile can be absolute, based on the "include_path", or relative based on the current script's directory.

The contents of included files can be considered to be copied directly into that line in the current script. That's true even if the "include" is inside a function definition.

Require

Require is just like Include, except if the file is not found you'll get a fatal error.
Error Handling

INI

Set script to output error details to standard-out.

ini_set('display_errors', 'On');

Try Catch


try
{
}
catch(Exception $exception)
{
    echo $exception->getMessage();
}

Exception

Throwing exceptions:

throw new Exception("Error message.");
HTTP

Parameters

Accessing GET parameters:

$email = $_GET["email"];

Set Headers

Set a custom HTTP response code and message.


$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
header($protocol . ' ' . $code . ' ' . $text);

This sets the status and text that are accessible through javascript:

//var request = new XMLHttpRequest();
//request.status = 500
//request.statusText = "Internal Server Error"

500 is the standard "Internal Server Error" code.


Email

Send


$headers = "From: noreply@website.com\r\n";
mail($to, $subject, $message, $headers);

Use these headers for HTML formatted messages.

$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type:text/html;charset=UTF-8\r\n";
$headers .= "From: noreply@website.com\r\n";

Validate

Copied this regular expression from the web for verifying a email address is probably valid.

function isValidEmail($text)
{
    $regex = "/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/";
    return (preg_match($regex, $text));
}

MySQLi

A php database library which may need to be installed separately.

You can check if the correct library is installed:

if (!function_exists('mysqli_connect')) 
{
    throw new Exception("mySQLi not found.");
}

Open Connection

(servername will be "localhost" if running script and database on the same machine)


$connection = mysqli_connect($servername, $database_username, $database_password, $database_name);
if ($connection->connect_error) 
{
    throw new Exception("Connection failed: " . $connection->connect_error);
}
//query database
mysqli_close($connection);

Basic Query

Fast-and-dirty query without error checking.


$sql = "SELECT Id, Name FROM Customer";
$result = $connection->query($sql);
$id = null;
$name = null;
if ($result->num_rows > 0) 
{
    while($row = $result->fetch_assoc()) 
    {
        $id = $row["Id"];
        $name = $row["Name"];
    }
}

"fetch_assoc" is return a row of data as an associative array using the column names from the SELECT statement.

Prepare Statement


$sql = mysqli_prepare($connection, "SELECT Id, Name FROM Customer"); //note: no ending semi-colon in query
if(!$sql)
{
    throw new Exception("Could not prepare query statement in ".__FUNCTION__."."); //see constants __FUNCTION__ and __METHOD__
}
//actually run the query and load any results
$sql->close();

Bind Results

You can fetch a row of results directly into a set of variables by binding them.

The arguments in "bind_result" must be in the same order as the columns in the SELECT statement.


if(!$sql->execute())
{
    throw new Exception($sql->error);
}
$var1 = null;
$var2 = null;
$var3 = null;
$sql->bind_result($var1, $var2, $var3);
if(!$sql->fetch())
{
    throw new Exception($sql->error . " in " . __FUNCTION__ . ".");
}
echo "Got ".$var1." and ".$var2." and ".$var3;

Result variables must be bound between "execute" and "fetch".

Counting Results

"store_result" loads all results from the database into local memory. It is required before you can count the resulting rows.

"num_rows" returns the number of rows in the result set.


$sql->bind_result($var1, $var2, $var3);
$sql->store_result();
if($sql->num_rows > 0)
{
    if(!$sql->fetch())
    {
        throw new Exception($sql->error);
    }
}

Alternate Format


$sql = mysqli_prepare($connection, "SELECT SourceId FROM Source WHERE Name = ?");
if(!$sql)
{
    throw new Exception("Could not prepare query statement in ".__FUNCTION__.".");
}
$sql->bind_param("s", $sourceName);
$sql->execute();
$sql->store_result();
if($sql->num_rows === 0)
{
    //action: no results
}
$sourceId = null;
$sql->bind_result($sourceId);
$sql->fetch();
$sql->close();

Parameterized Query

Always use parameterized queries when including variable data in a query. This will protect you from SQL injection attacks and many formatting errors.

Replace each variable in the query with a "?" (question mark).


$sql = mysqli_prepare($connection, "SELECT Id, Name FROM Customer WHERE LocationId=? AND Category=?");
if(!$sql)
{
    throw new Exception("Could not prepare query statement.");
}

$sql->bind_param("is", $locationId, $category);

if(!$sql->execute())
{
    throw new Exception($sql->error);
}

The first argument in "bind_param" is the types of the variables, one character each, in the same order as the query.
i = integer
d = double
s = string
b = blob

The rest of the arguments in "bind_param" are the variables, in the same order as the query.
Web

Fetch document as text

$content = file_get_contents($url);
echo $content;

//cannot handle badly formed HTML
$dom = new domDocument;
$dom->loadHTML($content); 
$dom->preserveWhiteSpace = false;
$bodyTable = $dom->getelementsbytagname('body');
echo $bodyTable->item(0);

Routing Pattern

How to create URL routing rules in a PHP application.

.htaccess

The .htaccess file operates at the directory level. The file in the current directory will override any file in a higher/ancestor directory.

You can use .htaccess to route everything to an index.php instead of following the full URL path.
PHPUnit Unit Tests

Setup

Install: sudo apt install phpunit

Verify Installtion: phpunit --version

Run a test file: phpunit myunittests.php

Unit Test

Test functions must be named "test...".
Will only run the first test class found in the file.
An "exit()" being run during a test will exit phpunit.


    <?php
    
    include("../file_under_test.php");

    use PHPUnit\Framework\TestCase;
//    use MyProject\MyNamespace; //haven't got namespacing working yet

    class TestXYZ extends TestCase
    {
        public function testSomething() : void
        {
            //Arrange
            $password_raw = "redyellowblue";
            //Act
            $hashed_a = create_password_hash($password_raw);
            $hashed_b = create_password_hash($password_raw);
            //Assert
            self::assertNotSame($hashed_a, $hashed_b);
        }
    }

    ?>

Testing Global Functions

You cannot override, overload, monkey patch, or mock global functions.
Here is an example of refactoring code so that unit testing is still possible (assuming you are not using Classes, which can be mocked).

Original file:

    <?php
        if(php_sapi_name() == "cli") {
            http_response_code(404);
            include('404_not_found.php');
            die();
        }
        
        if($argv[0] == "create_admin") {
            //stuff
        }
        else if($argv[1] == "remove_user") {
            //stuff
        }
    ?>
This is hard to unit test because we can't change the result of "if(php_sapi_name() == "cli")", it will always be false during testing.

Refactored file:

    <?php
        if(php_sapi_name() == "cli") {
            http_response_code(404);
            include('404_not_found.php');
            die();
        }
        
        handle_command($argv);
        
        function handle_command($arguments) {
            if($arguments[0] == "create_admin") {
                //stuff
            }
            else if($arguments[1] == "remove_user") {
                //stuff
            }
        }
    ?>
Function "handle_command" can be fully unit tested. The glue putting it all together would need to be end-to-end tested.

Testing Echo


//in the code under test
echo("abc");

//test case
public function testEchoMessage() : void
{
    $this->expectOutputString("abc");
    functionUnderTest();
}    

Testing Exceptions


$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("message");

For more explicit assertions on an exception, use "try/catch" within the test method to get the exception.