---
title: 'Classes and Objects'
source: 'https://academia.sh/en/courses/php-object-model/classes-and-objects'
course: 'Object-Oriented PHP and Standards'
language: en
updated: '2026-10-21T05:03:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows the property, method and constructor forms of a class and that objects carry separate state'
    - 'Classifies whether a definition change reaches its consumers with a before and after run'
    - 'Measures that renaming a private property does not affect the consumer while renaming a public one does'
    - 'Verifies that a consumer writing to a public property does not stop when the name changes and only triggers an E_DEPRECATED notice'
---

# Classes and Objects

What the code that uses a class experiences when a name inside the class changes: a private property, a public property, and a consumer that writes to a public property; the four reaches that measure where a change goes.

The previous course measured PHP's language inside a single file: when the type of a value changes, through which channel an error is heard. This course steps outside the file. In a real program the definitions live in one file, the code that uses them lives in others, and one day a definition changes. The question is this: where does this change reach? The course asks it of every mechanism with the same method and answers with one of four names.

**In place:** the change stayed inside the definition, and the consumers gave the same result without being edited. **At the seam:** the change was absorbed at a single seam, a loader or a setup file. **Breaking:** the change reached the consumers and stopped them; the exit code is not 0. **Implicit:** the change reached the consumers, they ran, but the result changed. Breaking and implicit are different from the channel distinction in PHP Fundamentals; there the question was through which channel a problem is heard, while here the results of two runs are compared. What Systems Programming with C measured was different too: there it was a change in data, here it is a change in a definition. Twelve changes will be measured over the course.

## Class, Property and Method

A class is the mold of objects. It contains properties and methods. A property is the data the object carries, a method is the work the object can do. `new` builds an object, an instance, from the class, and every instance carries its own property values. Inside a method, the object reaches itself with `$this`. The class below also shows this course's file name rule: the name of a class file is the same as the name of the class, classes are written in `StudlyCaps`, and methods and properties in `camelCase`.

```php
<?php
// Account.php — a class that carries a property, a method and a constructor
class Account
{
    public function __construct(private int $balance = 0) {}

    public function deposit(int $amount): void
    {
        $this->balance += $amount;
    }

    public function balance(): int
    {
        return $this->balance;
    }
}
```

`__construct` is the constructor: it runs on the `new` call. The `private` keyword in front of the parameter makes the parameter a property at the same time; a separate property declaration and an assignment line are not needed. `private` is a visibility: only the class's own methods can reach the property. This is encapsulation. The class opens methods to the outside, not data.

```php
<?php
// account_usage.php — two objects carry two separate states; a private property cannot be reached from outside
require 'Account.php';

$first = new Account(10);
$second = new Account();
$first->deposit(50);
$second->deposit(5);
echo $first->balance(), ' ', $second->balance(), "\n";

try {
    echo $first->balance;
} catch (Error $error) {
    echo get_class($error), "\n";
}
```

```
60 5
Error
```

**OM1.** Two objects were built from the same class and the deposited amounts did not mix: `60` and `5`. The line that reached the private property without a method threw `Error`. The only surface open to the consumer code is the methods; the name of the property is not part of that surface. This observation is the reason the measurement below comes out in place.


## A Property in the Constructor, readonly and the Three Visibilities

Writing a visibility in front of a constructor parameter (constructor promotion) is a shorthand for the long form: the property declaration and the `$this->balance = $balance` assignment are made automatically. The two forms produce the same object.

```php
<?php
// Equivalent.php — the long form and the short form build the same object
class LongForm
{
    private int $balance;

    public function __construct(int $balance = 0)
    {
        $this->balance = $balance;
    }

    public function balance(): int
    {
        return $this->balance;
    }
}

class ShortForm
{
    public function __construct(private int $balance = 0) {}

    public function balance(): int
    {
        return $this->balance;
    }
}

var_dump((new LongForm(7))->balance() === (new ShortForm(7))->balance());
```

```
bool(true)
```

If a property will not change once it is built, `readonly` is written: the value is assigned once in the constructor and cannot be changed from anywhere afterwards. Reading stays open, writing causes an `Error`.

```php
<?php
// Fixed.php — a readonly property is assigned once
class Fixed
{
    public function __construct(public readonly int $number) {}
}

$fixed = new Fixed(4);
echo $fixed->number, "\n";

try {
    $fixed->number = 5;
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
4
Error
```

There are three levels of visibility. `public` is reachable from anywhere, `protected` from inside the class and its subclasses, `private` only from inside the class where it is defined. In the class below all three are read from the inside, while from the outside only `public` can be read.

