In one sentence

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.

Why it matters

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.

The big picture

Three levels of visibility

๐ŸŒpublicAnyone can use it: other classes, your routes, your views.
๐Ÿ‘จโ€๐Ÿ‘งprotectedThis class and its child classes (see Inheritance).
๐Ÿ”’privateOnly code inside this exact class.
Every property, method and constant gets one of these keywords. Start with private and open up only when you need to.
KeywordSame classChild classOutside code
publicโœ…โœ…โœ…
protectedโœ…โœ…โŒ
privateโœ…โŒโŒ
Step by step

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.

  1. 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 ๐Ÿ˜ฌ
            
  2. 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 property
            

    There's no way to set views to an arbitrary number any more. It can only go up by one.

  3. 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.
            
  4. Hide helper methods with private

    Methods 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)));
              }
            
  5. Make data unchangeable with readonly

    Sometimes 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 objects

    An object that never changes after creation is called immutable. Instead of editing it, you create a new one, as add() does. Laravel's Carbon dates have an immutable version for the same reason. final stops other classes from extending it (more on that in Inheritance).

Common mistakes

Avoid this, do that

โŒ Avoid

          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.

โœ… Do

          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.

๐Ÿ’ก Pro tip: start private

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.

๐Ÿ‹๏ธ Try it yourself
  1. Build a BankAccount class with a private int $balance (in cents) starting at 0.
  2. Add deposit(int $amount) and withdraw(int $amount). Throw InvalidArgumentException for amounts of zero or less.
  3. Make withdraw throw if the balance would go negative.
  4. Add a public balance(): int getter, and confirm that $account->balance = 1000; fails.
  5. Bonus: create a readonly class Transaction holding the amount and type, and keep a private list of them.
โ„น๏ธ Where you'll see this in Laravel
  • Mass assignment protection: a model's $fillable list (a protected property) 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 readonly classes (see Actions & DTOs).
Recap

Key takeaways

Check yourself

Mini quiz