28 Readonly Properties

Mit PHP 8.1 wurde das Konzept der Readonly Properties (schreibgeschützte Eigenschaften) eingeführt, das einen wichtigen Schritt in Richtung Unveränderlichkeit (Immutability) von Objekten darstellt. Diese Spracherweiterung ermöglicht es, Klasseneigenschaften zu definieren, die nach ihrer Initialisierung nicht mehr verändert werden können.

28.1 Grundkonzept

Eine readonly-Eigenschaft kann nur einmal innerhalb des Konstruktors oder bei der Eigenschaftsdeklaration initialisiert werden. Jeder Versuch, den Wert später zu ändern, führt zu einer Error-Exception.

class Point
{
    public readonly float $x;
    public readonly float $y;
    
    public function __construct(float $x, float $y)
    {
        $this->x = $x;
        $this->y = $y;
    }
}

$point = new Point(10.5, 20.7);
echo $point->x; // 10.5

// Versuch, eine readonly-Eigenschaft zu ändern
try {
    $point->x = 15.0; // Löst einen Error aus
} catch (Error $e) {
    echo "Fehler: " . $e->getMessage(); // "Fehler: Cannot modify readonly property Point::$x"
}

28.2 Vorteile von Readonly Properties

  1. Unveränderlichkeit (Immutability): Readonly Properties sind ein Grundbaustein für unveränderliche Objekte, die in vielen Programmierparadigmen, insbesondere in funktionaler Programmierung, bevorzugt werden.

  2. Sicherheit: Sie verhindern unbeabsichtigte Änderungen an Objekten nach ihrer Erstellung.

  3. Klarheit: Sie machen deutlich, welche Teile eines Objekts nicht für Änderungen vorgesehen sind.

  4. Parallelisierbarkeit: Unveränderliche Objekte sind von Natur aus threadsicher, was in parallelen Verarbeitungsszenarien vorteilhaft ist.

  5. Vorhersagbarkeit: Der Zustand eines Objekts mit readonly Properties ändert sich nach der Initialisierung nicht mehr, was das Verhalten vorhersehbarer macht.

28.3 Regeln und Einschränkungen

Bei der Verwendung von readonly Properties sind einige wichtige Regeln zu beachten:

28.3.1 1. Typdeklaration ist erforderlich

Readonly Properties müssen einen Typ haben:

class User
{
    // Korrekt: Mit Typdeklaration
    public readonly string $name;
    
    // Falsch: Ohne Typdeklaration
    // public readonly $email; // Syntax-Fehler
}

28.3.2 2. Initialisierung

Readonly Properties können auf zwei Arten initialisiert werden:

class Example
{
    // 1. Bei der Deklaration
    public readonly string $initializedAtDeclaration = "Standardwert";
    
    // 2. Im Konstruktor
    public readonly string $initializedInConstructor;
    
    public function __construct(string $value)
    {
        $this->initializedInConstructor = $value;
    }
}

28.3.3 3. Keine erneute Zuweisung

Nach der Initialisierung kann der Wert nicht mehr geändert werden:

class Product
{
    public readonly string $sku;
    
    public function __construct(string $sku)
    {
        $this->sku = $sku;
    }
    
    public function updateSku(string $newSku): void
    {
        // Dies würde einen Fehler verursachen
        // $this->sku = $newSku;
    }
}

28.3.4 4. Ausschließlich auf Eigenschaften anwendbar

Das readonly-Schlüsselwort kann nur auf Eigenschaften angewendet werden, nicht auf Methoden, Parameter oder lokale Variablen:

class Calculator
{
    // Korrekt: readonly-Eigenschaft
    public readonly float $pi = 3.14159;
    
    // Falsch: readonly-Methode
    // public readonly function calculate() {} // Syntax-Fehler
    
    public function add(
        float $a,
        float $b
        // readonly float $c // Falsch: readonly-Parameter
    ): float {
        // readonly float $result; // Falsch: readonly lokale Variable
        return $a + $b;
    }
}

28.3.5 5. Eingeschränkte Verwendung in Vererbung

Wenn eine Eigenschaft in einer Basisklasse als readonly deklariert ist, muss sie in allen abgeleiteten Klassen ebenfalls readonly sein:

class Base
{
    public readonly string $id;
    
    public function __construct(string $id)
    {
        $this->id = $id;
    }
}

class Derived extends Base
{
    // Falsch: Überschreiben einer readonly-Eigenschaft ohne readonly
    // public string $id; // Löst einen Fehler aus
    
    // Korrekt: Beibehaltung von readonly
    public readonly string $id;
}

28.4 Kombinationen mit anderen Sprachfeatures

28.4.1 Mit Constructor Property Promotion