```php
<?php
// Visibility.php — three visibilities from inside and from outside
class Base
{
    public int $a = 1;
    protected int $b = 2;
    private int $c = 3;

    public function read(): string
    {
        return $this->a . $this->b . $this->c;
    }
}

class Child extends Base
{
    public function peek(): string
    {
        return $this->a . $this->b;
    }
}

$object = new Child();
echo $object->read(), ' ', $object->peek(), "\n";

$base = new Base();
foreach (['a', 'b', 'c'] as $name) {
    try {
        echo $base->$name, ' ';
    } catch (Error $e) {
        echo get_class($e), ' ';
    }
}
echo "\n";
```

```
123 12
1 Error Error
```

`Child` read the `protected` `b` from its own inside but could not reach the `private` `c`; that is why `peek()` joined only `a` and `b`. Looking at a `Base` object from the outside, only `a` could be read. The visibility of a property decides who the code that depends on it can be made of.

## The Measurement Core

Where a change reaches is measured with two runs: with the old state of the definition and with the new one. The course leaves this job to the file `reach.php`. The file stays the same in every lesson and is set up once.

```php
<?php
// reach.php — helpers that measure where a change reaches

function write_files(string $dir, array $files): void
{
    foreach ($files as $name => $content) {
        if ($content === null) {
            continue; // null: the file was deleted
        }
        $path = "$dir/$name";
        if (!is_dir(dirname($path))) {
            mkdir(dirname($path), 0777, true);
        }
        file_put_contents($path, $content);
    }
}

function read_files(string $dir): array
{
    $files = [];
    $walker = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($walker as $file) {
        $name = substr($file->getPathname(), strlen($dir) + 1);
        $files[$name] = file_get_contents($file->getPathname());
    }
    return $files;
}

function run(string $dir): array
{
    $command = 'cd ' . escapeshellarg($dir) . ' && php -d display_errors=0 main.php 2>/dev/null';
    exec($command, $lines, $code);
    return [$code, implode("\n", $lines)];
}

function measure(string $name, array $base, array $change, array $seams = []): string
{
    if (array_key_exists('main.php', $change)) {
        throw new LogicException("$name: the consumer is not edited");
    }
    write_files("measure/$name/before", $base);
    write_files("measure/$name/after", array_merge($base, $change));
    [$codeBefore, $outputBefore] = run("measure/$name/before");
    [$codeAfter, $outputAfter] = run("measure/$name/after");
    if ($codeBefore !== 0) {
        throw new LogicException("$name: the baseline did not run");
    }
    $reach = match (true) {
        $codeAfter !== 0 => 'breaking',
        $outputBefore !== $outputAfter => 'implicit',
        array_intersect(array_keys($change), $seams) !== [] => 'at-seam',
        default => 'in-place',
    };
    printf(
        "%-18s files=%d  exit=%-3d output %-7s -> %s\n",
        $name,
        count($change),
        $codeAfter,
        $outputBefore === $outputAfter ? 'same' : 'changed',
        $reach
    );
    return $reach;
}
```

Reading the file from top to bottom is enough. `write_files()` writes files under a directory. The key is the file name and the value is the content; a file whose value is `null` means "deleted" and is not written, and later lessons will describe changes that remove a file with this marker. If a subdirectory is needed, `mkdir` creates it. `read_files()` returns the files of a directory as name and content pairs; it is not called in this lesson, but it is given in full because the file is the course's shared core.

`run()` enters a directory and runs the file `main.php` there with `php`. Two settings are the foundation of the measurement: `display_errors` is turned off and standard error is discarded. That way a warning or an error message does not get mixed into the output; what is measured is not the text of a message but the exit code. The return value is a pair of the exit code and the output.

`measure()` does the real work and takes four parameters: the name of the observation, the baseline files, the change files and a list of seams. The list may be passed empty; in this lesson's observations it is empty. The function first checks the consumer: if the change array contains `main.php` it throws an exception, because a change that edits the consumer is not measured. Then it writes the baseline to the directory `measure/<name>/before`, lays the change over the baseline and writes the result to the directory `measure/<name>/after`. Both directories are run. If the exit code of the earlier run is not 0, the function again throws an exception: a result that comes from a broken baseline does not measure a change.

The `match (true)` block is the decision order, and the reach is given in this order: first breaking, then implicit, then at the seam, and if none of them, in place. The order matters. If a change both alters the output and stops the consumer, it counts as breaking, because stopping is heard earlier. This order is not explained again in any lesson of the course. The last `printf` prints a one-line summary of each observation: the name, the number of edited files, the exit code after the change, whether the output is the same or changed, and the reach. The number of files does not decide the reach; what decides it is whether the consumer was edited.

## The Private Property Changes Name

The first observation changes the name of the private property of the `Account` class from `balance` to `total`. The consumer `main.php` calls only the `deposit()` and `balance()` methods. The driver runs the `measure()` call and then prints the output of the two runs separately.

