PHP runs on the server: a request comes in, a script runs top to bottom, and
whatever it echoes becomes the response. The reference below is grouped by what
you are trying to do, and the filter box searches all of it at once. Type array
and every array function on the page comes to you, or type 8.4 to see what
arrived in that release.
Every snippet is checked against PHP 8.5, the current release. Anything that
needs PHP 8.0 or newer says so in the notes column, so you can tell at a glance
whether it will run on the server you have. Variables like $user and $pdo
are placeholders for your own. If you do not have PHP installed, the
official Docker image is the quickest way to try something: docker run --rm -it php:8.5-cli php -a,
and the Docker cheat sheet has the rest.
Searches the task, the command and the third column. Press / from anywhere on the page.
222 commands
Running PHP
| Task | Command |
|---|---|
| Check which version you have | php -v |
| Run a script | php script.php |
| Open an interactive shell | php -a |
| Run one line without a file | php -r 'echo PHP_VERSION;' |
| Serve the current folder on localhost | php -S localhost:8000 |
| Check a file for syntax errors | php -l file.php |
| List loaded extensions | php -m |
| Find which php.ini is in use | php --ini |
The built-in server started by php -S is for development only. It handles one request at a time and is not hardened for the internet.
Syntax and variables
| Task | Code | Notes |
|---|---|---|
| Start a PHP file | <?php | Leave off the closing ?> in files that are only PHP |
| Turn on strict type checks | declare(strict_types=1); | First statement in the file |
| Print something | echo "Hello"; | |
| Short echo inside HTML | <?= $name ?> | |
| Assign a variable | $name = "Ada"; | Every variable starts with $ |
| Define a constant | const MAX_USERS = 100; | |
| Comment | // one line # one line /* block */ | |
| Dump a value while debugging | var_dump($value); | |
| Get a value's type as a string | get_debug_type($value) | PHP 8.0+ |
| Is it set and not null | isset($value) | |
| Is it empty, falsy or unset | empty($value) | True for 0, "0", "", [] and null |
| Default when null or unset | $page = $input ?? 1; | |
| Assign only if null or unset | $options["debug"] ??= false; | PHP 7.4+ |
| Call a method only if not null | $user?->address?->city | PHP 8.0+ |
| Compare value and type | $a === $b | Prefer this to == |
| Compare for sorting | $a <=> $b | Returns -1, 0 or 1 |
| Cast to a type | (int) "42" | Also (string), (float), (bool), (array) |
| Whole-number division | intdiv(7, 2) | 7 / 2 is 3.5 |
| Raise to a power | 2 ** 10 | |
| Readable big numbers | 1_000_000 | PHP 7.4+ |
Strings
| Task | Code | Notes |
|---|---|---|
| Interpolate a variable | "Hello, $name" | Double quotes only |
| Interpolate an expression | "Total: {$order->total}" | |
| No interpolation | 'Hello, $name' | Prints $name literally |
| Join strings | $first . " " . $last | |
| Append to a string | $html .= "</ul>"; | |
| Length in bytes | strlen($s) | strlen("café") is 5 |
| Length in characters | mb_strlen($s) | mb_strlen("café") is 4 |
| Change case | strtolower($s) strtoupper($s) ucfirst($s) | |
| Strip whitespace from both ends | trim($s) | ltrim and rtrim for one end |
| Does it contain a substring | str_contains($s, "cat") | PHP 8.0+ |
| Does it start or end with | str_starts_with($url, "https") | str_ends_with too. PHP 8.0+ |
| Position of a substring | strpos($s, "cat") | Returns false if missing and 0 at the start |
| Replace text | str_replace("cat", "dog", $s) | |
| Part of a string | substr($s, 0, 5) | mb_substr for multibyte text |
| Split into an array | explode(",", $csv) | |
| Join an array into a string | implode(", ", $names) | |
| Pad to a length | str_pad($n, 3, "0", STR_PAD_LEFT) | 7 becomes 007 |
| Repeat | str_repeat("-", 20) | |
| Format with placeholders | sprintf("%s has %d items", $name, $count) | |
| Format a number | number_format(1234.5, 2) | Returns 1,234.50 |
| Match a pattern | preg_match('/\d+/', $s, $matches) | Returns 1, 0 or false |
| Replace by pattern | preg_replace('/\s+/', ' ', $s) | |
| Escape for HTML output | htmlspecialchars($s) | Do this to anything a user typed |
| Encode to JSON | json_encode($data) | |
| Decode JSON into arrays | json_decode($json, true) | Without true you get objects |
| Decode JSON and throw on bad input | json_decode($json, true, flags: JSON_THROW_ON_ERROR) |
Arrays
PHP has one array type that does the job of a list and a dictionary. Keys are integers or strings, and the order you insert things is the order you get them back.
| Task | Code | Notes |
|---|---|---|
| Create a list | $nums = [1, 2, 3]; | |
| Create a keyed array | $user = ["name" => "Ada", "age" => 36]; | |
| Read a value | $user["name"] | |
| Append to the end | $nums[] = 4; | |
| Remove a key | unset($user["age"]); | Leaves a gap in integer keys |
| Count items | count($nums) | |
| Does a key exist | array_key_exists("name", $user) | isset() is false when the value is null |
| Does a value exist | in_array(3, $nums, true) | Pass true for strict comparison |
| Find the key of a value | array_search(3, $nums, true) | Returns false if missing |
| Unpack into variables | [$first, $second] = $nums; | |
| Unpack by key | ["name" => $name] = $user; | |
| Merge two arrays | [...$a, ...$b] | String keys allowed from PHP 8.1 |
| Is it a plain 0, 1, 2 list | array_is_list($arr) | PHP 8.1+ |
| Loop with keys and values | foreach ($user as $key => $value) { } |
Array functions
| Task | Code | Notes |
|---|---|---|
| Transform every value | array_map(fn($n) => $n * 2, $nums) | Callback comes first |
| Keep values that pass a test | array_filter($nums, fn($n) => $n > 1) | Keeps the original keys |
| Drop empty and falsy values | array_filter($values) | |
| Fold into one value | array_reduce($nums, fn($carry, $n) => $carry + $n, 0) | |
| Add them up | array_sum($nums) | |
| First value that passes a test | array_find($users, fn($u) => $u->active) | PHP 8.4+. array_any and array_all too |
| First or last value | array_first($nums) array_last($nums) | PHP 8.5+. null when empty |
| First key | array_key_first($arr) | PHP 7.3+ |
| Renumber keys from 0 | array_values($arr) | |
| Just the keys | array_keys($user) | |
| Merge, renumbering integer keys | array_merge($a, $b) | Later string keys win |
| Merge, keeping the left side's keys | $a + $b | Earlier keys win |
| A slice | array_slice($nums, 1, 2) | Offset, then length |
| Remove or insert in place | array_splice($nums, 1, 1) | |
| Push and pop at the end | array_push($nums, 5) array_pop($nums) | |
| Shift and unshift at the start | array_shift($nums) array_unshift($nums, 0) | |
| Remove duplicates | array_unique($nums) | |
| Values in both arrays | array_intersect($a, $b) | |
| Values in the first but not the second | array_diff($a, $b) | |
| Pull one field out of each row | array_column($rows, "email") | |
| Index rows by a field | array_column($rows, null, "id") | |
| Two lists into keys and values | array_combine($keys, $values) | |
| Swap keys and values | array_flip($arr) | |
| Split into chunks | array_chunk($nums, 2) | |
| A range of numbers | range(1, 10) | |
| Fill with a value | array_fill(0, 5, null) |
Most of these return a new array and leave the original alone. The exceptions are the ones that change the array in place: array_splice, array_push, array_pop, array_shift, array_unshift, and every sort function below.
Sorting arrays
Every sort function sorts the array you pass in place. None of them return the sorted array, so $sorted = sort($nums) gives you true, not a list.
| Task | Code | Keeps keys? |
|---|---|---|
| Sort values, ascending | sort($nums) | No, renumbers |
| Sort values, descending | rsort($nums) | No, renumbers |
| Sort by value, keeping keys | asort($prices) | Yes |
| Sort by value descending, keeping keys | arsort($prices) | Yes |
| Sort by key | ksort($user) | Yes |
| Sort by key, descending | krsort($user) | Yes |
| Sort with your own comparison | usort($users, fn($a, $b) => $a->age <=> $b->age) | No, renumbers |
| Your own comparison, keeping keys | uasort($users, fn($a, $b) => $a->age <=> $b->age) | Yes |
| Your own comparison on keys | uksort($arr, fn($a, $b) => strcmp($a, $b)) | Yes |
| Human order: img2 before img10 | natsort($files) | Yes |
| Shuffle | shuffle($nums) | No, renumbers |
Sorting has been stable since PHP 8.0, so items that compare equal keep their original order. To sort descending by one field and then ascending by another, compare arrays: [$b->score, $a->name] <=> [$a->score, $b->name].
Control flow
| Task | Code | Notes |
|---|---|---|
| If, else if, else | if ($a) { } elseif ($b) { } else { } | |
| Pick one of two values | $label = $count === 1 ? "item" : "items"; | |
| First truthy value | $name = $input ?: "Anonymous"; | Treats 0 and "" as missing, unlike ?? |
| Match a value to a result | match ($code) { 200, 201 => "OK", 404 => "Not found", default => "Error" } | PHP 8.0+. Strict comparison |
| Match on conditions | match (true) { $age < 13 => "child", $age < 18 => "teen", default => "adult" } | PHP 8.0+ |
| Switch | switch ($x) { case 1: ...; break; default: ...; } | Loose comparison, falls through without break |
| Loop over an array | foreach ($items as $item) { } | |
| Loop and change each item | foreach ($nums as &$n) { $n *= 2; } unset($n); | Always unset the reference after |
| Counting loop | for ($i = 0; $i < 10; $i++) { } | |
| While loop | while ($row = $stmt->fetch()) { } | |
| Run at least once | do { } while ($retry); | |
| Skip to the next iteration | continue; | |
| Leave two nested loops | break 2; | |
| Catch an exception | try { } catch (RuntimeException $e) { } finally { } | |
| Catch more than one type | catch (TypeError | ValueError $e) | |
| Catch without using the variable | catch (JsonException) { } | PHP 8.0+ |
| Throw | throw new InvalidArgumentException("Bad id"); | |
| Throw as an expression | $id = $input["id"] ?? throw new InvalidArgumentException("Missing id"); | PHP 8.0+ |
Functions and arrow functions
| Task | Code | Notes |
|---|---|---|
| Define a typed function | function add(int $a, int $b): int { return $a + $b; } | |
| Default parameter | function greet(string $name = "world") { } | |
| Parameter or return that may be null | function find(?int $id): ?User { } | |
| Accept more than one type | function load(int|string $id) { } | PHP 8.0+ |
| Return nothing | function logMessage(string $msg): void { } | |
| Never returns (always throws or exits) | function fail(): never { throw new Exception(); } | PHP 8.1+ |
| Any number of arguments | function sum(int ...$nums): int { } | |
| Spread an array into arguments | sum(...$numbers) | |
| Call with named arguments | htmlspecialchars($s, double_encode: false) | PHP 8.0+. Skip the ones you do not need |
| Change the caller's variable | function addOne(array &$list) { } | Pass by reference |
| Anonymous function | $double = function ($n) { return $n * 2; }; | |
| Anonymous function using an outer variable | function ($n) use ($rate) { return $n * $rate; } | use copies the value in |
| Arrow function | fn($n) => $n * $rate | PHP 7.4+. Sees outer variables automatically |
| Turn a function into a callable | $len = strlen(...); | PHP 8.1+ |
| Chain calls left to right | $slug = $title |> trim(...) |> strtolower(...); | PHP 8.5+ |
| Pipe through an arrow function | $x |> (fn($s) => str_replace(" ", "-", $s)) | PHP 8.5+. Brackets are required |
| Remember a value between calls | static $count = 0; | |
| Check a function exists | function_exists("mb_strlen") |
Arrow functions are limited to a single expression and capture outer variables by value, so assigning to one inside the arrow function does not change it outside. When you need several statements, use function with a use clause.
Classes and interfaces
| Task | Code | Notes |
|---|---|---|
| Constructor that declares properties | public function __construct(private int $id) {} | PHP 8.0+ |
| Create an object | $user = new User(1, "Ada"); | |
| Call a method straight after new | new Money(5)->format() | PHP 8.4+. Wrap in brackets before that |
| Property or method | $user->name $user->save() | |
| Static method or property | User::find(1) self::$count | |
| Class constant | const ROLE = "admin"; self::ROLE | |
| Call the parent's version | parent::__construct($id); | |
| Class name as a string | User::class | |
| Inherit from a class | class Admin extends User { } | |
| Declare an interface | interface Shape { public function area(): float; } | |
| Implement interfaces | class Circle implements Shape, JsonSerializable { } | |
| Class that cannot be created directly | abstract class Model { abstract public function table(): string; } | |
| Class that cannot be extended | final class Money { } | |
| Share methods without inheritance | trait Timestamps { } use Timestamps; | use goes inside the class |
| Property that is set once | public readonly int $id; | PHP 8.1+ |
| Every property read-only | readonly class Point { } | PHP 8.2+ |
| Public to read, private to write | public private(set) string $name; | PHP 8.4+ |
| Backed enum | enum Status: string { case Active = "active"; } | PHP 8.1+ |
| Enum from a stored value | Status::from("active") Status::tryFrom($input) | tryFrom returns null instead of throwing |
| Check an object's type | $shape instanceof Circle | |
| Copy an object | $copy = clone $user; | Shallow copy |
| Copy and change properties | return clone($this, ["x" => 5]); | PHP 8.5+. The with-er pattern for readonly classes |
| Turn into a string | public function __toString(): string { } |
Visibility is public, protected or private, and it applies to properties, methods and constants. Leave it off a method and it is public, but spell it out anyway: it is the first thing anyone reading the class looks for.
Namespaces
A namespace is a prefix that stops your User class colliding with a library's. The convention, PSR-4, is that the namespace mirrors the folder, so App\Models\User lives in src/Models/User.php.
| Task | Code | Notes |
|---|---|---|
| Declare the file's namespace | namespace App\Models; | First statement after declare |
| Import a class | use App\Models\User; | |
| Import under another name | use App\Models\User as Account; | |
| Import several from one namespace | use App\Models\{User, Post}; | |
| Import a function | use function App\Support\slugify; | |
| Import a constant | use const App\Support\VERSION; | |
| Reach a global class from inside a namespace | new \DateTimeImmutable() | Or add use DateTimeImmutable; |
| The current namespace as a string | __NAMESPACE__ |
Inside a namespace, an unimported class name is looked up in that namespace only, so new DateTime() in App\Models fails. Functions and constants are kinder: strlen falls back to the global one if App\Models\strlen does not exist.
Superglobals
Arrays PHP fills in for every request, readable from anywhere without passing them around. Treat everything in them as untrusted input.
| What you want | Code | Notes |
|---|---|---|
| Query string value, ?page=2 | $_GET["page"] ?? 1 | |
| Form field from a POST | $_POST["email"] ?? "" | |
| Validated integer from the query string | filter_input(INPUT_GET, "page", FILTER_VALIDATE_INT) | false if invalid, null if missing |
| Validated email from a form | filter_var($_POST["email"], FILTER_VALIDATE_EMAIL) | |
| Request method | $_SERVER["REQUEST_METHOD"] | |
| Requested path and query | $_SERVER["REQUEST_URI"] | |
| A request header | $_SERVER["HTTP_ACCEPT_LANGUAGE"] | Uppercased, dashes to underscores, HTTP_ prefix |
| JSON request body | json_decode(file_get_contents("php://input"), true) | $_POST stays empty for JSON |
| A cookie | $_COOKIE["theme"] ?? "light" | |
| Set a cookie | setcookie("theme", "dark", ["expires" => time() + 86400, "httponly" => true, "samesite" => "Lax"]); | Before any output |
| Start or resume a session | session_start(); | Before any output |
| Store in the session | $_SESSION["user_id"] = $user->id; | |
| New session id after login | session_regenerate_id(true); | Prevents session fixation |
| An uploaded file | $_FILES["avatar"]["tmp_name"] | Check ["error"] === UPLOAD_ERR_OK first |
| Keep an uploaded file | move_uploaded_file($_FILES["avatar"]["tmp_name"], $dest) | |
| An environment variable | getenv("DATABASE_URL") | $_ENV is often empty, depending on php.ini |
| Send a header or redirect | header("Location: /login"); exit; | Before any output |
| Set the response status | http_response_code(404); |
$_REQUEST merges GET, POST and cookies into one array, which means a cookie can quietly override a form field. Read from the specific array you expect instead.
PDO basics
PDO is the built-in database layer, with one API for MySQL, PostgreSQL, SQLite and others. The full worked example is further down the page.
| Task | Code | Notes |
|---|---|---|
| Connect to MySQL | $pdo = new PDO("mysql:host=localhost;dbname=app;charset=utf8mb4", $user, $pass); | |
| Connect to PostgreSQL | $pdo = new PDO("pgsql:host=localhost;dbname=app", $user, $pass); | |
| Connect to SQLite | $pdo = new PDO("sqlite:" . __DIR__ . "/app.db"); | |
| Throw exceptions on errors | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | The default since PHP 8.0 |
| Return rows as keyed arrays | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC | Pass in the options array |
| Prepare with positional placeholders | $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); | |
| Run it with values | $stmt->execute([$id]); | |
| Prepare with named placeholders | $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email"); | |
| Run it with named values | $stmt->execute(["email" => $email]); | |
| Fetch one row | $row = $stmt->fetch(); | false when there are no rows |
| Fetch every row | $rows = $stmt->fetchAll(); | |
| Fetch a single value | $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() | |
| Fetch rows as objects of a class | $stmt->fetchAll(PDO::FETCH_CLASS, User::class) | |
| Id of the row just inserted | $pdo->lastInsertId() | |
| Rows changed by an UPDATE or DELETE | $stmt->rowCount() | Not reliable for SELECT |
| Start, commit or undo a transaction | $pdo->beginTransaction(); $pdo->commit(); $pdo->rollBack(); |
Placeholders are for values only. A table name, a column name or ASC and DESC cannot be bound, so check those against a fixed list of allowed values before they go anywhere near the query.
Composer
Composer is PHP's package manager. It reads composer.json, writes the exact installed versions to composer.lock, and generates an autoloader in vendor/.
| Task | Command |
|---|---|
| Start a composer.json interactively | composer init |
| Add a package | composer require monolog/monolog |
| Add a development-only package | composer require --dev phpunit/phpunit |
| Install exactly what the lock file says | composer install |
| Install for production | composer install --no-dev --optimize-autoloader |
| Upgrade everything within constraints | composer update |
| Upgrade one package | composer update monolog/monolog |
| Remove a package | composer remove monolog/monolog |
| See which packages have newer versions | composer outdated --direct |
| Check for known security advisories | composer audit |
| Why is this package installed | composer why psr/log |
| Rebuild the autoloader after changing it | composer dump-autoload |
| Run a script from composer.json | composer run test |
| Start a new project from a template | composer create-project laravel/laravel my-app |
| Check composer.json is valid | composer validate |
| Load everything in your code | require __DIR__ . "/vendor/autoload.php"; |
install and update are not interchangeable. install reproduces the lock file, so it is what you run after cloning and in CI. update resolves fresh versions and rewrites the lock file, so it is a change you commit and review.
A class, start to finish
Most of the class syntax above in one place: strict types, a namespace, a backed enum with a method, an interface, and a readonly class with constructor property promotion.
<?php
declare(strict_types=1);
namespace App\Billing;
enum Currency: string
{
case GBP = 'GBP';
case USD = 'USD';
public function symbol(): string
{
return match ($this) {
Currency::GBP => '£',
Currency::USD => '$',
};
}
}
interface HasTotal
{
public function total(): int;
}
final readonly class Money implements HasTotal
{
public function __construct(
public int $pence,
public Currency $currency = Currency::GBP,
) {}
public function total(): int
{
return $this->pence;
}
public function add(Money $other): self
{
return new self($this->pence + $other->pence, $this->currency);
}
public function format(): string
{
return $this->currency->symbol() . number_format($this->pence / 100, 2);
}
}
$price = new Money(1999);
echo $price->add(new Money(500))->format(); // £24.99Money is held in pence as an integer on purpose. Floats cannot represent most
decimal fractions exactly, so 0.1 + 0.2 == 0.3 is false in PHP, as it is in
almost every language.
Property hooks
PHP 8.4 added hooks, which run code when a property is read or written, so a class no longer needs a getter and setter pair just to normalise a value.
<?php
class User
{
// Runs on every write. $value is the incoming value.
public string $email {
set => strtolower(trim($value));
}
// Computed on every read, with no stored value behind it.
public string $domain {
get => substr($this->email, strpos($this->email, '@') + 1);
}
// Anyone can read it, only the class can change it.
public private(set) int $logins = 0;
public function recordLogin(): void
{
$this->logins++;
}
}
$user = new User();
$user->email = ' Ada@Example.com ';
echo $user->email; // ada@example.com
echo $user->domain; // example.comA PDO query, start to finish
Connect once, use prepared statements for every query that includes a value, and wrap related writes in a transaction so they succeed or fail together.
<?php
$pdo = new PDO(
'mysql:host=localhost;dbname=shop;charset=utf8mb4',
$username,
$password,
[PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC],
);
// Read: the value never touches the SQL string.
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(); // an array, or false if no row matched
// Write: both updates happen, or neither does.
$pdo->beginTransaction();
try {
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
->execute([$amount, $fromId]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
->execute([$amount, $toId]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}beginTransaction() sits outside the try deliberately. If it fails there is no
transaction to roll back, and calling rollBack() would throw a second exception
that hides the first.
Composer autoloading
Tell Composer where your namespace lives and it generates the require calls for
you. This maps every class under App\ to a file under src/:
{
"require": {
"php": "^8.3"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}Run composer dump-autoload after changing the autoload block, then
require __DIR__ . '/vendor/autoload.php'; once at your entry point. After that,
new App\Models\User() loads src/Models/User.php on first use. Commit
composer.json and composer.lock, and add vendor/ to .gitignore. The
Git cheat sheet covers the rest of that workflow.
Strings across several lines
Heredoc interpolates like a double-quoted string. Nowdoc, with the marker in single quotes, is taken literally like a single-quoted one. Since PHP 7.3 the closing marker can be indented, and that indentation is stripped from every line.
<?php
$html = <<<HTML
<p>Hello, {$user->name}</p>
<p>You have {$count} messages.</p>
HTML;
$template = <<<'TEXT'
Nothing here is interpolated: $name stays as written.
TEXT;PHP inside HTML
Templates read more easily with the colon syntax for control structures and the short echo tag for output. Escape everything that came from a user.
<ul>
<?php foreach ($items as $item): ?>
<li><?= htmlspecialchars($item->name) ?></li>
<?php endforeach; ?>
</ul>
<?php if ($items === []): ?>
<p>Nothing here yet.</p>
<?php endif; ?>Gotchas
The mistakes that turn up in almost every PHP codebase at some point.
| Looks right | What actually happens | Do this instead |
|---|---|---|
if (strpos($s, 'a')) | False when the match is at position 0 | str_contains($s, 'a'), or compare !== false |
$a == $b | '1' == '01' and null == false are both true | === |
strlen('café') | 5, because it counts bytes | mb_strlen() for characters |
array_filter($list) then json_encode | Gaps in the keys make it a JSON object, not an array | Wrap it in array_values() |
foreach ($a as &$v) then another loop using $v | The second loop overwrites the last element | unset($v) straight after the first loop |
isset($data['key']) | False when the key exists but holds null | array_key_exists('key', $data) |
array_merge($a, $b) on integer keys | Keys are renumbered from 0 | $a + $b to keep them |
$sorted = sort($list) | $sorted is true, the list was sorted in place | Call sort($list), then use $list |
'Hello $name' | Prints $name literally | Double quotes, or sprintf() |
Echoing before header() or session_start() | "Headers already sent" warning, and nothing is set | Send headers first, and leave off the closing ?> so stray whitespace cannot sneak out |
Under the hood sort() is a hybrid: insertion sort
for short runs and quick sort for everything
else. Both are on the site as step-through visualisations if you want to see what
your array goes through.
Common questions
Which version of PHP does this cheat sheet cover?
PHP 8.5, the current release, which came out in November 2025. Most of the page works on any PHP 8, and anything that needs a newer version says so in the notes column, for example the pipe operator and array_first need 8.5, property hooks and array_find need 8.4, and enums need 8.1. Run php -v to see which version you have.
What is the difference between == and === in PHP?
== compares values after converting types, so "1" == "01" and null == false are both true. === compares value and type, so 1 === "1" is false. Use === unless you have a specific reason not to. PHP 8 made == less surprising: "abc" == 0 used to be true and is now false, because a non-numeric string is no longer converted to 0.
What is the difference between single and double quotes in PHP?
Double-quoted strings replace variables and escape sequences like \n with their values, so "Hi $name" prints the name. Single-quoted strings are taken literally apart from \' and \\, so 'Hi $name' prints the dollar sign and the word name. Use single quotes when there is nothing to interpolate and double quotes when there is.
What is the difference between isset, empty and is_null?
isset is true when a variable exists and is not null, and it does not warn about undefined variables or keys. empty is true when a variable is missing or falsy, which includes 0, "0", an empty string and an empty array. is_null is true only for null and warns if the variable is undefined. For a default value, the ?? operator is usually clearer than any of them.
How do I prevent SQL injection in PHP?
Use prepared statements, with PDO or mysqli, and pass every value as a parameter rather than putting it into the SQL string. The database then treats the value as data, never as part of the query. Identifiers such as table names and sort direction cannot be parameters, so check those against a fixed list of allowed values.
Should I commit composer.lock?
Yes, for an application. The lock file records the exact version of every package, so composer install gives every developer, CI run and server the same code. For a library that other projects install, committing it is optional, because the projects using your library resolve their own versions and ignore your lock file.
What is the difference between echo and print in PHP?
Very little in practice. Both are language constructs that output a string. echo can take several comma-separated arguments and returns nothing, while print takes one argument and returns 1, so it can be used inside an expression. Most PHP code uses echo, and <?= inside templates is shorthand for it.
What is the difference between an arrow function and an anonymous function?
An arrow function, fn($x) => $x * 2, is a single expression and automatically sees variables from the surrounding scope, by value. An anonymous function, function ($x) use ($rate) { ... }, can hold any number of statements but must list the outer variables it needs in a use clause. Neither can change an outer variable unless you capture it by reference with use (&$var) in an anonymous function.
