Every PHP developer has encountered the session variable thatâs set in one file, modified in another, and debugged in a third. Or the class where every property is public and every method is accessible from anywhere. This code works, but itâs fragile. A single change can ripple through the entire application in unpredictable ways.
Encapsulation is the remedy. It bundles data with the methods that operate on it, restricts direct access to internal state, and exposes only whatâs necessary through a controlled interface. The result is code thatâs easier to understand, safer to change, and simpler to debug.
What Youâll Learn
- The four principles of encapsulation in PHP
- Access modifiers: public, private, and protected in practice
- Building an encapsulated user authentication system from scratch
- How encapsulation improves code readability and reduces bugs
- Balancing encapsulation with flexibility
Understanding Encapsulation
Encapsulation means hiding the internal state of an object and requiring all interaction to occur through an objectâs methods. This protects the objectâs integrity by preventing external code from putting it into an invalid state.
The Four Principles
Data hiding: Properties should be private by default. External code shouldnât know or care about internal data structures.
Controlled access: Public methods provide a controlled interface for interacting with the objectâs data. Validation, transformation, and business rules live in these methods.
Single responsibility: Each class encapsulates one concern. Changes to that concern happen in one place.
Abstraction via interface: The public methods form the objectâs contract. The implementation can change without affecting callers.
Access Modifiers in PHP
PHP provides three access modifiers that enforce encapsulation:
<?php
class BankAccount
{
private float $balance = 0;
public function deposit(float $amount): void
{
if ($amount > 0) {
$this->balance += $amount;
}
}
public function getBalance(): float
{
return $this->balance;
}
}- Public: Accessible from anywhere. Use for the classâs interface.
- Private: Accessible only within the defining class. Use for internal state.
- Protected: Accessible within the class and its subclasses. Use when you specifically intend inheritance.
Building an Encapsulated System
Letâs build a user authentication system progressively, starting from a non-encapsulated approach to a fully encapsulated one.
The Problem: Session-Based User Management
<?php
session_start();
$_SESSION['user_id'] = 1;
$_SESSION['user_name'] = 'Chris';
$_SESSION['user_email'] = '[email protected]';
$_SESSION['user_role'] = 'admin';
// Later in the code
$_SESSION['user_role'] = 'user';
// Retrieve user data
$userId = $_SESSION['user_id'];
$userName = $_SESSION['user_name'];
// Delete user data
unset($_SESSION['user_id']);This works but has no protection. Any code can modify any session variable. Thereâs no validation, no type safety, and no single point of control.
Step 1: Basic Class Encapsulation
<?php
session_start();
class User
{
private ?int $userId = null;
private ?string $userName = null;
private ?string $userEmail = null;
private ?string $userRole = null;
public function __construct()
{
if (isset($_SESSION['user_id'])) {
$this->userId = $_SESSION['user_id'];
$this->userName = $_SESSION['user_name'];
$this->userEmail = $_SESSION['user_email'];
$this->userRole = $_SESSION['user_role'];
}
}
public function login(
int $id,
string $name,
string $email,
string $role
): void {
$_SESSION['user_id'] = $id;
$_SESSION['user_name'] = $name;
$_SESSION['user_email'] = $email;
$_SESSION['user_role'] = $role;
$this->userId = $id;
$this->userName = $name;
$this->userEmail = $email;
$this->userRole = $role;
}
public function logout(): void
{
unset(
$_SESSION['user_id'],
$_SESSION['user_name'],
$_SESSION['user_email'],
$_SESSION['user_role']
);
$this->userId = null;
$this->userName = null;
$this->userEmail = null;
$this->userRole = null;
}
public function getUserId(): ?int { return $this->userId; }
public function getUserName(): ?string { return $this->userName; }
public function getUserEmail(): ?string { return $this->userEmail; }
public function getUserRole(): ?string { return $this->userRole; }
}
$user = new User();
if (!$user->getUserId()) {
$user->login(1, 'Chris', '[email protected]', 'admin');
}
echo 'User ID: ' . $user->getUserId() . "\n";
echo 'User Name: ' . $user->getUserName() . "\n";
$user->logout();Now all user data flows through the User class. Properties are private. Access is controlled through methods. But the class still manually manages session state.
Step 2: Separate Session Encapsulation
Extract session management into its own class:
<?php
class Session
{
public function __construct()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
public function set(string $key, mixed $value): void
{
$_SESSION[$key] = $value;
}
public function get(string $key): mixed
{
return $_SESSION[$key] ?? null;
}
public function remove(string $key): void
{
unset($_SESSION[$key]);
}
public function destroy(): void
{
session_destroy();
}
}
class User
{
private ?int $userId = null;
private ?string $userName = null;
private ?string $userEmail = null;
private ?string $userRole = null;
public function __construct(private Session $session)
{
if ($session->get('user_id')) {
$this->userId = $session->get('user_id');
$this->userName = $session->get('user_name');
$this->userEmail = $session->get('user_email');
$this->userRole = $session->get('user_role');
}
}
public function login(
int $id,
string $name,
string $email,
string $role
): void {
$this->session->set('user_id', $id);
$this->session->set('user_name', $name);
$this->session->set('user_email', $email);
$this->session->set('user_role', $role);
$this->userId = $id;
$this->userName = $name;
$this->userEmail = $email;
$this->userRole = $role;
}
public function logout(): void
{
$this->session->remove('user_id');
$this->session->remove('user_name');
$this->session->remove('user_email');
$this->session->remove('user_role');
$this->userId = null;
$this->userName = null;
$this->userEmail = null;
$this->userRole = null;
}
public function getUserId(): ?int { return $this->userId; }
public function getUserName(): ?string { return $this->userName; }
public function getUserEmail(): ?string { return $this->userEmail; }
public function getUserRole(): ?string { return $this->userRole; }
}
$session = new Session();
$user = new User($session);
if (!$user->getUserId()) {
$user->login(1, 'Chris', '[email protected]', 'admin');
}Now Session handles its own concern (session state management) and User handles its own concern (user data and behavior). The Session class encapsulates the $_SESSION superglobal, preventing direct manipulation.
Step 3: Authentication as a Separate Concern
The final level of encapsulation separates authentication logic from user data:
<?php
class Authentication
{
public function __construct(private Session $session) {}
public function login(
int $id,
string $name,
string $email,
string $role
): void {
$this->session->set('user_id', $id);
$this->session->set('user_name', $name);
$this->session->set('user_email', $email);
$this->session->set('user_role', $role);
}
public function logout(): void
{
$this->session->remove('user_id');
$this->session->remove('user_name');
$this->session->remove('user_email');
$this->session->remove('user_role');
}
}
class User
{
private ?int $userId = null;
private ?string $userName = null;
private ?string $userEmail = null;
private ?string $userRole = null;
public function __construct(private Session $session)
{
$this->userId = $session->get('user_id');
$this->userName = $session->get('user_name');
$this->userEmail = $session->get('user_email');
$this->userRole = $session->get('user_role');
}
public function getUserId(): ?int { return $this->userId; }
public function getUserName(): ?string { return $this->userName; }
public function getUserEmail(): ?string { return $this->userEmail; }
public function getUserRole(): ?string { return $this->userRole; }
public function isLoggedIn(): bool
{
return $this->userId !== null;
}
}
$session = new Session();
$authentication = new Authentication($session);
$user = new User($session);
if (!$user->isLoggedIn()) {
$authentication->login(1, 'Chris', '[email protected]', 'admin');
}
echo 'User ID: ' . $user->getUserId() . "\n";
$authentication->logout();
$session->destroy();Now three classes handle three distinct concerns:
Sessionencapsulates PHP session managementAuthenticationencapsulates login/logout logicUserencapsulates user data access
This is more code, but each class has one reason to change. Need to switch from $_SESSION to Redis? Change only Session. Need to add two-factor authentication? Modify only Authentication. Need to add user preferences? Extend only User.
Encapsulation in Practice: A Practical Example
Without Encapsulation
<?php
class Calculator
{
public float $result = 0;
public function add(float $value): void
{
$this->result += $value;
}
public function subtract(float $value): void
{
$this->result -= $value;
}
}
$calc = new Calculator();
$calc->result = 100; // Direct manipulation bypasses logic
$calc->add(50);
echo $calc->result; // 150The public $result property allows external code to bypass the add and subtract methods, potentially putting the calculator into an inconsistent state.
With Encapsulation
<?php
class Calculator
{
private float $result = 0;
public function add(float $value): void
{
$this->result += $value;
}
public function subtract(float $value): void
{
$this->result -= $value;
}
public function getResult(): float
{
return $this->result;
}
public function reset(): void
{
$this->result = 0;
}
}
$calc = new Calculator();
$calc->add(50);
$calc->subtract(20);
echo $calc->getResult(); // 30The private $result property can only be modified through the classâs methods. This guarantees that the calculatorâs state evolves through defined operations.
Real-World Use Cases
- E-commerce cart systems: Encapsulate cart items, totals, and discount calculations behind a clean interface
- User permission systems: Store role and permission data privately, expose
can()orhasPermission()methods - Payment processing: Encapsulate sensitive payment data and expose only transaction results
- ORM entities: Keep database fields private with public getters and setters that enforce business rules
- Configuration managers: Encapsulate config sources (files, env, database) behind a unified interface
Best Practices
- Default to private: Start with the most restrictive access and relax only when needed
- Use getters and setters: Even if they just return or set a value, they provide a stable interface for future changes
- Prefer immutability: Readonly properties (PHP 8.1+) prevent modification after construction
- Hide implementation details: The calling code shouldnât know whether data comes from a database, API, or cache
- Test through the interface: Write tests against public methods, not private internals
Common Mistakes to Avoid
- Making everything public for convenience: Short-term ease creates long-term maintenance problems
- Creating getters and setters for every property: Only expose what external code actually needs
- Using protected when private suffices: Protected creates a broader contract (subclasses) thatâs harder to change
- Letting encapsulation prevent optimization: Sometimes a public property is the right choice for performance-critical code
- Over-encapsulating value objects: Simple data transfer objects donât need elaborate getter/setter patterns
Frequently Asked Questions
Whatâs the difference between encapsulation and abstraction? Encapsulation bundles data with methods and restricts access. Abstraction hides complexity behind a simplified interface. They work together but are distinct concepts.
Should all properties have getters and setters? No. Only expose whatâs needed. If a property is internal state that external code shouldnât access, keep it truly private.
Can encapsulation hurt performance? Rarely. Method call overhead is negligible in modern PHP. The maintainability benefits far outweigh micro-performance concerns.
How do I test private methods? Donât. Test through public methods that use the private logic. If a private method is complex enough to warrant direct testing, consider extracting it into its own class.
Does PHP support readonly classes? Yes, PHP 8.2 introduced readonly classes, making all properties readonly automatically. This is a powerful tool for value objects and DTOs.
Is encapsulation the same as data hiding? Data hiding is a key aspect of encapsulation, but encapsulation is broader: it includes bundling related data and behavior together.
How do I balance encapsulation with flexibility? Start strict and relax as needed. Itâs easier to make a private property public than to make a public property private (because external code may depend on it).
Conclusion
Encapsulation is PHPâs most important tool for writing maintainable code. It creates clear boundaries between components, prevents accidental misuse, and makes change predictable. Well-encapsulated code communicates its intent through its interface and protects its integrity through controlled access.
The progression from session variables to encapsulated classes demonstrates the pattern: identify the concern, encapsulate the data, expose only whatâs needed, and keep everything else private.
Your future self â the one debugging at 2 AM â will thank you for every private property and every thoughtful interface.