```php
<?php
// classes.php — the name of a private property changes; the consumer calls only methods
require_once 'reach.php';

measure('private-property', [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            private int $balance = 0;
            public function deposit(int $amount): void { $this->balance += $amount; }
            public function balance(): int { return $this->balance; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'account.php';
        $account = new Account();
        $account->deposit(50);
        $account->deposit(25);
        echo $account->balance(), "\n";
        PHP,
], [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            private int $total = 0;
            public function deposit(int $amount): void { $this->total += $amount; }
            public function balance(): int { return $this->total; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/private-property/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
private-property   files=1  exit=0   output same    -> in-place
before: 75
after: 75
```

**OM2.** One file was edited, the exit code stayed 0, the output stayed the same: `75` before and after. The consumer file was never opened. What kept the change inside the class is the visibility; the only thing the consumer could see was the methods, and the signatures of both stayed the same. The reach of this lesson is **in place**. It should not be confused with the "silent" channel of PHP Fundamentals: there a problem gave no signal, here there is no problem at all.

## When the Property Is Public

The same change gives a different result when the property is `public`. In the opposing measurement the `balance` property of the `Account` class is public and the consumer reads the property directly. The name again becomes `total`; the consumer `main.php` is again not edited.

```php
<?php
// public.php — the same change, but the property is public and the consumer reads it
require_once 'reach.php';

measure('public-property', [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $balance = 0;
            public function deposit(int $amount): void { $this->balance += $amount; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'account.php';
        $account = new Account();
        $account->deposit(50);
        $account->deposit(25);
        var_dump($account->balance);
        PHP,
], [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $total = 0;
            public function deposit(int $amount): void { $this->total += $amount; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/public-property/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
public-property    files=1  exit=0   output changed -> implicit
before: int(75)
after: NULL
```

**OM3.** The exit code is still 0, but the output changed: `NULL` instead of `int(75)`. The consumer read a property that no longer exists in the definition and got `NULL`. This lesson does not measure the presence of a signal, it measures the comparison of two runs, and the comparison gives the reach **implicit**. The same code and the same change moved from the reach **in place** to the reach **implicit** only because the visibility was different.

## The Bounding Measurement

The result above depended on the consumer reading the property. If the consumer writes to the same property, the situation changes. In this measurement the name of the `public` property of the `Account` class changes again, but the consumer assigns the value itself before reading it.

```php
<?php
// writer.php — a consumer that writes to a public property does not stop when the name changes
require_once 'reach.php';

measure('public-write', [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $balance = 0;
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'account.php';
        $account = new Account();
        $account->balance = 75;
        echo $account->balance, "\n";
        PHP,
], [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $total = 0;
        }
        PHP,
]);
```

```
public-write       files=1  exit=0   output same    -> in-place
```

The reach came out in place, but this does not mean the language protected the consumer. The name `balance` no longer exists in the definition; when a property that does not exist is written, PHP adds it to the object as a dynamic property, and since PHP 8.2 it reports this with `E_DEPRECATED` unless the class is marked with `#[\AllowDynamicProperties]`. The written value comes back when it is read, so `measure()` finds the two outputs the same. Whether the language gave a signal can be measured separately, in a block with an error handler.

```php
<?php
// dynamic.php — which level does writing to a property that does not exist report
$names = [E_DEPRECATED => 'E_DEPRECATED', E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE'];
set_error_handler(function (int $level) use ($names): bool {
    echo 'reported: ', $names[$level] ?? 'other', "\n";
    return true;
});

class Account
{
    public int $total = 0;
}

$account = new Account();
$account->balance = 75;
echo $account->balance, "\n";
```

```
reported: E_DEPRECATED
75
```

**OM4.** The language gave a signal but did not change the class: writing to a property that does not exist reported `E_DEPRECATED` and the value was still read, `75`. The consumer did not stop, because a write talks to the object, not to the definition. An in-place result is not a measure of how sturdy the definition is, but of which part of the definition the consumer touches: the reading consumer lost the name and fell under **implicit**, while the writing consumer stayed **in place** because it recreated the name itself. The lesson measured the level of this notice only by the name `E_DEPRECATED`; the text of the message was not printed.

## Summary

- A class carries properties and methods, `new` builds an instance, and every instance keeps its own state; `private` visibility closes a property to the outside.
- Where a change reaches is measured with a before and after run and one of four reaches is chosen; the decision order is breaking, implicit, at the seam, in place.
- When the name of a private property changed, the consumer heard nothing because it called methods: `75` before and after. The reach of this lesson is **in place**.
- The same change on a `public` property, with a reading consumer, moved to **implicit**: `NULL` instead of `int(75)`.
- A consumer that writes to a public property did not stop when the name changed; PHP created a dynamic property and reported `E_DEPRECATED`, and `measure()` again found the reach **in place**.

## Next Step

When the inside of a class changed, its consumers were not affected, because visibility stood in between. The Inheritance lesson asks about the case where a class extends another: when the inside of the parent class changes, what does the subclass experience?
