02: Modern PHP Syntax for TS Developers
Modern PHP Syntax for TS Developers
Section titled “Modern PHP Syntax for TS Developers”Overview
Section titled “Overview”Modern PHP (8.0+) has adopted many syntax features that TypeScript/JavaScript developers will recognize. This chapter explores the syntactic similarities and differences, helping you write idiomatic PHP using familiar patterns.
Learning Objectives
Section titled “Learning Objectives”By the end of this chapter, you’ll be able to:
- ✅ Use arrow functions in PHP (like TypeScript’s arrow syntax)
- ✅ Apply null coalescing and nullish coalescing operators
- ✅ Use spread operators for arrays
- ✅ Write match expressions (better than switch)
- ✅ Leverage string interpolation
- ✅ Use named arguments (like object parameters in TS)
- ✅ Destructure arrays (PHP’s equivalent to array destructuring)
Code Examples
Section titled “Code Examples”📁 View Code Examples on GitHub
This chapter includes working examples of modern PHP syntax:
- Arrow functions and closures
- Match expressions
- Spread operators and named arguments
- Array methods and TypeScript-style helpers
Run the examples:
git clone https://github.com/dalehurley/codewithphp.gitcd codewithphp/code/php-typescript-developers/chapter-02php arrow-functions.phpArrow Functions
Section titled “Arrow Functions”TypeScript
Section titled “TypeScript”// Arrow function syntaxconst double = (x: number): number => x * 2;
// Multi-line arrow functionconst greet = (name: string): string => { const message = `Hello, ${name}!`; return message;};
// Array methods with arrow functionsconst numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(x => x * 2);const evens = numbers.filter(x => x % 2 === 0);PHP (7.4+)
Section titled “PHP (7.4+)”<?phpdeclare(strict_types=1);
// Arrow function (short syntax - single expression only)$double = fn(int $x): int => $x * 2;
// Multi-line requires traditional closure$greet = function(string $name): string { $message = "Hello, {$name}!"; return $message;};
// Array methods with arrow functions$numbers = [1, 2, 3, 4, 5];$doubled = array_map(fn($x) => $x * 2, $numbers);$evens = array_filter($numbers, fn($x) => $x % 2 === 0);Key Differences:
| Feature | TypeScript | PHP |
|---|---|---|
| Keyword | None | fn |
| Single Expression | ✅ | ✅ |
| Multi-line Body | ✅ | ❌ (use function) |
| Implicit Return | ✅ | ✅ |
| Auto-capture Variables | ✅ | ✅ |
Important: PHP arrow functions (fn) can only have a single expression. For multi-line functions, use traditional closures.
Null Coalescing Operators
Section titled “Null Coalescing Operators”The ?? Operator (Null Coalescing)
Section titled “The ?? Operator (Null Coalescing)”TypeScript:
// Nullish coalescing (??)const name = user.name ?? "Guest";const port = config.port ?? 3000;PHP:
<?php// Null coalescing (??)$name = $user['name'] ?? "Guest";$port = $config['port'] ?? 3000;
// Chain multiple$value = $a ?? $b ?? $c ?? "default";Identical behavior! Returns the right operand if the left is null or undefined.
The ??= Operator (Null Coalescing Assignment)
Section titled “The ??= Operator (Null Coalescing Assignment)”TypeScript:
// Assign only if null/undefinedport ??= 3000;PHP (7.4+):
<?php$port ??= 3000; // Only assigns if $port is null or undefinedThe ?-> Operator (Optional Chaining)
Section titled “The ?-> Operator (Optional Chaining)”TypeScript:
// Optional chainingconst city = user?.address?.city;const firstItem = array?.[0];PHP (8.0+):
<?php// Nullsafe operator$city = $user?->address?->city;$firstItem = $array[0] ?? null; // No ?. for arraysPHP Limitation: No optional chaining for arrays—use ?? instead.
Spread Operator
Section titled “Spread Operator”TypeScript
Section titled “TypeScript”// Array spreadconst arr1 = [1, 2, 3];const arr2 = [4, 5, 6];const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Object spreadconst user = { name: "Alice", age: 30 };const updatedUser = { ...user, age: 31 };
// Function argumentsfunction sum(...numbers: number[]): number { return numbers.reduce((a, b) => a + b, 0);}PHP (7.4+ for arrays, 8.1+ for named args)
Section titled “PHP (7.4+ for arrays, 8.1+ for named args)”<?php// Array spread (7.4+)$arr1 = [1, 2, 3];$arr2 = [4, 5, 6];$combined = [...$arr1, ...$arr2]; // [1, 2, 3, 4, 5, 6]
// Associative array spread (8.1+)$user = ['name' => 'Alice', 'age' => 30];$updatedUser = [...$user, 'age' => 31];
// Function arguments (variadic)function sum(int ...$numbers): int { return array_sum($numbers);}
sum(1, 2, 3, 4); // 10PHP Limitation: Spread in associative arrays requires PHP 8.1+.
Match Expression (Better Switch)
Section titled “Match Expression (Better Switch)”TypeScript Switch
Section titled “TypeScript Switch”function getStatus(code: number): string { switch (code) { case 200: return "OK"; case 404: return "Not Found"; case 500: return "Server Error"; default: return "Unknown"; }}PHP Match (8.0+)
Section titled “PHP Match (8.0+)”<?phpdeclare(strict_types=1);
function getStatus(int $code): string { return match($code) { 200 => "OK", 404 => "Not Found", 500 => "Server Error", default => "Unknown" };}Benefits of Match over Switch:
- ✅ Returns a value (expression, not statement)
- ✅ Strict comparison (
===by default) - ✅ No fall-through (no
breakneeded) - ✅ Exhaustiveness checking
Multiple conditions:
<?php$result = match($status) { 200, 201, 202 => "Success", 400, 404 => "Client Error", 500, 502, 503 => "Server Error", default => "Unknown"};String Interpolation
Section titled “String Interpolation”TypeScript
Section titled “TypeScript”const name = "Alice";const age = 30;
// Template literalsconst message = `Hello, ${name}! You are ${age} years old.`;const multiline = ` Line 1 Line 2 Line 3`;<?php$name = "Alice";$age = 30;
// Double quotes with variable interpolation$message = "Hello, {$name}! You are {$age} years old.";
// Heredoc (multiline)$multiline = <<<EOTLine 1Line 2Line 3EOT;
// Nowdoc (no interpolation, like single quotes)$nowdoc = <<<'EOT'Variables like {$name} are not parsed here.EOT;Key Differences:
| Feature | TypeScript | PHP |
|---|---|---|
| Syntax | `${var}` | "{$var}" or "$var" |
| Quotes | Backticks | Double quotes |
| Multiline | Native in backticks | Heredoc (<<<EOT) |
| Expression | `${1 + 1}` | Not directly (use concatenation) |
Simple variables can omit braces in PHP:
<?php$name = "Alice";echo "Hello, $name!"; // Worksecho "Hello, {$name}!"; // More explicit (recommended)Named Arguments
Section titled “Named Arguments”TypeScript (Object Parameters)
Section titled “TypeScript (Object Parameters)”function createUser({ name, email, age = 18, isActive = true}: { name: string; email: string; age?: number; isActive?: boolean;}): User { return { name, email, age, isActive };}
// Call with named parametersconst user = createUser({ name: "Alice", email: "alice@example.com", age: 30});PHP (8.0+)
Section titled “PHP (8.0+)”<?phpdeclare(strict_types=1);
function createUser( string $name, string $email, int $age = 18, bool $isActive = true): array { return compact('name', 'email', 'age', 'isActive');}
// Call with named arguments$user = createUser( name: "Alice", email: "alice@example.com", age: 30);
// Skip optional parameters$user2 = createUser( name: "Bob", email: "bob@example.com", isActive: false // Skip $age, use default);PHP Named Arguments Benefits:
- ✅ Skip optional parameters
- ✅ Reorder arguments
- ✅ Self-documenting code
Array Destructuring
Section titled “Array Destructuring”TypeScript
Section titled “TypeScript”// Array destructuringconst [a, b, c] = [1, 2, 3];
// Object destructuringconst { name, age } = { name: "Alice", age: 30 };
// Nested destructuringconst { user: { name } } = data;PHP (7.1+)
Section titled “PHP (7.1+)”<?php// Array destructuring[$a, $b, $c] = [1, 2, 3];
// Associative array destructuring['name' => $name, 'age' => $age] = ['name' => 'Alice', 'age' => 30];
// Nested destructuring['user' => ['name' => $name]] = $data;
// Skip elements[, , $third] = [1, 2, 3]; // $third = 3PHP Limitation: No object destructuring—only arrays.
Property Promotion (Constructor Shorthand)
Section titled “Property Promotion (Constructor Shorthand)”TypeScript
Section titled “TypeScript”class User { constructor( public name: string, public email: string, private age: number ) {}}
const user = new User("Alice", "alice@example.com", 30);PHP (8.0+)
Section titled “PHP (8.0+)”<?phpdeclare(strict_types=1);
class User { public function __construct( public string $name, public string $email, private int $age ) {}}
$user = new User("Alice", "alice@example.com", 30);echo $user->name; // "Alice"Identical concept! Properties are automatically declared and assigned.
Trailing Comma Support
Section titled “Trailing Comma Support”TypeScript
Section titled “TypeScript”const arr = [ 1, 2, 3, // ✅ Trailing comma allowed];
function foo( a: number, b: number, // ✅ Trailing comma allowed) {}PHP (7.3+ for arrays, 8.0+ for parameters)
Section titled “PHP (7.3+ for arrays, 8.0+ for parameters)”<?php$arr = [ 1, 2, 3, // ✅ Trailing comma allowed];
function foo( int $a, int $b, // ✅ Trailing comma allowed (PHP 8.0+)): void {}Nullsafe Operator Chain
Section titled “Nullsafe Operator Chain”TypeScript
Section titled “TypeScript”// Optional chainingconst city = user?.profile?.address?.city;PHP (8.0+)
Section titled “PHP (8.0+)”<?php// Nullsafe operator$city = $user?->profile?->address?->city;Identical behavior! Returns null if any property in the chain is null.
First-Class Callable Syntax (PHP 8.1+)
Section titled “First-Class Callable Syntax (PHP 8.1+)”TypeScript
Section titled “TypeScript”class Calculator { add(a: number, b: number): number { return a + b; }}
const calc = new Calculator();const addFn = calc.add.bind(calc);const result = addFn(5, 10); // 15
// Or with arrow functionconst addFn2 = (a: number, b: number) => calc.add(a, b);PHP (8.1+)
Section titled “PHP (8.1+)”<?phpdeclare(strict_types=1);
class Calculator { public function add(int $a, int $b): int { return $a + $b; }}
$calc = new Calculator();
// OLD way (verbose)$addFn = fn($a, $b) => $calc->add($a, $b);
// NEW way (PHP 8.1+): First-class callable$addFn = $calc->add(...);$result = $addFn(5, 10); // 15
// Also works with built-in functions$upperFn = strtoupper(...);echo $upperFn('hello'); // "HELLO"
// Static methods$jsonDecodeFn = json_decode(...);Benefits:
- ✅ Cleaner syntax
- ✅ Works with methods, functions, and static methods
- ✅ Proper type checking
Array Functions Comparison
Section titled “Array Functions Comparison”TypeScript developers coming to PHP often miss familiar array methods. Here’s how they translate:
TypeScript Array Methods
Section titled “TypeScript Array Methods”const numbers = [1, 2, 3, 4, 5];
// mapconst doubled = numbers.map(x => x * 2);
// filterconst evens = numbers.filter(x => x % 2 === 0);
// reduceconst sum = numbers.reduce((acc, x) => acc + x, 0);
// findconst firstEven = numbers.find(x => x % 2 === 0);
// someconst hasEven = numbers.some(x => x % 2 === 0);
// everyconst allPositive = numbers.every(x => x > 0);
// includesconst hasThree = numbers.includes(3);
// forEachnumbers.forEach(x => console.log(x));PHP Array Functions
Section titled “PHP Array Functions”<?php$numbers = [1, 2, 3, 4, 5];
// map$doubled = array_map(fn($x) => $x * 2, $numbers);
// filter$evens = array_filter($numbers, fn($x) => $x % 2 === 0);
// reduce$sum = array_reduce($numbers, fn($acc, $x) => $acc + $x, 0);
// find (first match) - no direct equivalent, use loop or array_filter$firstEven = current(array_filter($numbers, fn($x) => $x % 2 === 0)) ?: null;
// some - no direct equivalent, use loop$hasEven = count(array_filter($numbers, fn($x) => $x % 2 === 0)) > 0;
// every - no direct equivalent$allPositive = count(array_filter($numbers, fn($x) => $x > 0)) === count($numbers);
// includes$hasThree = in_array(3, $numbers, true); // Third param = strict comparison
// forEacharray_walk($numbers, fn($x) => print($x . "\n"));// Or use regular foreach loop (more idiomatic)foreach ($numbers as $x) { echo $x . "\n";}Important Differences:
- PHP’s
array_maphas parameters reversed:array_map(callback, array) - Use
in_array()for checking existence (withstrict: truefor type-safe) - No direct
find(),some(),every()equivalents—usearray_filter()with additional logic - PHP prefers
foreachloops overforEach()method
Modern PHP Array Helper (Custom)
Section titled “Modern PHP Array Helper (Custom)”You can create TypeScript-like helpers:
<?phpdeclare(strict_types=1);
class Arr { /** * @template T * @param array<T> $array * @param callable(T): bool $callback * @return T|null */ public static function find(array $array, callable $callback): mixed { foreach ($array as $item) { if ($callback($item)) { return $item; } } return null; }
/** * @template T * @param array<T> $array * @param callable(T): bool $callback */ public static function some(array $array, callable $callback): bool { foreach ($array as $item) { if ($callback($item)) { return true; } } return false; }
/** * @template T * @param array<T> $array * @param callable(T): bool $callback */ public static function every(array $array, callable $callback): bool { foreach ($array as $item) { if (!$callback($item)) { return false; } } return true; }}
// Usage$numbers = [1, 2, 3, 4, 5];$firstEven = Arr::find($numbers, fn($x) => $x % 2 === 0); // 2$hasEven = Arr::some($numbers, fn($x) => $x % 2 === 0); // true$allPositive = Arr::every($numbers, fn($x) => $x > 0); // trueAttributes vs Decorators
Section titled “Attributes vs Decorators”TypeScript Decorators
Section titled “TypeScript Decorators”// TypeScript decorators (experimental)function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling ${propertyKey} with`, args); return original.apply(this, args); };}
class Calculator { @log add(a: number, b: number): number { return a + b; }}PHP Attributes (8.0+)
Section titled “PHP Attributes (8.0+)”<?phpdeclare(strict_types=1);
// Define attribute#[Attribute]class Route { public function __construct( public string $path, public string $method = 'GET' ) {}}
// Use attributeclass UserController { #[Route('/users', 'GET')] public function index(): array { return ['users' => []]; }
#[Route('/users', 'POST')] public function store(): array { return ['created' => true]; }}
// Read attributes via reflection$reflection = new ReflectionClass(UserController::class);foreach ($reflection->getMethods() as $method) { $attributes = $method->getAttributes(Route::class); foreach ($attributes as $attribute) { $route = $attribute->newInstance(); echo "{$route->method} {$route->path} -> {$method->getName()}\n"; }}Differences:
- TypeScript decorators modify behavior at runtime (when supported)
- PHP attributes are metadata only—they don’t change behavior automatically
- PHP requires reflection to read attributes
- Common PHP use cases: routing, validation, ORM mapping
Looping Constructs
Section titled “Looping Constructs”TypeScript Loops
Section titled “TypeScript Loops”const items = ['a', 'b', 'c'];
// for...of (values)for (const item of items) { console.log(item);}
// for...in (keys)for (const key in items) { console.log(key); // "0", "1", "2"}
// forEachitems.forEach((item, index) => { console.log(index, item);});
// Classic forfor (let i = 0; i < items.length; i++) { console.log(items[i]);}PHP Loops
Section titled “PHP Loops”<?php$items = ['a', 'b', 'c'];
// foreach (most common)foreach ($items as $item) { echo $item . "\n";}
// foreach with indexforeach ($items as $index => $item) { echo "{$index}: {$item}\n";}
// Classic forfor ($i = 0; $i < count($items); $i++) { echo $items[$i] . "\n";}
// while$i = 0;while ($i < count($items)) { echo $items[$i] . "\n"; $i++;}Associative Arrays (like Objects):
// TypeScriptconst user = { name: "Alice", age: 30 };for (const [key, value] of Object.entries(user)) { console.log(`${key}: ${value}`);}<?php// PHP$user = ['name' => 'Alice', 'age' => 30];foreach ($user as $key => $value) { echo "{$key}: {$value}\n";}Union Types in Practice
Section titled “Union Types in Practice”TypeScript
Section titled “TypeScript”function format(value: string | number | boolean): string { if (typeof value === "string") { return value.toUpperCase(); } if (typeof value === "number") { return value.toFixed(2); } return value ? "Yes" : "No";}PHP (8.0+)
Section titled “PHP (8.0+)”<?phpdeclare(strict_types=1);
function format(string|int|float|bool $value): string { return match(true) { is_string($value) => strtoupper($value), is_int($value) || is_float($value) => number_format($value, 2), is_bool($value) => $value ? "Yes" : "No", };}
// Or with if/elseiffunction formatAlt(string|int|float|bool $value): string { if (is_string($value)) { return strtoupper($value); } if (is_int($value) || is_float($value)) { return number_format($value, 2); } return $value ? "Yes" : "No";}Match with Truthiness:
match(true)allows conditional expressions as cases- More elegant than long if/elseif chains
- Exhaustive by default (must handle all cases)
Practical Comparison Example
Section titled “Practical Comparison Example”Let’s build a simple user validator in both languages:
TypeScript
Section titled “TypeScript”interface User { name: string; email: string; age?: number; isActive?: boolean;}
const validateUser = (user: User): string[] => { const errors: string[] = [];
if (!user.name?.trim()) { errors.push("Name is required"); }
if (!user.email?.includes("@")) { errors.push("Invalid email"); }
if ((user.age ?? 0) < 18) { errors.push("Must be 18 or older"); }
return errors;};
// Usageconst user = { name: "Alice", email: "alice@example.com", age: 30 };const errors = validateUser(user);<?phpdeclare(strict_types=1);
class User { public function __construct( public string $name, public string $email, public ?int $age = null, public bool $isActive = true ) {}}
/** * @return array<string> */function validateUser(User $user): array { $errors = [];
if (empty(trim($user->name))) { $errors[] = "Name is required"; }
if (!str_contains($user->email, "@")) { $errors[] = "Invalid email"; }
if (($user->age ?? 0) < 18) { $errors[] = "Must be 18 or older"; }
return $errors;}
// Usage$user = new User("Alice", "alice@example.com", 30);$errors = validateUser($user);Hands-On Exercise
Section titled “Hands-On Exercise”Task 1: Refactor to Modern PHP
Section titled “Task 1: Refactor to Modern PHP”Given this old-style PHP code, refactor it using modern syntax:
<?php// Old stylefunction calculateDiscount($price, $discountPercent = 10, $isMember = false) { if (!isset($price)) { return 0; }
$discount = $isMember ? $discountPercent * 1.5 : $discountPercent;
switch ($discount) { case 10: $label = "Standard"; break; case 15: $label = "Member"; break; default: $label = "Custom"; break; }
return array( 'original' => $price, 'discount' => $discount, 'final' => $price - ($price * $discount / 100), 'label' => $label );}Solution
<?phpdeclare(strict_types=1);
function calculateDiscount( ?float $price, float $discountPercent = 10, bool $isMember = false): array { $price ??= 0; // Null coalescing assignment
// Ternary with clear intent $discount = $isMember ? $discountPercent * 1.5 : $discountPercent;
// Match expression (better than switch) $label = match($discount) { 10 => "Standard", 15 => "Member", default => "Custom" };
// Named array keys return [ 'original' => $price, 'discount' => $discount, 'final' => $price - ($price * $discount / 100), 'label' => $label ];}Modern features used:
- Type declarations
- Null coalescing assignment (
??=) - Match expression
- Short array syntax
[]
Task 2: Convert TypeScript to PHP
Section titled “Task 2: Convert TypeScript to PHP”Convert this TypeScript function to modern PHP:
const processItems = (items: number[]): { sum: number; avg: number } => { const sum = items.reduce((acc, val) => acc + val, 0); const avg = sum / items.length; return { sum, avg };};
const numbers = [1, 2, 3, 4, 5];const result = processItems(numbers);console.log(`Sum: ${result.sum}, Avg: ${result.avg}`);Solution
<?phpdeclare(strict_types=1);
/** * @param array<int> $items */function processItems(array $items): array { $sum = array_reduce( $items, fn($acc, $val) => $acc + $val, 0 ); $avg = $sum / count($items);
return compact('sum', 'avg'); // Or: return ['sum' => $sum, 'avg' => $avg];}
$numbers = [1, 2, 3, 4, 5];$result = processItems($numbers);echo "Sum: {$result['sum']}, Avg: {$result['avg']}" . PHP_EOL;Key translations:
- Arrow function →
fn($acc, $val) => $acc + $val reduce()→array_reduce()length→count()- Template literal → String interpolation with
{}
Key Takeaways
Section titled “Key Takeaways”- Arrow functions (
fn) support only single expressions; use traditional closures for multi-line - Null coalescing (
??,??=) works identically to TypeScript’s nullish coalescing - Nullsafe operator (
?->) is PHP’s version of optional chaining (objects only, not arrays) - Match expressions are superior to switch: return values, strict comparison, no fall-through
- Named arguments (PHP 8.0+) allow skipping optional parameters and reordering
- Property promotion (PHP 8.0+) makes constructors as concise as TypeScript
- String interpolation uses double quotes
"{$var}"instead of backticks`${var}` - Spread operator works for arrays; associative array spread requires PHP 8.1+
- First-class callables (PHP 8.1+): Use
fn(...)syntax for cleaner function references - Array functions differ:
array_map(callback, array)has reversed parameter order vs JS - PHP lacks
array.find(),array.some(),array.every()—usearray_filter()or custom helpers - Attributes (PHP 8.0+) are metadata-only, unlike TypeScript decorators that modify behavior
foreachis idiomatic in PHP; prefer it overarray_walk()for most iterations- Union types work great with
match(true)for elegant type-based logic
Syntax Cheat Sheet
Section titled “Syntax Cheat Sheet”| Feature | TypeScript | PHP |
|---|---|---|
| Arrow function | (x) => x * 2 | fn($x) => $x * 2 |
| Null coalescing | a ?? b | $a ?? $b |
| Optional chain | obj?.prop | $obj?->prop |
| Spread | ...arr | ...$arr |
| Match/Switch | switch (x) {} | match($x) {} |
| String template | `Hello ${x}` | "Hello {$x}" |
| Named args | foo({ x: 1 }) | foo(x: 1) |
| Destructure | [a, b] = arr | [$a, $b] = $arr |
Next Steps
Section titled “Next Steps”Now that you’re familiar with modern PHP syntax, let’s dive deeper into functions and closures.
Next Chapter: 03: Functions & Closures: From JS to PHP
Resources
Section titled “Resources”Questions or feedback? Open an issue on GitHub