Readonly Properties lassen sich hervorragend mit Constructor Property Promotion kombinieren, was zu sehr kompaktem und lesbarem Code führt:

class Rectangle
{
    public function __construct(
        public readonly float $width,
        public readonly float $height
    ) {}
    
    public function area(): float
    {
        return $this->width * $this->height;
    }
}

$rect = new Rectangle(10.0, 5.0);
echo $rect->area(); // 50.0

28.4.2 Mit Union Types und Nullable Types

Readonly Properties können mit Union Types und Nullable Types kombiniert werden:

class Configuration
{
    public function __construct(
        public readonly string $appName,
        public readonly string|int $version,
        public readonly ?string $environment = null
    ) {}
}

$config = new Configuration("MyApp", "1.0.3");
$prodConfig = new Configuration("MyApp", 2, "production");

28.4.3 Mit PHP 8.2: Readonly Classes

In PHP 8.2 wurde das Konzept der readonly Properties erweitert, indem ganze Klassen als readonly deklariert werden können:

// Ab PHP 8.2
readonly class ImmutableValue
{
    // Alle Eigenschaften sind implizit readonly
    public string $name;
    private int $count;
    
    public function __construct(string $name, int $count)
    {
        $this->name = $name;
        $this->count = $count;
    }
}

$value = new ImmutableValue("Test", 42);
// $value->name = "NewName"; // Fehler: Kann nicht geändert werden

In einer readonly-Klasse: - Alle Eigenschaften sind implizit readonly - Es ist nicht möglich, nicht-readonly Eigenschaften zu deklarieren - Dynamische Eigenschaften können nicht erstellt werden

28.5 Praxisbeispiele

28.5.1 Value Objects

Value Objects sind unveränderliche Objekte, die durch ihre Eigenschaften definiert werden und sich perfekt für readonly Properties eignen:

readonly class Money
{
    public function __construct(
        public float $amount,
        public string $currency
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException("Betrag darf nicht negativ sein");
        }
    }
    
    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException("Währungen müssen übereinstimmen");
        }
        
        return new self($this->amount + $other->amount, $this->currency);
    }
    
    public function multiply(float $factor): self
    {
        return new self($this->amount * $factor, $this->currency);
    }
    
    public function format(): string
    {
        return number_format($this->amount, 2) . ' ' . $this->currency;
    }
}

$price = new Money(100.0, "EUR");
$tax = new Money(19.0, "EUR");
$total = $price->add($tax);

echo $total->format(); // "119.00 EUR"

28.5.2 Data Transfer Objects (DTOs)

DTOs werden häufig verwendet, um Daten zwischen Systemschichten zu transportieren, und sollten in der Regel unveränderlich sein:

readonly class UserDTO
{
    public function __construct(
        public int $id,
        public string $username,
        public string $email,
        public array $roles = []
    ) {}
    
    public static function fromArray(array $data): self
    {
        return new self(
            $data['id'] ?? 0,
            $data['username'] ?? '',
            $data['email'] ?? '',
            $data['roles'] ?? []
        );
    }
    
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'username' => $this->username,
            'email' => $this->email,
            'roles' => $this->roles
        ];
    }
}

// Verwendung in einem Controller
$userData = $request->toArray();
$userDTO = UserDTO::fromArray($userData);

// Weitergabe an einen Service
$user = $userService->createUser($userDTO);

28.5.3 Konfigurationsobjekte

Konfigurationseinstellungen sollten nach dem Laden nicht mehr geändert werden können:

readonly class DatabaseConfig
{
    public function __construct(
        public string $host,
        public int $port,
        public string $username,
        public string $password,
        public string $database,
        public bool $useSSL = true,
        public int $timeout = 30
    ) {}
    
    public static function fromEnv(): self
    {
        return new self(
            $_ENV['DB_HOST'] ?? 'localhost',
            (int)($_ENV['DB_PORT'] ?? 3306),
            $_ENV['DB_USER'] ?? 'root',
            $_ENV['DB_PASS'] ?? '',
            $_ENV['DB_NAME'] ?? 'app',
            (bool)($_ENV['DB_SSL'] ?? true),
            (int)($_ENV['DB_TIMEOUT'] ?? 30)
        );
    }
    
    public function getDsn(): string
    {
        $ssl = $this->useSSL ? ';sslmode=require' : '';
        return "mysql:host={$this->host};port={$this->port};dbname={$this->database}{$ssl}";
    }
}

$dbConfig = DatabaseConfig::fromEnv();
$pdo = new PDO(
    $dbConfig->getDsn(),
    $dbConfig->username,
    $dbConfig->password,
    [
        PDO::ATTR_TIMEOUT => $dbConfig->timeout,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]
);

28.6 Entwurfsmuster und Architekturüberlegungen

