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.
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"
}Unveränderlichkeit (Immutability): Readonly Properties sind ein Grundbaustein für unveränderliche Objekte, die in vielen Programmierparadigmen, insbesondere in funktionaler Programmierung, bevorzugt werden.
Sicherheit: Sie verhindern unbeabsichtigte Änderungen an Objekten nach ihrer Erstellung.
Klarheit: Sie machen deutlich, welche Teile eines Objekts nicht für Änderungen vorgesehen sind.
Parallelisierbarkeit: Unveränderliche Objekte sind von Natur aus threadsicher, was in parallelen Verarbeitungsszenarien vorteilhaft ist.
Vorhersagbarkeit: Der Zustand eines Objekts mit readonly Properties ändert sich nach der Initialisierung nicht mehr, was das Verhalten vorhersehbarer macht.
Bei der Verwendung von readonly Properties sind einige wichtige Regeln zu beachten:
Readonly Properties müssen einen Typ haben:
class User
{
// Korrekt: Mit Typdeklaration
public readonly string $name;
// Falsch: Ohne Typdeklaration
// public readonly $email; // Syntax-Fehler
}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;
}
}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;
}
}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;
}
}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;
}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.0Readonly 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");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 werdenIn 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
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"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);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
]
);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();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"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.
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 ArrayFü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);
}
}Kombinieren Sie readonly mit Constructor Property Promotion: Diese Kombination führt zu besonders kompaktem und lesbarem Code.
Verwenden Sie readonly für Value Objects: Value Objects sind ein idealer Anwendungsfall für readonly Properties.
Ab PHP 8.2: Nutzen Sie readonly Klassen: Wenn
alle Eigenschaften einer Klasse unveränderlich sein sollen, deklarieren
Sie die ganze Klasse als readonly.