Dive deeper into PHP OOP with static members, constants, magic methods, namespaces, and advanced class design patterns
Building on OOP basics, this lesson explores advanced class features that make PHP a powerful object-oriented language. You'll learn about static members that belong to the class rather than instances, constants for immutable values, and magic methods that PHP calls automatically in specific situations.
Namespaces help organize large codebases by preventing naming conflicts and grouping related classes logically. Magic methods like __get, __set, and __call enable dynamic property and method handling, while __toString and __invoke add flexibility to how objects behave. Understanding these advanced features is crucial for working with modern PHP frameworks and writing professional, maintainable code.
This lesson prepares you for real-world PHP development where these patterns are used extensively. You'll see how frameworks leverage these features and learn to apply them appropriately in your own code. By mastering these concepts, you'll be able to read and contribute to professional PHP codebases with confidence.
Static members belong to the class itself rather than individual objects. They're shared across all instances and can be accessed without creating an object.
Static Properties:
Shared data across all instances:
1class Database {
2 private static ?PDO $connection = null;
3 private static int $queryCount = 0;
4
5 public static function connect(string $dsn): void {
6 if (self::$connection === null) {
7 self::$connection = new PDO($dsn);
8 }
9 }
10
11 public static function query(string $sql): PDOStatement {
12 self::$queryCount++;
13 return self::$connection->query($sql);
14 }
15
16 public static function getQueryCount(): int {
17 return self::$queryCount;
18 }
19}
20
21// Access without creating object
22Database::connect('mysql:host=localhost;dbname=test');
23Database::query('SELECT * FROM users');
24echo Database::getQueryCount(); // 1Static Methods:
Utility functions and factory methods:
1class StringHelper {
2 public static function slugify(string $text): string {
3 $text = strtolower($text);
4 $text = preg_replace('/[^a-z0-9]+/', '-', $text);
5 return trim($text, '-');
6 }
7
8 public static function truncate(string $text, int $length): string {
9 if (strlen($text) <= $length) {
10 return $text;
11 }
12 return substr($text, 0, $length) . '...';
13 }
14}
15
16$slug = StringHelper::slugify('Hello World!'); // "hello-world"
17$short = StringHelper::truncate('Long text here', 10); // "Long text..."Factory Pattern with Static Methods:
1class User {
2 private function __construct(
3 private string $name,
4 private string $email,
5 private string $role
6 ) {}
7
8 public static function createRegularUser(
9 string $name,
10 string $email
11 ): self {
12 return new self($name, $email, 'user');
13 }
14
15 public static function createAdmin(
16 string $name,
17 string $email
18 ): self {
19 return new self($name, $email, 'admin');
20 }
21
22 public static function fromArray(array $data): self {
23 return new self(
24 $data['name'],
25 $data['email'],
26 $data['role'] ?? 'user'
27 );
28 }
29}
30
31$user = User::createRegularUser('John', '[email protected]');
32$admin = User::createAdmin('Admin', '[email protected]');
33$fromData = User::fromArray(['name' => 'Jane', 'email' => '[email protected]']);self vs static:
1class ParentClass {
2 protected static string $name = 'Parent';
3
4 public static function getName(): string {
5 return self::$name; // Always returns 'Parent'
6 }
7
8 public static function getNameLate(): string {
9 return static::$name; // Uses late static binding
10 }
11}
12
13class ChildClass extends ParentClass {
14 protected static string $name = 'Child';
15}
16
17echo ParentClass::getName(); // "Parent"
18echo ParentClass::getNameLate(); // "Parent"
19echo ChildClass::getName(); // "Parent" (uses self)
20echo ChildClass::getNameLate(); // "Child" (uses static)When to Use Static:
When NOT to Use Static:
Class constants are immutable values associated with a class. They're useful for configuration, enumeration-like values, and preventing magic strings.
Defining Constants:
1class HttpStatus {
2 public const OK = 200;
3 public const CREATED = 201;
4 public const BAD_REQUEST = 400;
5 public const UNAUTHORIZED = 401;
6 public const NOT_FOUND = 404;
7 public const SERVER_ERROR = 500;
8
9 public static function getMessage(int $code): string {
10 return match($code) {
11 self::OK => 'OK',
12 self::CREATED => 'Created',
13 self::BAD_REQUEST => 'Bad Request',
14 self::UNAUTHORIZED => 'Unauthorized',
15 self::NOT_FOUND => 'Not Found',
16 self::SERVER_ERROR => 'Server Error',
17 default => 'Unknown Status'
18 };
19 }
20}
21
22if ($response->code === HttpStatus::OK) {
23 echo HttpStatus::getMessage(HttpStatus::OK);
24}Visibility Modifiers (PHP 7.1+):
1class Config {
2 public const PUBLIC_KEY = 'public_value';
3 protected const PROTECTED_KEY = 'protected_value';
4 private const PRIVATE_KEY = 'private_value';
5
6 public static function getPrivateKey(): string {
7 return self::PRIVATE_KEY; // Accessible within class
8 }
9}
10
11echo Config::PUBLIC_KEY; // OK
12// echo Config::PRIVATE_KEY; // ERRORTyped Constants (PHP 8.3+):
1class Settings {
2 public const int MAX_USERS = 100;
3 public const float TAX_RATE = 0.08;
4 public const string APP_NAME = 'MyApp';
5 public const array ALLOWED_ROLES = ['admin', 'user', 'guest'];
6}Constants in Practice:
1class Order {
2 public const STATUS_PENDING = 'pending';
3 public const STATUS_PROCESSING = 'processing';
4 public const STATUS_SHIPPED = 'shipped';
5 public const STATUS_DELIVERED = 'delivered';
6 public const STATUS_CANCELLED = 'cancelled';
7
8 private string $status = self::STATUS_PENDING;
9
10 public function process(): void {
11 if ($this->status !== self::STATUS_PENDING) {
12 throw new LogicException("Can only process pending orders");
13 }
14 $this->status = self::STATUS_PROCESSING;
15 }
16
17 public function ship(): void {
18 if ($this->status !== self::STATUS_PROCESSING) {
19 throw new LogicException("Can only ship processing orders");
20 }
21 $this->status = self::STATUS_SHIPPED;
22 }
23
24 public function getStatus(): string {
25 return $this->status;
26 }
27
28 public static function getAllStatuses(): array {
29 return [
30 self::STATUS_PENDING,
31 self::STATUS_PROCESSING,
32 self::STATUS_SHIPPED,
33 self::STATUS_DELIVERED,
34 self::STATUS_CANCELLED
35 ];
36 }
37}Enums (PHP 8.1+) - Better Than Constants:
1enum OrderStatus: string {
2 case PENDING = 'pending';
3 case PROCESSING = 'processing';
4 case SHIPPED = 'shipped';
5 case DELIVERED = 'delivered';
6 case CANCELLED = 'cancelled';
7
8 public function canShip(): bool {
9 return $this === self::PROCESSING;
10 }
11}
12
13class Order {
14 private OrderStatus $status = OrderStatus::PENDING;
15
16 public function ship(): void {
17 if (!$this->status->canShip()) {
18 throw new LogicException("Cannot ship order in this status");
19 }
20 $this->status = OrderStatus::SHIPPED;
21 }
22}Magic methods are special methods that PHP calls automatically in response to certain events. They start with double underscores (__) and add dynamic behavior to classes.
__get() and __set() - Property Overloading:
1class DynamicProperties {
2 private array $data = [];
3
4 public function __get(string $name): mixed {
5 echo "Getting: $name\n";
6 return $this->data[$name] ?? null;
7 }
8
9 public function __set(string $name, mixed $value): void {
10 echo "Setting: $name = $value\n";
11 $this->data[$name] = $value;
12 }
13
14 public function __isset(string $name): bool {
15 return isset($this->data[$name]);
16 }
17
18 public function __unset(string $name): void {
19 unset($this->data[$name]);
20 }
21}
22
23$obj = new DynamicProperties();
24$obj->name = "John"; // Calls __set()
25echo $obj->name; // Calls __get()__call() and __callStatic() - Method Overloading:
1class QueryBuilder {
2 private array $conditions = [];
3
4 public function __call(string $method, array $args): self {
5 if (str_starts_with($method, 'where')) {
6 $field = lcfirst(substr($method, 5)); // whereAge -> age
7 $this->conditions[$field] = $args[0];
8 return $this;
9 }
10
11 throw new BadMethodCallException("Method $method doesn't exist");
12 }
13
14 public function getConditions(): array {
15 return $this->conditions;
16 }
17}
18
19$query = new QueryBuilder();
20$query->whereName('John')
21 ->whereAge(30)
22 ->whereCity('New York');
23
24print_r($query->getConditions());
25// ['name' => 'John', 'age' => 30, 'city' => 'New York']__toString() - String Representation:
1class Money {
2 public function __construct(
3 private float $amount,
4 private string $currency = 'USD'
5 ) {}
6
7 public function __toString(): string {
8 return $this->currency . ' ' . number_format($this->amount, 2);
9 }
10}
11
12$price = new Money(1234.56);
13echo $price; // "USD 1,234.56"
14echo "Price: $price"; // "Price: USD 1,234.56"__invoke() - Callable Objects:
1class Multiplier {
2 public function __construct(private int $factor) {}
3
4 public function __invoke(int $number): int {
5 return $number * $this->factor;
6 }
7}
8
9$double = new Multiplier(2);
10$triple = new Multiplier(3);
11
12echo $double(5); // 10
13echo $triple(5); // 15
14
15// Use as callback
16$numbers = [1, 2, 3, 4, 5];
17$doubled = array_map($double, $numbers);
18// [2, 4, 6, 8, 10]__clone() - Object Cloning:
1class User {
2 private DateTime $createdAt;
3
4 public function __construct(
5 private string $name,
6 private Address $address
7 ) {
8 $this->createdAt = new DateTime();
9 }
10
11 public function __clone() {
12 // Create new DateTime object
13 $this->createdAt = clone $this->createdAt;
14 // Deep clone the address
15 $this->address = clone $this->address;
16 }
17}
18
19$user1 = new User('John', new Address('123 Main St'));
20$user2 = clone $user1; // Calls __clone()__serialize() and __unserialize() (PHP 7.4+):
1class Session {
2 private PDO $connection;
3 private array $data;
4
5 public function __serialize(): array {
6 // Only serialize data, not connection
7 return ['data' => $this->data];
8 }
9
10 public function __unserialize(array $data): void {
11 $this->data = $data['data'];
12 // Reconnect to database
13 $this->connection = new PDO(/* ... */);
14 }
15}All Magic Methods:
Namespaces prevent naming conflicts and organize code into logical groups. Combined with autoloading, they enable clean, maintainable codebases.
Basic Namespaces:
1// File: src/Models/User.php
2namespace App\Models;
3
4class User {
5 private string $name;
6}
7
8// File: src/Services/UserService.php
9namespace App\Services;
10
11use App\Models\User;
12
13class UserService {
14 public function createUser(string $name): User {
15 return new User($name);
16 }
17}Using Classes from Namespaces:
1// Fully qualified name
2$user = new \App\Models\User();
3
4// With use statement
5use App\Models\User;
6$user = new User();
7
8// With alias
9use App\Models\User as UserModel;
10$user = new UserModel();
11
12// Multiple imports
13use App\Models\{User, Post, Comment};Global Namespace:
1namespace App\Database;
2
3class Connection {
4 public function query() {
5 // Access global DateTime
6 $now = new \DateTime();
7
8 // Access global function
9 $result = \json_encode([]);
10 }
11}Autoloading with PSR-4:
1// composer.json
2{
3 "autoload": {
4 "psr-4": {
5 "App\\": "src/"
6 }
7 }
8}
9
10// File structure:
11// src/
12// Models/
13// User.php -> App\Models\User
14// Post.php -> App\Models\Post
15// Services/
16// UserService.php -> App\Services\UserService
17// Controllers/
18// UserController.php -> App\Controllers\UserControllerPractical Example:
1// File: src/Database/Connection.php
2namespace App\Database;
3
4class Connection {
5 private static ?\PDO $instance = null;
6
7 public static function getInstance(): \PDO {
8 if (self::$instance === null) {
9 self::$instance = new \PDO(/* ... */);
10 }
11 return self::$instance;
12 }
13}
14
15// File: src/Models/User.php
16namespace App\Models;
17
18use App\Database\Connection;
19
20class User {
21 public static function find(int $id): ?self {
22 $db = Connection::getInstance();
23 $stmt = $db->prepare('SELECT * FROM users WHERE id = ?');
24 $stmt->execute([$id]);
25
26 $data = $stmt->fetch();
27 if (!$data) {
28 return null;
29 }
30
31 $user = new self();
32 // ... populate user ...
33 return $user;
34 }
35}
36
37// File: index.php
38require 'vendor/autoload.php'; // Composer autoloader
39
40use App\Models\User;
41
42$user = User::find(1);Namespace Best Practices:
Building a Complete Configuration Management System
1<?php
2declare(strict_types=1);
3
4/**
5 * Configuration Management System
6 *
7 * Demonstrates advanced class concepts:
8 * - Static methods for global access
9 * - Constants for configuration keys
10 * - Magic methods for dynamic access
11 * - Singleton pattern
12 * - Type safety
13 */
14
15class Config {
16 private static ?self $instance = null;
17 private array $data = [];
18
19 // Configuration keys as constants
20 public const APP_NAME = 'app.name';
21 public const APP_ENV = 'app.env';
22 public const APP_DEBUG = 'app.debug';
23 public const DB_HOST = 'database.host';
24 public const DB_PORT = 'database.port';
25 public const DB_NAME = 'database.name';
26 public const CACHE_ENABLED = 'cache.enabled';
27 public const CACHE_TTL = 'cache.ttl';
28
29 // Private constructor for singleton
30 private function __construct() {
31 $this->loadDefaults();
32 }
33
34 // Prevent cloning
35 private function __clone() {}
36
37 // Prevent unserialization
38 public function __wakeup() {
39 throw new \Exception("Cannot unserialize singleton");
40 }
41
42 /**
43 * Get singleton instance
44 */
45 public static function getInstance(): self {
46 if (self::$instance === null) {
47 self::$instance = new self();
48 }
49 return self::$instance;
50 }
51
52 /**
53 * Load default configuration
54 */
55 private function loadDefaults(): void {
56 $this->data = [
57 self::APP_NAME => 'MyApp',
58 self::APP_ENV => 'production',
59 self::APP_DEBUG => false,
60 self::DB_HOST => 'localhost',
61 self::DB_PORT => 3306,
62 self::DB_NAME => 'myapp',
63 self::CACHE_ENABLED => true,
64 self::CACHE_TTL => 3600
65 ];
66 }
67
68 /**
69 * Get configuration value
70 */
71 public function get(string $key, mixed $default = null): mixed {
72 return $this->data[$key] ?? $default;
73 }
74
75 /**
76 * Set configuration value
77 */
78 public function set(string $key, mixed $value): void {
79 $this->data[$key] = $value;
80 }
81
82 /**
83 * Check if key exists
84 */
85 public function has(string $key): bool {
86 return isset($this->data[$key]);
87 }
88
89 /**
90 * Get all configuration
91 */
92 public function all(): array {
93 return $this->data;
94 }
95
96 /**
97 * Magic getter for dynamic access
98 */
99 public function __get(string $key): mixed {
100 return $this->get($key);
101 }
102
103 /**
104 * Magic setter for dynamic access
105 */
106 public function __set(string $key, mixed $value): void {
107 $this->set($key, $value);
108 }
109
110 /**
111 * Magic isset
112 */
113 public function __isset(string $key): bool {
114 return $this->has($key);
115 }
116
117 /**
118 * Static helper methods
119 */
120 public static function value(string $key, mixed $default = null): mixed {
121 return self::getInstance()->get($key, $default);
122 }
123
124 public static function setValue(string $key, mixed $value): void {
125 self::getInstance()->set($key, $value);
126 }
127}
128
129class Logger {
130 private static int $logCount = 0;
131 private string $name;
132
133 public const LEVEL_DEBUG = 'DEBUG';
134 public const LEVEL_INFO = 'INFO';
135 public const LEVEL_WARNING = 'WARNING';
136 public const LEVEL_ERROR = 'ERROR';
137
138 public function __construct(string $name) {
139 $this->name = $name;
140 }
141
142 private function log(string $level, string $message): void {
143 self::$logCount++;
144
145 $timestamp = date('Y-m-d H:i:s');
146 $formatted = sprintf(
147 "[%s] [%s] [%s] %s",
148 $timestamp,
149 $level,
150 $this->name,
151 $message
152 );
153
154 echo $formatted . PHP_EOL;
155 }
156
157 public function debug(string $message): void {
158 if (Config::value(Config::APP_DEBUG, false)) {
159 $this->log(self::LEVEL_DEBUG, $message);
160 }
161 }
162
163 public function info(string $message): void {
164 $this->log(self::LEVEL_INFO, $message);
165 }
166
167 public function warning(string $message): void {
168 $this->log(self::LEVEL_WARNING, $message);
169 }
170
171 public function error(string $message): void {
172 $this->log(self::LEVEL_ERROR, $message);
173 }
174
175 public static function getTotalLogs(): int {
176 return self::$logCount;
177 }
178
179 /**
180 * Magic call for dynamic log levels
181 */
182 public function __call(string $method, array $args): void {
183 if (str_starts_with($method, 'log')) {
184 $level = strtoupper(substr($method, 3));
185 $this->log($level, $args[0] ?? '');
186 }
187 }
188}
189
190class Formatter {
191 /**
192 * Format bytes to human readable
193 */
194 public static function bytes(int $bytes): string {
195 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
196 $bytes = max($bytes, 0);
197 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
198 $pow = min($pow, count($units) - 1);
199 $bytes /= (1 << (10 * $pow));
200
201 return round($bytes, 2) . ' ' . $units[$pow];
202 }
203
204 /**
205 * Format duration in seconds
206 */
207 public static function duration(int $seconds): string {
208 $parts = [];
209
210 $days = floor($seconds / 86400);
211 if ($days > 0) {
212 $parts[] = "{$days}d";
213 $seconds %= 86400;
214 }
215
216 $hours = floor($seconds / 3600);
217 if ($hours > 0) {
218 $parts[] = "{$hours}h";
219 $seconds %= 3600;
220 }
221
222 $minutes = floor($seconds / 60);
223 if ($minutes > 0) {
224 $parts[] = "{$minutes}m";
225 $seconds %= 60;
226 }
227
228 if ($seconds > 0 || empty($parts)) {
229 $parts[] = "{$seconds}s";
230 }
231
232 return implode(' ', $parts);
233 }
234
235 /**
236 * Make callable for use in array_map
237 */
238 public function __invoke(mixed $value, string $type = 'bytes'): string {
239 return match($type) {
240 'bytes' => self::bytes((int)$value),
241 'duration' => self::duration((int)$value),
242 default => (string)$value
243 };
244 }
245}
246
247// ==================== DEMONSTRATION ====================
248
249echo str_repeat("=", 60) . PHP_EOL;
250echo " CONFIGURATION & LOGGING SYSTEM" . PHP_EOL;
251echo str_repeat("=", 60) . PHP_EOL . PHP_EOL;
252
253// Configuration usage
254$config = Config::getInstance();
255
256echo "1. Configuration Management:" . PHP_EOL;
257echo " App Name: " . $config->get(Config::APP_NAME) . PHP_EOL;
258echo " Environment: " . $config->get(Config::APP_ENV) . PHP_EOL;
259echo " Debug Mode: " . ($config->get(Config::APP_DEBUG) ? 'Yes' : 'No') . PHP_EOL;
260
261// Static access
262Config::setValue(Config::APP_DEBUG, true);
263echo " Debug after change: " . (Config::value(Config::APP_DEBUG) ? 'Yes' : 'No') . PHP_EOL;
264
265// Logger usage
266echo PHP_EOL . "2. Logging:" . PHP_EOL;
267
268$logger = new Logger('App');
269$logger->info('Application started');
270$logger->debug('Debug information');
271$logger->warning('This is a warning');
272$logger->error('An error occurred');
273
274echo " Total logs: " . Logger::getTotalLogs() . PHP_EOL;
275
276// Formatter usage
277echo PHP_EOL . "3. Formatters:" . PHP_EOL;
278
279$sizes = [1024, 1048576, 1073741824];
280echo " File sizes:" . PHP_EOL;
281foreach ($sizes as $size) {
282 echo " " . Formatter::bytes($size) . PHP_EOL;
283}
284
285$durations = [90, 3665, 90000];
286echo " Durations:" . PHP_EOL;
287foreach ($durations as $duration) {
288 echo " " . Formatter::duration($duration) . PHP_EOL;
289}
290
291// Formatter as callable
292$formatter = new Formatter();
293$formatted = array_map($formatter(...), $sizes);
294echo " Mapped: " . implode(', ', $formatted) . PHP_EOL;
295
296echo PHP_EOL . str_repeat("=", 60) . PHP_EOL;
297echo "✅ Demo Complete!" . PHP_EOL;
298?>This system demonstrates advanced OOP patterns:
Singleton Pattern:
Static Members Usage:
Class Constants:
Magic Methods:
Type Safety:
Real-World Patterns:
Benefits:
Design Principles:
Your progress is saved in this browser, on this device only.