28.6.1 Builder-Pattern

Bei komplexen Objekten mit vielen optionalen Parametern kann das Builder-Pattern mit readonly Properties kombiniert werden:

readonly class Email
{
    public function __construct(
        public string $to,
        public string $subject,
        public string $body,
        public ?string $from = null,
        public ?string $replyTo = null,
        public array $cc = [],
        public array $bcc = [],
        public array $attachments = []
    ) {}
}

class EmailBuilder
{
    private string $to = '';
    private string $subject = '';
    private string $body = '';
    private ?string $from = null;
    private ?string $replyTo = null;
    private array $cc = [];
    private array $bcc = [];
    private array $attachments = [];
    
    public function to(string $to): self
    {
        $this->to = $to;
        return $this;
    }
    
    public function subject(string $subject): self
    {
        $this->subject = $subject;
        return $this;
    }
    
    public function body(string $body): self
    {
        $this->body = $body;
        return $this;
    }
    
    public function from(?string $from): self
    {
        $this->from = $from;
        return $this;
    }
    
    // Weitere Builder-Methoden...
    
    public function build(): Email
    {
        if (empty($this->to)) {
            throw new InvalidArgumentException("Empfänger (to) muss angegeben werden");
        }
        
        if (empty($this->subject)) {
            throw new InvalidArgumentException("Betreff (subject) muss angegeben werden");
        }
        
        if (empty($this->body)) {
            throw new InvalidArgumentException("Nachrichtentext (body) muss angegeben werden");
        }
        
        return new Email(
            $this->to,
            $this->subject,
            $this->body,
            $this->from,
            $this->replyTo,
            $this->cc,
            $this->bcc,
            $this->attachments
        );
    }
}

// Verwendung des Builders
$email = (new EmailBuilder())
    ->to('recipient@example.com')
    ->subject('Wichtige Mitteilung')
    ->body('Dies ist eine Testnachricht.')
    ->from('sender@example.com')
    ->build();

28.6.2 Immutable Collections

Readonly Properties ermöglichen die Implementierung unveränderlicher Sammlungen:

readonly class ImmutableCollection
{
    private array $items;
    
    public function __construct(array $items = [])
    {
        $this->items = $items;
    }
    
    public function map(callable $callback): self
    {
        return new self(array_map($callback, $this->items));
    }
    
    public function filter(callable $callback): self
    {
        return new self(array_filter($this->items, $callback));
    }
    
    public function merge(self $other): self
    {
        return new self(array_merge($this->items, $other->toArray()));
    }
    
    public function toArray(): array
    {
        return $this->items;
    }
    
    public function count(): int
    {
        return count($this->items);
    }
}

$numbers = new ImmutableCollection([1, 2, 3, 4, 5]);
$doubled = $numbers->map(fn($n) => $n * 2);
$filtered = $doubled->filter(fn($n) => $n > 5);

echo implode(', ', $filtered->toArray()); // "6, 8, 10"

28.7 Best Practices

  1. Verwenden Sie readonly für alle unveränderlichen Daten: Wenn ein Wert nach der Initialisierung nicht mehr geändert werden soll, markieren Sie ihn als readonly.

  2. Achten Sie auf tiefe Veränderlichkeit: Beachten Sie, dass readonly nur die Eigenschaft selbst schützt, nicht aber deren Inhalt, wenn es sich um ein Objekt oder Array handelt:

class Container
{
    public readonly array $items;
    
    public function __construct(array $items)
    {
        $this->items = $items;
    }
}

$container = new Container([1, 2, 3]);
// $container->items = [4, 5, 6]; // Fehler: Eigenschaft ist readonly

// Aber der Inhalt des Arrays kann geändert werden:
$container->items[0] = 99; // Funktioniert, ändert ein Element im Array

Für echte Unveränderlichkeit müssen Sie zusätzliche Maßnahmen ergreifen:

readonly class DeepImmutableContainer
{
    private array $items;
    
    public function __construct(array $items)
    {
        $this->items = $items;
    }
    
    public function getItems(): array
    {
        // Return a copy to prevent modification of internal state
        return $this->items;
    }
    
    public function withItem(int $index, mixed $value): self
    {
        $newItems = $this->items;
        $newItems[$index] = $value;
        return new self($newItems);
    }
}
  1. Kombinieren Sie readonly mit Constructor Property Promotion: Diese Kombination führt zu besonders kompaktem und lesbarem Code.

  2. Verwenden Sie readonly für Value Objects: Value Objects sind ein idealer Anwendungsfall für readonly Properties.

  3. Ab PHP 8.2: Nutzen Sie readonly Klassen: Wenn alle Eigenschaften einer Klasse unveränderlich sein sollen, deklarieren Sie die ganze Klasse als readonly.