02: Modern PHP: What's Changed

02: Modern PHP: What’s Changed Beginner
Section titled “02: Modern PHP: What’s Changed Beginner”Overview
Section titled “Overview”If your last encounter with PHP was years ago, prepare to be pleasantly surprised. Modern PHP (8.0+) has undergone a dramatic transformation that brings it closer to Ruby in expressiveness while adding powerful features of its own.
This chapter explores how PHP has evolved from a loosely-typed scripting language into a modern, performant language with sophisticated type systems, elegant syntax, and developer-friendly features that Ruby developers will appreciate.
Prerequisites
Section titled “Prerequisites”Before starting this chapter, you should have:
- Completion of Chapter 01: Mapping Concepts: Rails vs Laravel or equivalent understanding of Rails concepts
- Basic familiarity with Ruby syntax and language features
- Estimated Time: ~45-60 minutes
Note: This chapter is reference-heavy and doesn’t require you to write code. You can read through it to understand PHP’s evolution, then refer back to specific sections as needed.
What You’ll Build
Section titled “What You’ll Build”By the end of this chapter, you will have:
- A comprehensive understanding of PHP 8.4’s modern features
- Side-by-side comparisons of Ruby and PHP syntax
- Knowledge of type safety improvements in modern PHP
- Understanding of performance enhancements with JIT compilation
- Mental models for translating Ruby patterns to PHP
- Practical examples showing PHP 8.4 features in action
What You’ll Learn
Section titled “What You’ll Learn”- How PHP 8.x transformed the language
- Modern PHP features that rival Ruby’s elegance
- Type safety improvements and strict typing
- Performance enhancements with JIT compilation
- New syntax features in PHP 8.4 (property hooks, asymmetric visibility)
- PHP 8.3 features (typed constants, JSON validation, dynamic properties deprecation)
- Important limitations: what PHP doesn’t have (generics, pattern matching)
- Why modern PHP feels nothing like old PHP
- Side-by-side Ruby vs PHP comparisons
📦 Code Samples
Section titled “📦 Code Samples”All executable code examples for this chapter are available on GitHub:
- Type Safety Example — Type declarations, strict typing, union types
- Property Hooks Example — PHP 8.4 property hooks, computed properties, lazy loading
- Enums Example — Enums with methods, backing values, exhaustive matching
You can run these examples locally:
# Download all chapter 02 examplesgit clone https://github.com/dalehurley/codewithphp.gitcd codewithphp/code/rails-developers-love-laravel/chapter-02
# Run examplesphp 01-type-safety-example.phpphp 02-property-hooks-example.phpphp 03-enums-example.phpAll examples require PHP 8.4+
The PHP Renaissance
Section titled “The PHP Renaissance”PHP has undergone a renaissance since version 7.0 (2015) and especially with the 8.x series:
PHP 7.0-7.4 (2015-2019):
- Type declarations
- Return types
- Scalar type hints
- Anonymous classes
- Null coalescing operator
- 2x performance improvement
PHP 8.0 (2020):
- JIT compilation
- Named arguments
- Attributes (like Ruby decorators)
- Union types
- Match expressions
- Constructor property promotion
PHP 8.1 (2021):
- Enums
- Readonly properties
- Fibers (like Ruby Fibers)
- First-class callables
PHP 8.2 (2022):
- Readonly classes
- DNF types
- True/false/null standalone types
PHP 8.3 (2023):
- Typed class constants
- Dynamic properties deprecation
- JSON validation
PHP 8.4 (2024):
- Property hooks
- Asymmetric visibility
- New array functions
- HTML5 support
Each version added features that make PHP more elegant, type-safe, and performant.
Type System: From Loose to Strict
Section titled “Type System: From Loose to Strict”The Old Way (PHP 5.x)
Section titled “The Old Way (PHP 5.x)”<?php// Old PHP - no type safetyfunction getUser($id) { $user = User::find($id); if ($user == null) { // Weak comparison return null; } return $user->name;}
// Called with anythinggetUser("hello"); // No error!getUser([1, 2, 3]); // Still no error!This is the PHP that gave the language a bad reputation.
The Modern Way (PHP 8.4)
Section titled “The Modern Way (PHP 8.4)”<?php// Modern PHP - strict typesdeclare(strict_types=1);
function getUser(int $id): ?string{ return User::find($id)?->name;}
// Type errors caught immediatelygetUser("hello"); // TypeError!getUser([1, 2, 3]); // TypeError!Ruby Comparison
Section titled “Ruby Comparison”# Ruby - duck typing with optional type checking (Sorbet/RBS)def get_user(id) User.find(id)&.nameend
# With Sorbet type annotationssig { params(id: Integer).returns(T.nilable(String)) }def get_user(id) User.find(id)&.nameendModern PHP’s type system is more strict by default than Ruby, which can prevent entire classes of bugs.
::: tip Pro Tip: Enabling Strict Types
Always start your PHP files with declare(strict_types=1); for maximum type safety. This catches type errors at call-time instead of silently coercing types.
<?phpdeclare(strict_types=1); // Always add this!
// Now TypeErrors are thrown instead of silent coercionfunction add(int $a, int $b): int { return $a + $b;}
add(5, "10"); // TypeError (good!)// Without strict_types: silently converts "10" to 10 (bad!):::
::: warning Common Gotcha: Strict Types Are File-Scoped
declare(strict_types=1) only affects the file it’s declared in, not the files you call! Each file needs its own declaration.
<?phpdeclare(strict_types=1);
class UserService { public function process(int $id) { ... }}
// File: controller.php// NO strict_types declared here!$service = new UserService();$service->process("5"); // This STILL gets coerced to 5!Solution: Add declare(strict_types=1) to every PHP file, or use PHPStan/Psalm to catch these at build time.
:::
Modern Syntax: Null Safety
Section titled “Modern Syntax: Null Safety”Null Coalescing
Section titled “Null Coalescing”Ruby:
# Rubyname = user.name || "Guest"name = user&.name || "Guest"PHP:
<?php// PHP 7.0+ - Null coalescing operator$name = $user->name ?? "Guest";
// PHP 8.0+ - Null-safe operator$name = $user?->profile?->name ?? "Guest";Nearly identical to Ruby’s safe navigation operator!
Null Coalescing Assignment
Section titled “Null Coalescing Assignment”Ruby:
# Ruby@cache ||= []@cache ||= expensive_operation()PHP:
<?php// PHP 7.4+ - Null coalescing assignment$this->cache ??= [];$this->cache ??= expensive_operation();Same pattern, different syntax.
Constructor Property Promotion
Section titled “Constructor Property Promotion”One of PHP 8.0’s best features eliminates boilerplate:
Before PHP 8.0
Section titled “Before PHP 8.0”<?phpclass User{ private string $name; private string $email; private int $age;
public function __construct(string $name, string $email, int $age) { $this->name = $name; $this->email = $email; $this->age = $age; }}PHP 8.0+
Section titled “PHP 8.0+”<?phpclass User{ public function __construct( private string $name, private string $email, private int $age, ) {}}Much cleaner! Compare to Ruby:
# Rubyclass User attr_reader :name, :email, :age
def initialize(name, email, age) @name = name @email = email @age = age endendPHP’s version is even more concise.
Property Hooks (PHP 8.4)
Section titled “Property Hooks (PHP 8.4)”PHP 8.4 introduced property hooks - a game-changing feature that rivals Ruby’s elegance:
Ruby Properties
Section titled “Ruby Properties”# Ruby - automatic getters/settersclass User attr_accessor :name
# Custom setter with logic def name=(value) @name = value.strip.capitalize end
def name @name endend
user = User.newuser.name = " john "puts user.name # "John"PHP 8.4 Property Hooks
Section titled “PHP 8.4 Property Hooks”<?phpclass User{ // Property hook with automatic backing field public string $name { set(string $value) { $this->name = trim(ucfirst($value)); } }
// Computed property (no backing field) public string $fullName { get => "{$this->firstName} {$this->lastName}"; }
// Asymmetric visibility (new in 8.4!) public private(set) int $id;}
$user = new User();$user->name = " john ";echo $user->name; // "John"echo $user->fullName; // Computed on access
$user->id = 5; // Error! Can only set internallyThis is incredibly elegant and rivals or exceeds Ruby’s property capabilities.
Lazy Loading with Property Hooks
Section titled “Lazy Loading with Property Hooks”<?phpclass Post{ private ?Collection $comments = null;
public Collection $comments { get { $this->comments ??= $this->loadComments(); return $this->comments; } }
private function loadComments(): Collection { return Comment::where('post_id', $this->id)->get(); }}
$post = Post::find(1);// Comments only loaded when accessed$comments = $post->comments;Compare to Ruby:
# Rubyclass Post def comments @comments ||= load_comments end
private
def load_comments Comment.where(post_id: id) endendBoth achieve lazy loading elegantly.
::: tip Pro Tip: Property Hooks vs Traditional Getters/Setters Property hooks are cleaner than traditional get/set methods:
Old Way (verbose):
<?phpclass User { private string $name;
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = trim(ucfirst($name)); }}
$user->setName("john"); // Ugly method callNew Way (elegant):
<?phpclass User { public string $name { set => trim(ucfirst($value)); }}
$user->name = "john"; // Natural property access:::
::: warning Common Gotcha: Infinite Recursion with Property Hooks Be careful not to create infinite loops!
<?phpclass User { public string $name { set(string $value) { // ❌ WRONG - creates infinite recursion! $this->name = strtoupper($value); } }}Why: Setting $this->name inside the set hook calls the hook again!
Solution: Use the backing field or a different property:
<?phpclass User { private string $_name; // Backing field
public string $name { get => $this->_name; set(string $value) { $this->_name = strtoupper($value); // ✅ Correct } }}Or use the shorthand (PHP automatically handles the backing field):
<?phpclass User { public string $name { set => strtoupper($value); // ✅ Works! No recursion }}:::
Enums: Type-Safe Constants
Section titled “Enums: Type-Safe Constants”Ruby Approach
Section titled “Ruby Approach”# Ruby - typically uses symbols or constantsclass Status DRAFT = "draft" PUBLISHED = "published" ARCHIVED = "archived"end
post.status = Status::DRAFT
# Or with gemsclass Status < T::Enum enums do Draft = new Published = new Archived = new endendPHP 8.1+ Enums
Section titled “PHP 8.1+ Enums”<?phpenum Status: string{ case Draft = 'draft'; case Published = 'published'; case Archived = 'archived';
// Enums can have methods! public function color(): string { return match($this) { self::Draft => 'yellow', self::Published => 'green', self::Archived => 'gray', }; }
public function isPublic(): bool { return $this === self::Published; }}
// Usage$status = Status::Published;echo $status->value; // "published"echo $status->color(); // "green"echo $status->isPublic(); // true
// Type safetyfunction setStatus(Status $status): void { // Only valid Status enums accepted}
setStatus(Status::Draft); // ✓setStatus("draft"); // ✗ TypeError!PHP’s enums are first-class language features with full type safety.
Match Expressions: Better Switch
Section titled “Match Expressions: Better Switch”Ruby Case
Section titled “Ruby Case”# Ruby case statementstatus = case role when "admin" "full_access" when "editor" "edit_access" when "viewer" "read_access" else "no_access" endPHP 8.0 Match
Section titled “PHP 8.0 Match”<?php// PHP 8.0+ match expression$status = match($role) { 'admin' => 'full_access', 'editor' => 'edit_access', 'viewer' => 'read_access', default => 'no_access',};
// Match is an expression (returns value)// Strict comparison (===)// No fallthrough// Exhaustive checking with enums
$color = match($status) { Status::Draft => 'yellow', Status::Published => 'green', Status::Archived => 'gray', // Error if any case is missing!};Match expressions are more concise and type-safe than traditional switch statements.
Named Arguments
Section titled “Named Arguments”Ruby Keyword Arguments
Section titled “Ruby Keyword Arguments”# Ruby - keyword argumentsdef create_user(name:, email:, age: 18, active: true) # ...end
create_user( name: "John", email: "john@example.com", age: 25)
# Can reorder argumentscreate_user( email: "john@example.com", name: "John")PHP 8.0+ Named Arguments
Section titled “PHP 8.0+ Named Arguments”<?php// PHP 8.0+ - named argumentsfunction createUser( string $name, string $email, int $age = 18, bool $active = true): User { // ...}
createUser( name: "John", email: "john@example.com", age: 25);
// Can reorder argumentscreateUser( email: "john@example.com", name: "John");
// Skip optional argumentscreateUser( name: "John", email: "john@example.com", active: false // Skip $age, use its default);PHP now has the same flexibility as Ruby’s keyword arguments!
Union Types and Type Safety
Section titled “Union Types and Type Safety”Ruby Type Annotations (with Sorbet)
Section titled “Ruby Type Annotations (with Sorbet)”# Ruby with Sorbetsig { params(id: T.any(Integer, String)).returns(T.nilable(User)) }def find_user(id) User.find(id)endPHP 8.0+ Union Types
Section titled “PHP 8.0+ Union Types”<?php// PHP 8.0+ - union typesfunction findUser(int|string $id): ?User{ return User::find($id);}
// PHP 8.2+ - DNF types (Disjunctive Normal Form)function process((Stringable&JsonSerializable)|string $data): void{ // Accepts: (Stringable AND JsonSerializable) OR string}
// PHP 8.2+ - standalone typesfunction getValue(): true|false|null{ // Explicit true/false/null types}PHP’s union types are built into the language, not a third-party tool.
Attributes: Metadata That Matters
Section titled “Attributes: Metadata That Matters”Ruby Decorators (via gems)
Section titled “Ruby Decorators (via gems)”# Ruby - typically requires gemsclass User < ApplicationRecord # Using custom decorators/concerns include Cacheable
cached(:ttl => 3600) def expensive_operation # ... endendPHP 8.0+ Attributes
Section titled “PHP 8.0+ Attributes”<?php// PHP 8.0+ - built-in attributes#[Route('/users/{id}', methods: ['GET'])]#[Cache(ttl: 3600)]#[Authorize(role: 'admin')]class UserController{ #[Validate(['id' => 'integer|min:1'])] public function show(int $id): JsonResponse { // Attributes processed by framework return response()->json(User::find($id)); }}
// Laravel uses attributes extensively#[AsController]class PostController extends Controller{ #[Get('/posts')] public function index(): View { return view('posts.index'); }}Attributes provide clean, declarative metadata without decorators or mixins.
Arrow Functions and Closures
Section titled “Arrow Functions and Closures”Ruby Blocks and Lambdas
Section titled “Ruby Blocks and Lambdas”# Ruby - multiple syntaxesnumbers = [1, 2, 3, 4, 5]
# Block syntaxdoubled = numbers.map { |n| n * 2 }
# Lambda/procmultiply = ->(x, y) { x * y }result = multiply.call(3, 4)
# Block with do/endnumbers.each do |n| puts nendPHP Arrow Functions
Section titled “PHP Arrow Functions”<?php// PHP 7.4+ - arrow functions (short closures)$numbers = [1, 2, 3, 4, 5];
// Arrow function - auto-captures variables$doubled = array_map(fn($n) => $n * 2, $numbers);
$multiplier = 3;$tripled = array_map(fn($n) => $n * $multiplier, $numbers);
// PHP 8.0+ - traditional closure with shorthand$multiply = fn($x, $y) => $x * $y;
// Multi-line closure$process = function($n) use ($multiplier) { echo $n * $multiplier; return $n * $multiplier;};Arrow functions make PHP feel more functional, like Ruby.
First-Class Callables
Section titled “First-Class Callables”Ruby Method References
Section titled “Ruby Method References”# Ruby - symbols as method referencesnumbers = ["1", "2", "3"]integers = numbers.map(&:to_i)
# Method objectsmethod = User.method(:find)user = method.call(1)PHP 8.1+ First-Class Callables
Section titled “PHP 8.1+ First-Class Callables”<?php// PHP 8.1+ - first-class callable syntax$numbers = ["1", "2", "3"];$integers = array_map(intval(...), $numbers);
// Method references$finder = User::find(...);$user = $finder(1);
// Instance methods$user = new User();$getName = $user->getName(...);echo $getName(); // Calls $user->getName()
// Static methods$validator = Validator::make(...);$validator(['email' => 'required']);The ... syntax creates clean callable references.
Collections and Functional Programming
Section titled “Collections and Functional Programming”Ruby Enumerables
Section titled “Ruby Enumerables”# Ruby - rich enumerable methodsusers = User.all
result = users .select { |u| u.active? } .map { |u| u.name.upcase } .sort .take(10)
# Ruby's lazy enumerablesresult = (1..Float::INFINITY) .lazy .select { |n| n.even? } .take(10) .to_aLaravel Collections
Section titled “Laravel Collections”<?php// Laravel Collections - Ruby-inspired$users = User::all();
$result = $users ->filter(fn($u) => $u->active) ->map(fn($u) => strtoupper($u->name)) ->sort() ->take(10);
// Lazy collections (PHP 8.x)$result = LazyCollection::make(function() { yield from range(1, INF);}) ->filter(fn($n) => $n % 2 === 0) ->take(10) ->all();
// Rich collection methods (150+ methods!)$collection = collect([1, 2, 3, 4, 5]);
$collection->sum();$collection->avg();$collection->chunk(2);$collection->groupBy('status');$collection->pluck('name', 'id');$collection->pipe(fn($c) => $c->sum() * 2);Laravel’s Collections API is directly inspired by Ruby’s Enumerable.
Performance: JIT Compilation
Section titled “Performance: JIT Compilation”One area where modern PHP pulls ahead is performance.
PHP 8.0+ JIT Compiler
Section titled “PHP 8.0+ JIT Compiler”PHP 8.0 introduced Just-In-Time compilation:
<?php// JIT automatically optimizes hot code pathsfunction fibonacci(int $n): int{ if ($n <= 1) return $n; return fibonacci($n - 1) + fibonacci($n - 2);}
// JIT identifies this as hot code and compiles to machine codefor ($i = 0; $i < 100000; $i++) { fibonacci(20);}Benchmarks (approximate):
- PHP 7.4 (no JIT): 100ms
- PHP 8.4 (with JIT): 30-40ms
- Ruby 3.3 (YJIT): 80-100ms
PHP’s JIT provides 2-3x performance improvement in CPU-intensive tasks.
Real-World Performance
Section titled “Real-World Performance”Web Request Handling:
- PHP 8.4: ~15,000 req/sec (simple JSON response)
- Ruby 3.3 + Rails: ~3,000-5,000 req/sec
Database Operations:
- Similar performance (database is bottleneck)
JSON Parsing:
- PHP: Significantly faster (native C implementation)
- Ruby: Slower but acceptable for most use cases
PHP’s performance advantage matters for:
- High-traffic applications
- API-heavy workloads
- Real-time processing
- Cost optimization (fewer servers needed)
::: tip Pro Tip: Enabling JIT in Production
JIT is disabled by default. Enable it in php.ini:
; Recommended for productionopcache.enable=1opcache.jit_buffer_size=100Mopcache.jit=1255 ; tracing mode (best for web apps)JIT Modes:
1255(tracing) - Best for web applications (default recommendation)1205(function) - Best for CPU-intensive scripts0(disabled) - Debugging/development
Test it:
php -d opcache.jit=1255 -d opcache.jit_buffer_size=100M your-script.php:::
::: warning Common Gotcha: JIT Doesn’t Help Everything JIT primarily helps CPU-intensive code, not I/O-bound operations:
JIT Helps:
- ✅ Complex calculations
- ✅ String manipulation loops
- ✅ Array operations
- ✅ Recursive algorithms
JIT Doesn’t Help Much:
- ❌ Database queries (I/O bound)
- ❌ API calls (network bound)
- ❌ File operations (I/O bound)
- ❌ Most web requests (already fast)
Reality: For typical Laravel web apps, JIT provides 5-15% improvement, not the 2-3x you see in benchmarks. Still worth enabling! :::
Fibers: Lightweight Concurrency
Section titled “Fibers: Lightweight Concurrency”Ruby Fibers
Section titled “Ruby Fibers”# Ruby Fiberfiber = Fiber.new do puts "Fiber started" Fiber.yield "intermediate result" puts "Fiber resumed" "final result"end
puts fiber.resume # "intermediate result"puts fiber.resume # "final result"PHP 8.1+ Fibers
Section titled “PHP 8.1+ Fibers”<?php// PHP 8.1+ Fiber$fiber = new Fiber(function(): void { echo "Fiber started\n"; Fiber::suspend("intermediate result"); echo "Fiber resumed\n";});
echo $fiber->start(); // "intermediate result"echo $fiber->resume("final result");Both languages support cooperative multitasking with similar APIs.
Readonly Classes
Section titled “Readonly Classes”Ruby Frozen Objects
Section titled “Ruby Frozen Objects”# Ruby - freeze objectsclass Point attr_reader :x, :y
def initialize(x, y) @x = x @y = y freeze # Make immutable endend
point = Point.new(10, 20)point.x = 30 # Error! Frozen objectPHP 8.2+ Readonly Classes
Section titled “PHP 8.2+ Readonly Classes”<?php// PHP 8.2+ - readonly classesreadonly class Point{ public function __construct( public int $x, public int $y, ) {}}
$point = new Point(10, 20);$point->x = 30; // Error! Readonly property
// All properties are automatically readonly// Can only be initialized in constructorPHP’s readonly classes provide compile-time immutability guarantees.
Array Improvements
Section titled “Array Improvements”Ruby Arrays
Section titled “Ruby Arrays”# Ruby arraysarr = [1, 2, 3, 4, 5]
arr.first # 1arr.last # 5arr.first(3) # [1, 2, 3]arr.last(2) # [4, 5]
# Functional operationsarr.any? { |n| n > 3 }arr.all? { |n| n > 0 }arr.find { |n| n > 3 }PHP 8.4 Array Functions
Section titled “PHP 8.4 Array Functions”<?php// PHP 8.4+ - new array functions$arr = [1, 2, 3, 4, 5];
array_first($arr); // 1array_last($arr); // 5array_find($arr, fn($n) => $n > 3); // 4array_find_key($arr, fn($n) => $n > 3); // 3array_any($arr, fn($n) => $n > 3); // truearray_all($arr, fn($n) => $n > 0); // true
// Previous versions required Collection or custom functionsPHP is catching up to Ruby’s array convenience methods!
PHP 8.3 Features: Typed Constants and JSON Validation
Section titled “PHP 8.3 Features: Typed Constants and JSON Validation”Typed Class Constants
Section titled “Typed Class Constants”PHP 8.3 introduced typed class constants, providing better type safety:
Before PHP 8.3:
<?phpclass Status{ const DRAFT = 'draft'; // No type checking const PUBLISHED = 'published';}PHP 8.3+:
<?phpclass Status{ public const string DRAFT = 'draft'; public const string PUBLISHED = 'published'; public const int MAX_LENGTH = 255;}
// Type safety enforcedfunction setStatus(string $status): void{ // Only string constants accepted}Compare to Ruby:
# Ruby - constants are always strings/symbolsclass Status DRAFT = "draft" PUBLISHED = "published"endDynamic Properties Deprecation
Section titled “Dynamic Properties Deprecation”Important for Ruby Developers: PHP 8.3 deprecated dynamic properties (creating properties on the fly), which Ruby allows freely.
Ruby (always allowed):
# Ruby - dynamic properties always workuser = User.newuser.custom_field = "value" # Works!PHP 8.3+ (deprecated):
<?php$user = new User();$user->customField = "value"; // Deprecated warning in 8.3, error in 9.0!Solution: Use #[AllowDynamicProperties] attribute or explicitly declare properties:
<?php#[AllowDynamicProperties]class User{ // Now dynamic properties work}
// Or better: declare properties explicitlyclass User{ public ?string $customField = null; // Explicit property}This is a breaking change for Ruby developers who expect dynamic properties to work!
JSON Validation
Section titled “JSON Validation”PHP 8.3 added json_validate() for validating JSON without parsing:
<?php// PHP 8.3+ - validate JSON without parsing$json = '{"name": "John", "age": 30}';
if (json_validate($json)) { // JSON is valid, safe to parse $data = json_decode($json);} else { // Invalid JSON throw new InvalidArgumentException('Invalid JSON');}
// More efficient than json_decode() + check for errorsPHP 8.4: HTML5 Support
Section titled “PHP 8.4: HTML5 Support”PHP 8.4 added better HTML5 support in the DOM extension:
<?php// PHP 8.4+ - improved HTML5 parsing$html = '<!DOCTYPE html><html><body><section><article>Content</article></section></body></html>';
$dom = new DOMDocument();$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
// Better handling of HTML5 semantic elements$articles = $dom->getElementsByTagName('article');This is particularly useful for web scraping and HTML processing, though most Laravel applications use Blade templates instead.
Important Limitations: What PHP Doesn’t Have
Section titled “Important Limitations: What PHP Doesn’t Have”No Generics (Yet)
Section titled “No Generics (Yet)”Unlike Ruby (with Sorbet/RBS) or languages like TypeScript, PHP doesn’t have generics:
Ruby with Sorbet:
# Ruby with Sorbet - genericssig { params(items: T::Array[User]).returns(T::Array[String]) }def get_names(items) items.map(&:name)endPHP (no generics):
<?php// PHP - no generics, must use docblocks or union types/** * @template T * @param array<T> $items * @return array<string> */function getNames(array $items): array{ // Type information lost at runtime return array_map(fn($item) => $item->name, $items);}
// Or use union types (limited)function getNames(array $items): array{ // Less type-safe than generics return array_map(fn($item) => $item->name, $items);}Workaround: Use PHPStan or Psalm for static analysis with generics support, or wait for PHP 9.0+ (generics are being discussed).
No Pattern Matching (Yet)
Section titled “No Pattern Matching (Yet)”Ruby has pattern matching (introduced in 2.7), but PHP doesn’t:
Ruby:
# Ruby - pattern matchingcase userin {role: "admin", active: true} "Full access"in {role: "editor"} "Edit access"else "No access"endPHP (use match instead):
<?php// PHP - use match expression (simpler but less powerful)$access = match($user->role) { 'admin' => $user->active ? 'Full access' : 'No access', 'editor' => 'Edit access', default => 'No access',};Comparing Language Evolution
Section titled “Comparing Language Evolution”Ruby’s Strengths
Section titled “Ruby’s Strengths”What Ruby Still Does Better:
- More elegant syntax (no semicolons, less punctuation)
- Better metaprogramming (method_missing, define_method)
- More mature block syntax
- Cleaner module system
- Better REPL (IRB)
Ruby Example:
# Ruby's elegant syntaxclass User < ApplicationRecord has_many :posts
scope :active, -> { where(active: true) }
def full_name "#{first_name} #{last_name}" endend
user = User.find(1)puts user.full_name if user.active?PHP’s Strengths
Section titled “PHP’s Strengths”What PHP Does Better:
- Stricter type system (compile-time checking)
- Better performance (JIT compilation)
- Simpler deployment (no application server)
- Ubiquitous hosting support
- Lower memory usage
- Property hooks (8.4)
- First-class enums
PHP Example:
<?phpdeclare(strict_types=1);
class User extends Model{ public function posts(): HasMany { return $this->hasMany(Post::class); }
public function scopeActive(Builder $query): void { $query->where('active', true); }
// Property hook (8.4) public string $fullName { get => "{$this->firstName} {$this->lastName}"; }}
$user = User::find(1);if ($user->active) { echo $user->fullName;}Both achieve the same result with different trade-offs.
Modern PHP Developer Experience
Section titled “Modern PHP Developer Experience”Package Management
Section titled “Package Management”Ruby:
# Gemfilegem 'rails', '~> 7.0'gem 'pg'
bundle installPHP:
# composer.json"require": { "laravel/framework": "^12.0", "doctrine/dbal": "^3.0"}
composer installComposer and Bundler are nearly identical in functionality.
Interactive Console
Section titled “Interactive Console”Ruby:
rails console
> user = User.first> user.posts.countPHP:
php artisan tinker
>>> $user = User::first()>>> $user->posts->count()Laravel’s Tinker provides the same REPL experience.
Code Generation
Section titled “Code Generation”Ruby:
rails generate model User name:string email:stringrails generate controller Users index showPHP:
php artisan make:model User -mphp artisan make:controller UserController --resourceSame philosophy, different commands.
The Bottom Line
Section titled “The Bottom Line”Modern PHP (8.4) is a fundamentally different language than PHP 5.x:
What Changed
Section titled “What Changed”✅ Type Safety: Strict types, union types, generics-like features ✅ Performance: JIT compilation, 2-3x faster ✅ Syntax: Null safety, arrow functions, match expressions ✅ OOP: Constructor promotion, property hooks, readonly ✅ Functional: First-class callables, collections API ✅ Tooling: Better IDEs, static analysis, great ecosystem
What’s the Same
Section titled “What’s the Same”❌ Syntax: Still uses $, ->, semicolons
❌ Legacy: Backwards compatibility can be messy
❌ Reputation: PHP’s bad reputation persists
Practical Example: Side-by-Side
Section titled “Practical Example: Side-by-Side”Let’s build a simple User service in both languages:
Ruby Version
Section titled “Ruby Version”class UserService attr_reader :repository, :mailer
def initialize(repository:, mailer:) @repository = repository @mailer = mailer end
def create_user(name:, email:, role: :user) user = repository.create( name: name.strip, email: email.downcase, role: role )
mailer.send_welcome(user) if user.persisted?
user end
def active_users repository .where(active: true) .order(created_at: :desc) endendPHP 8.4 Version
Section titled “PHP 8.4 Version”<?phpdeclare(strict_types=1);
class UserService{ public function __construct( private UserRepository $repository, private Mailer $mailer, ) {}
public function createUser( string $name, string $email, Role $role = Role::User, ): User { $user = $this->repository->create([ 'name' => trim($name), 'email' => strtolower($email), 'role' => $role, ]);
if ($user->exists) { $this->mailer->sendWelcome($user); }
return $user; }
public function activeUsers(): Collection { return $this->repository ->where('active', true) ->orderBy('created_at', 'desc') ->get(); }}Key Differences:
- PHP has explicit type declarations
- PHP uses enums for role (type-safe)
- PHP has constructor promotion (less boilerplate)
- Both achieve the same result
- PHP catches more errors at compile-time
Should Ruby Developers Learn Modern PHP?
Section titled “Should Ruby Developers Learn Modern PHP?”Yes, if you want to:
- ✅ Expand your toolkit with a faster, type-safe language
- ✅ Work with Laravel (excellent framework)
- ✅ Take advantage of ubiquitous PHP hosting
- ✅ Reduce infrastructure costs with better performance
- ✅ Learn from a different approach to web development
No, if you:
- ❌ Strongly prefer Ruby’s syntax aesthetics
- ❌ Are perfectly productive with Rails
- ❌ Don’t want to learn new patterns
- ❌ Have no business reason to switch
The Reality: Learning modern PHP and Laravel makes you a better developer by exposing you to different solutions to the same problems. Many patterns transfer back to Ruby!
Practice Exercises
Section titled “Practice Exercises”Exercise 1: Type Safety
Section titled “Exercise 1: Type Safety”Convert this Ruby code to modern PHP with full type safety:
def calculate_total(items, discount = 0, tax_rate = 0.1) subtotal = items.sum { |item| item[:price] * item[:quantity] } after_discount = subtotal * (1 - discount) after_discount * (1 + tax_rate)endSolution
<?phpdeclare(strict_types=1);
function calculateTotal( array $items, float $discount = 0.0, float $taxRate = 0.1,): float { $subtotal = array_reduce( $items, fn($sum, $item) => $sum + ($item['price'] * $item['quantity']), 0.0 );
$afterDiscount = $subtotal * (1 - $discount); return $afterDiscount * (1 + $taxRate);}
// Or with PHP 8.4 array functionsfunction calculateTotal( array $items, float $discount = 0.0, float $taxRate = 0.1,): float { $subtotal = collect($items) ->sum(fn($item) => $item['price'] * $item['quantity']);
$afterDiscount = $subtotal * (1 - $discount); return $afterDiscount * (1 + $taxRate);}Exercise 2: Property Hooks
Section titled “Exercise 2: Property Hooks”Create a User class with property hooks that:
- Capitalizes name on set
- Lowercases email on set
- Has a computed
fullNameproperty
Solution
<?phpclass User{ public string $firstName { set(string $value) { $this->firstName = ucfirst(strtolower(trim($value))); } }
public string $lastName { set(string $value) { $this->lastName = ucfirst(strtolower(trim($value))); } }
public string $email { set(string $value) { $this->email = strtolower(trim($value)); } }
public string $fullName { get => "{$this->firstName} {$this->lastName}"; }}
$user = new User();$user->firstName = " JOHN ";$user->lastName = " DOE ";$user->email = " John.Doe@EXAMPLE.com ";
echo $user->fullName; // "John Doe"echo $user->email; // "john.doe@example.com"Exercise 3: Enums with Methods
Section titled “Exercise 3: Enums with Methods”Create a Status enum with colors and a method to check if public:
Solution
<?phpenum Status: string{ case Draft = 'draft'; case Published = 'published'; case Archived = 'archived';
public function color(): string { return match($this) { self::Draft => 'yellow', self::Published => 'green', self::Archived => 'gray', }; }
public function isPublic(): bool { return $this === self::Published; }
public static function fromString(string $value): self { return match(strtolower($value)) { 'draft' => self::Draft, 'published' => self::Published, 'archived' => self::Archived, default => throw new \ValueError("Invalid status: $value"), }; }}
// Usage$status = Status::Published;echo $status->color(); // "green"echo $status->isPublic(); // true
$status2 = Status::fromString('draft');echo $status2->value; // "draft"Wrap-up
Section titled “Wrap-up”Congratulations! You’ve completed a comprehensive tour of modern PHP. Here’s what you’ve accomplished:
✓ Understood PHP’s Evolution - From PHP 5.x to PHP 8.4’s modern features ✓ Compared Ruby and PHP - Side-by-side syntax comparisons for every major feature ✓ Learned Type Safety - How strict types, union types, and enums work in PHP ✓ Explored Modern Syntax - Property hooks, match expressions, named arguments, and more ✓ Discovered Performance - JIT compilation and real-world performance benefits ✓ Practiced Translation - Converted Ruby code to modern PHP in exercises ✓ Built Mental Models - Understand how to think in PHP when coming from Ruby
Key Takeaways
Section titled “Key Takeaways”- Modern PHP is Nothing Like Old PHP - PHP 8.4 is a modern, type-safe language
- Type Safety Built-In - Strict types catch errors at compile-time
- Performance Matters - JIT compilation provides 2-3x speed improvements
- Ruby-Inspired Features - Collections, conventions, developer happiness
- Unique Innovations - Property hooks, enums, attributes go beyond Ruby
- Deployment Simplicity - No application server needed
- Cost Effective - Better performance = fewer servers = lower costs
What’s Next?
Section titled “What’s Next?”Now that you understand modern PHP’s capabilities, we’ll dive into Laravel’s developer experience in the next chapter. You’ll discover how Laravel takes these modern PHP features and builds an incredibly productive framework on top of them.
::: tip Continue Learning Move on to Chapter 03: Laravel Developer Experience to explore Artisan, Tinker, and Laravel’s productivity tools. :::
Further Reading
Section titled “Further Reading”- PHP 8.4 Release Notes — Official PHP 8.4 release announcement and feature list
- PHP The Right Way — Modern PHP best practices, coding standards, and community resources
- PHP Type System Documentation — Comprehensive guide to PHP’s type system
- PHP Attributes RFC — Technical details on PHP 8.0 attributes
- PHP JIT Compilation — Configuration and optimization guide for JIT
- PSR Standards — PHP Framework Interop Group standards (PSR-1, PSR-12 for coding standards)
- Laravel Collections Documentation — Laravel’s collection API (Ruby Enumerable-inspired)