Programming languages cheat sheetPHP logo

PHP cheat sheet

PHP syntax on one page, grouped by what you are trying to do: strings, arrays, classes, match, PDO queries and Composer, checked against PHP 8.5.

Last updated

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.

Running PHP

TaskCommand
Check which version you havephp -v
Run a scriptphp script.php
Open an interactive shellphp -a
Run one line without a filephp -r 'echo PHP_VERSION;'
Serve the current folder on localhostphp -S localhost:8000
Check a file for syntax errorsphp -l file.php
List loaded extensionsphp -m
Find which php.ini is in usephp --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

TaskCodeNotes
Start a PHP file<?phpLeave off the closing ?> in files that are only PHP
Turn on strict type checksdeclare(strict_types=1);First statement in the file
Print somethingecho "Hello";
Short echo inside HTML<?= $name ?>
Assign a variable$name = "Ada";Every variable starts with $
Define a constantconst MAX_USERS = 100;
Comment// one line # one line /* block */
Dump a value while debuggingvar_dump($value);
Get a value's type as a stringget_debug_type($value)PHP 8.0+
Is it set and not nullisset($value)
Is it empty, falsy or unsetempty($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?->cityPHP 8.0+
Compare value and type$a === $bPrefer this to ==
Compare for sorting$a <=> $bReturns -1, 0 or 1
Cast to a type(int) "42"Also (string), (float), (bool), (array)
Whole-number divisionintdiv(7, 2)7 / 2 is 3.5
Raise to a power2 ** 10
Readable big numbers1_000_000PHP 7.4+

Strings

TaskCodeNotes
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 bytesstrlen($s)strlen("café") is 5
Length in charactersmb_strlen($s)mb_strlen("café") is 4
Change casestrtolower($s) strtoupper($s) ucfirst($s)
Strip whitespace from both endstrim($s)ltrim and rtrim for one end
Does it contain a substringstr_contains($s, "cat")PHP 8.0+
Does it start or end withstr_starts_with($url, "https")str_ends_with too. PHP 8.0+
Position of a substringstrpos($s, "cat")Returns false if missing and 0 at the start
Replace textstr_replace("cat", "dog", $s)
Part of a stringsubstr($s, 0, 5)mb_substr for multibyte text
Split into an arrayexplode(",", $csv)
Join an array into a stringimplode(", ", $names)
Pad to a lengthstr_pad($n, 3, "0", STR_PAD_LEFT)7 becomes 007
Repeatstr_repeat("-", 20)
Format with placeholderssprintf("%s has %d items", $name, $count)
Format a numbernumber_format(1234.5, 2)Returns 1,234.50
Match a patternpreg_match('/\d+/', $s, $matches)Returns 1, 0 or false
Replace by patternpreg_replace('/\s+/', ' ', $s)
Escape for HTML outputhtmlspecialchars($s)Do this to anything a user typed
Encode to JSONjson_encode($data)
Decode JSON into arraysjson_decode($json, true)Without true you get objects
Decode JSON and throw on bad inputjson_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.

TaskCodeNotes
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 keyunset($user["age"]);Leaves a gap in integer keys
Count itemscount($nums)
Does a key existarray_key_exists("name", $user)isset() is false when the value is null
Does a value existin_array(3, $nums, true)Pass true for strict comparison
Find the key of a valuearray_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 listarray_is_list($arr)PHP 8.1+
Loop with keys and valuesforeach ($user as $key => $value) { }

Array functions

TaskCodeNotes
Transform every valuearray_map(fn($n) => $n * 2, $nums)Callback comes first
Keep values that pass a testarray_filter($nums, fn($n) => $n > 1)Keeps the original keys
Drop empty and falsy valuesarray_filter($values)
Fold into one valuearray_reduce($nums, fn($carry, $n) => $carry + $n, 0)
Add them uparray_sum($nums)
First value that passes a testarray_find($users, fn($u) => $u->active)PHP 8.4+. array_any and array_all too
First or last valuearray_first($nums) array_last($nums)PHP 8.5+. null when empty
First keyarray_key_first($arr)PHP 7.3+
Renumber keys from 0array_values($arr)
Just the keysarray_keys($user)
Merge, renumbering integer keysarray_merge($a, $b)Later string keys win
Merge, keeping the left side's keys$a + $bEarlier keys win
A slicearray_slice($nums, 1, 2)Offset, then length
Remove or insert in placearray_splice($nums, 1, 1)
Push and pop at the endarray_push($nums, 5) array_pop($nums)
Shift and unshift at the startarray_shift($nums) array_unshift($nums, 0)
Remove duplicatesarray_unique($nums)
Values in both arraysarray_intersect($a, $b)
Values in the first but not the secondarray_diff($a, $b)
Pull one field out of each rowarray_column($rows, "email")
Index rows by a fieldarray_column($rows, null, "id")
Two lists into keys and valuesarray_combine($keys, $values)
Swap keys and valuesarray_flip($arr)
Split into chunksarray_chunk($nums, 2)
A range of numbersrange(1, 10)
Fill with a valuearray_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.

TaskCodeKeeps keys?
Sort values, ascendingsort($nums)No, renumbers
Sort values, descendingrsort($nums)No, renumbers
Sort by value, keeping keysasort($prices)Yes
Sort by value descending, keeping keysarsort($prices)Yes
Sort by keyksort($user)Yes
Sort by key, descendingkrsort($user)Yes
Sort with your own comparisonusort($users, fn($a, $b) => $a->age <=> $b->age)No, renumbers
Your own comparison, keeping keysuasort($users, fn($a, $b) => $a->age <=> $b->age)Yes
Your own comparison on keysuksort($arr, fn($a, $b) => strcmp($a, $b))Yes
Human order: img2 before img10natsort($files)Yes
Shuffleshuffle($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

TaskCodeNotes
If, else if, elseif ($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 resultmatch ($code) { 200, 201 => "OK", 404 => "Not found", default => "Error" }PHP 8.0+. Strict comparison
Match on conditionsmatch (true) { $age < 13 => "child", $age < 18 => "teen", default => "adult" }PHP 8.0+
Switchswitch ($x) { case 1: ...; break; default: ...; }Loose comparison, falls through without break
Loop over an arrayforeach ($items as $item) { }
Loop and change each itemforeach ($nums as &$n) { $n *= 2; } unset($n);Always unset the reference after
Counting loopfor ($i = 0; $i < 10; $i++) { }
While loopwhile ($row = $stmt->fetch()) { }
Run at least oncedo { } while ($retry);
Skip to the next iterationcontinue;
Leave two nested loopsbreak 2;
Catch an exceptiontry { } catch (RuntimeException $e) { } finally { }
Catch more than one typecatch (TypeError | ValueError $e)
Catch without using the variablecatch (JsonException) { }PHP 8.0+
Throwthrow new InvalidArgumentException("Bad id");
Throw as an expression$id = $input["id"] ?? throw new InvalidArgumentException("Missing id");PHP 8.0+

Functions and arrow functions

TaskCodeNotes
Define a typed functionfunction add(int $a, int $b): int { return $a + $b; }
Default parameterfunction greet(string $name = "world") { }
Parameter or return that may be nullfunction find(?int $id): ?User { }
Accept more than one typefunction load(int|string $id) { }PHP 8.0+
Return nothingfunction logMessage(string $msg): void { }
Never returns (always throws or exits)function fail(): never { throw new Exception(); }PHP 8.1+
Any number of argumentsfunction sum(int ...$nums): int { }
Spread an array into argumentssum(...$numbers)
Call with named argumentshtmlspecialchars($s, double_encode: false)PHP 8.0+. Skip the ones you do not need
Change the caller's variablefunction addOne(array &$list) { }Pass by reference
Anonymous function$double = function ($n) { return $n * 2; };
Anonymous function using an outer variablefunction ($n) use ($rate) { return $n * $rate; }use copies the value in
Arrow functionfn($n) => $n * $ratePHP 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 callsstatic $count = 0;
Check a function existsfunction_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

TaskCodeNotes
Constructor that declares propertiespublic function __construct(private int $id) {}PHP 8.0+
Create an object$user = new User(1, "Ada");
Call a method straight after newnew Money(5)->format()PHP 8.4+. Wrap in brackets before that
Property or method$user->name $user->save()
Static method or propertyUser::find(1) self::$count
Class constantconst ROLE = "admin"; self::ROLE
Call the parent's versionparent::__construct($id);
Class name as a stringUser::class
Inherit from a classclass Admin extends User { }
Declare an interfaceinterface Shape { public function area(): float; }
Implement interfacesclass Circle implements Shape, JsonSerializable { }
Class that cannot be created directlyabstract class Model { abstract public function table(): string; }
Class that cannot be extendedfinal class Money { }
Share methods without inheritancetrait Timestamps { } use Timestamps;use goes inside the class
Property that is set oncepublic readonly int $id;PHP 8.1+
Every property read-onlyreadonly class Point { }PHP 8.2+
Public to read, private to writepublic private(set) string $name;PHP 8.4+
Backed enumenum Status: string { case Active = "active"; }PHP 8.1+
Enum from a stored valueStatus::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 propertiesreturn clone($this, ["x" => 5]);PHP 8.5+. The with-er pattern for readonly classes
Turn into a stringpublic 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.

TaskCodeNotes
Declare the file's namespacenamespace App\Models;First statement after declare
Import a classuse App\Models\User;
Import under another nameuse App\Models\User as Account;
Import several from one namespaceuse App\Models\{User, Post};
Import a functionuse function App\Support\slugify;
Import a constantuse const App\Support\VERSION;
Reach a global class from inside a namespacenew \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 wantCodeNotes
Query string value, ?page=2$_GET["page"] ?? 1
Form field from a POST$_POST["email"] ?? ""
Validated integer from the query stringfilter_input(INPUT_GET, "page", FILTER_VALIDATE_INT)false if invalid, null if missing
Validated email from a formfilter_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 bodyjson_decode(file_get_contents("php://input"), true)$_POST stays empty for JSON
A cookie$_COOKIE["theme"] ?? "light"
Set a cookiesetcookie("theme", "dark", ["expires" => time() + 86400, "httponly" => true, "samesite" => "Lax"]);Before any output
Start or resume a sessionsession_start();Before any output
Store in the session$_SESSION["user_id"] = $user->id;
New session id after loginsession_regenerate_id(true);Prevents session fixation
An uploaded file$_FILES["avatar"]["tmp_name"]Check ["error"] === UPLOAD_ERR_OK first
Keep an uploaded filemove_uploaded_file($_FILES["avatar"]["tmp_name"], $dest)
An environment variablegetenv("DATABASE_URL")$_ENV is often empty, depending on php.ini
Send a header or redirectheader("Location: /login"); exit;Before any output
Set the response statushttp_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.

TaskCodeNotes
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 errorsPDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTIONThe default since PHP 8.0
Return rows as keyed arraysPDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOCPass 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/.

TaskCommand
Start a composer.json interactivelycomposer init
Add a packagecomposer require monolog/monolog
Add a development-only packagecomposer require --dev phpunit/phpunit
Install exactly what the lock file sayscomposer install
Install for productioncomposer install --no-dev --optimize-autoloader
Upgrade everything within constraintscomposer update
Upgrade one packagecomposer update monolog/monolog
Remove a packagecomposer remove monolog/monolog
See which packages have newer versionscomposer outdated --direct
Check for known security advisoriescomposer audit
Why is this package installedcomposer why psr/log
Rebuild the autoloader after changing itcomposer dump-autoload
Run a script from composer.jsoncomposer run test
Start a new project from a templatecomposer create-project laravel/laravel my-app
Check composer.json is validcomposer validate
Load everything in your coderequire __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.99

Money 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.com

A 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 rightWhat actually happensDo this instead
if (strpos($s, 'a'))False when the match is at position 0str_contains($s, 'a'), or compare !== false
$a == $b'1' == '01' and null == false are both true===
strlen('café')5, because it counts bytesmb_strlen() for characters
array_filter($list) then json_encodeGaps in the keys make it a JSON object, not an arrayWrap it in array_values()
foreach ($a as &$v) then another loop using $vThe second loop overwrites the last elementunset($v) straight after the first loop
isset($data['key'])False when the key exists but holds nullarray_key_exists('key', $data)
array_merge($a, $b) on integer keysKeys are renumbered from 0$a + $b to keep them
$sorted = sort($list)$sorted is true, the list was sorted in placeCall sort($list), then use $list
'Hello $name'Prints $name literallyDouble quotes, or sprintf()
Echoing before header() or session_start()"Headers already sent" warning, and nothing is setSend 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.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.