Encapsulation means an object keeps its data private and only lets the outside world change it through methods it controls.
๐งA cash machine doesn't let you reach in and grab notes. You use its buttons (public methods), and it checks your PIN and balance first. The cash drawer (private data) stays locked inside.
Objects that can't be broken from outside
Valid data, always
If every change goes through a method, that method can refuse bad values like a negative price or an empty title.
Change the inside freely
Callers only know the public methods. You can rename or restructure private details without breaking anyone.
A clear, small surface
A class with three public methods is easy to learn. One with twenty public properties is a guessing game.
Three levels of visibility
private and open up only when you need to.| Keyword | Same class | Child class | Outside code |
|---|---|---|---|
public | โ | โ | โ |
protected | โ | โ | โ |
private | โ | โ | โ |
Lock down a class, one step at a time
We'll protect a Post so it can never end up with a bad title or a negative view count.
-
See the problem with all-public data
When properties are
public, any code anywhere can put nonsense in them, and the class can't stop it.class Post { public function __construct( public string $title, public int $views = 0, ) {} } $post = new Post('Hello'); $post->title = ''; // empty title: allowed ๐ฌ $post->views = -500; // negative views: allowed ๐ฌ -
Make data private and expose methods
Mark properties
private. Then add public methods that describe what callers can do (addView()) rather than handing out raw access.class Post { public function __construct( private string $title, private int $views = 0, ) {} public function title(): string { return $this->title; } public function addView(): void { $this->views++; } public function views(): int { return $this->views; } } $post = new Post('Hello'); $post->addView(); echo $post->views(); // 1 $post->views = 99; // Error: Cannot modify private propertyThere's no way to set
viewsto an arbitrary number any more. It can only go up by one. -
Guard the rules inside the class
An invariant is a rule that must always be true for an object, like "a title is never empty". Check it in the constructor and in every method that changes the data. Throw an exception when the rule is broken.
class Post { public function __construct(private string $title) { $this->rename($title); } public function rename(string $title): void { if (trim($title) === '') { throw new InvalidArgumentException('A post needs a title.'); } $this->title = trim($title); } } new Post(' '); // InvalidArgumentException: A post needs a title. -
Hide helper methods with
privateMethods can be private too. Use them for internal steps that callers shouldn't call directly. The public method stays short and readable.
public function slug(): string { return $this->slugify($this->title); } private function slugify(string $text): string { return strtolower(preg_replace('/[^a-z0-9]+/i', '-', trim($text))); } -
Make data unchangeable with
readonlySometimes data should be set once and never change, like an ID or an email in a message. A readonly property (PHP 8.1+) can be read publicly but written only once, from inside the class. PHP 8.2 adds the readonly class, which makes every property readonly.
final readonly class Money { public function __construct( public int $amount, // in cents public string $currency, ) {} public function add(Money $other): static { return new static($this->amount + $other->amount, $this->currency); } } $price = new Money(1999, 'USD'); echo $price->amount; // 1999, reading is fine $price->amount = 0; // Error: Cannot modify readonly property $total = $price->add(new Money(500, 'USD')); // a NEW objectโน๏ธ Immutable objectsAn object that never changes after creation is called immutable. Instead of editing it, you create a new one, as
add()does. Laravel'sCarbondates have an immutable version for the same reason.finalstops other classes from extending it (more on that in Inheritance).
Avoid this, do that
public function setStatus(string $s): void { $this->status = $s; }
public function setPublishedAt($d): void { $this->publishedAt = $d; }
$post->setStatus('published');
$post->setPublishedAt(new DateTime());
A setter for every property is public data in disguise, and callers must remember to do both steps.
public function publish(): void
{
$this->status = PostStatus::Published;
$this->publishedAt = new DateTimeImmutable();
}
$post->publish();
Name methods after real actions. The class keeps related data in sync by itself.
Default to private. It's easy to make something public later, but once outside code depends on a public property, taking it away breaks things.
- Build a
BankAccountclass with a privateint $balance(in cents) starting at 0. - Add
deposit(int $amount)andwithdraw(int $amount). ThrowInvalidArgumentExceptionfor amounts of zero or less. - Make
withdrawthrow if the balance would go negative. - Add a public
balance(): intgetter, and confirm that$account->balance = 1000;fails. - Bonus: create a
readonly class Transactionholding the amount and type, and keep a private list of them.
- Mass assignment protection: a model's
$fillablelist (aprotectedproperty) decides which attributes can be set from user input (see SQL Injection & Mass Assignment). - Hidden attributes:
protected $hidden = ['password'];keeps secrets out of JSON. - Injected dependencies are usually
private readonly:public function __construct(private readonly PostService $posts) {}. - DTOs (data transfer objects) are often
readonlyclasses (see Actions & DTOs).
Key takeaways
- Encapsulation = private data + public methods that control access.
publicis open to all,protectedto the class and its children,privateto the class only.- Enforce invariants in the constructor and in every method that changes data.
- Prefer action methods (
publish()) over blind setters. readonlyproperties and classes give you immutable objects.