<?php
/**
* ========================================
* 🔒 IMMUTABLE OBJECTS - Kompletní průvodce
* ========================================
*/
// ============================================================================
// CO TO JE?
// ============================================================================
/*
IMMUTABLE OBJEKT = objekt, který po vytvoření NELZE ZMĚNIT
Místo změny vracíš NOVÝ objekt
DŮVOD: Bezpečnost, předvídatelnost, žádné vedlejší efekty
*/
// ============================================================================
// ❌ PROBLÉM S MUTABLE OBJEKTY
// ============================================================================
class MutablePrice
{
public float $value;
public function __construct(float $value)
{
$this->value = $value;
}
}
// Problém:
$originalPrice = new MutablePrice(1000);
$cart = new Cart();
$cart->addItem($originalPrice);
// Někde jinde v kódu:
function applyDiscount($price) {
$price->value *= 0.9; // Změní originál!
}
applyDiscount($originalPrice);
// 😱 KATASTROFA: Změnil se i v košíku!
echo $cart->getTotal(); // 900 místo 1000!
// PROBLÉMY:
// 1. Nelze sledovat změny - nevíš, kdo co změnil
// 2. Vedlejší efekty - změna jedné proměnné ovlivní jinou
// 3. Thread-unsafe - problémy při paralelním zpracování
// 4. Těžké debugování - nevíš, kdy se hodnota změnila
// ============================================================================
// ✅ ŘEŠENÍ: IMMUTABLE OBJECTS
// ============================================================================
class ImmutablePrice
{
private float $value; // Private!
public function __construct(float $value)
{
$this->value = $value;
}
public function getValue(): float
{
return $this->value;
}
// Neměním $this, vracím NOVÝ objekt!
public function withDiscount(int $percent): self
{
return new self($this->value * (100 - $percent) / 100);
}
}
// Použití:
$originalPrice = new ImmutablePrice(1000);
$cart = new Cart();
$cart->addItem($originalPrice);
$discounted = $originalPrice->withDiscount(10); // Nový objekt!
echo $originalPrice->getValue(); // 1000 - nezměněno! ✅
echo $discounted->getValue(); // 900
echo $cart->getTotal(); // 1000 - košík nezměněn! ✅
// ============================================================================
// PŘÍKLAD 1: MONEY (Peníze)
// ============================================================================
class Money
{
private float $amount;
private string $currency;
public function __construct(float $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
public function getAmount(): float { return $this->amount; }
public function getCurrency(): string { return $this->currency; }
// Všechny operace vracejí NOVÝ objekt
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException("Currency mismatch");
}
return new self($this->amount + $other->amount, $this->currency);
}
public function subtract(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException("Currency mismatch");
}
return new self($this->amount - $other->amount, $this->currency);
}
public function multiply(float $multiplier): self
{
return new self($this->amount * $multiplier, $this->currency);
}
public function divide(float $divisor): self
{
if ($divisor == 0) {
throw new \InvalidArgumentException("Cannot divide by zero");
}
return new self($this->amount / $divisor, $this->currency);
}
public function withTax(float $taxRate): self
{
return new self($this->amount * (1 + $taxRate), $this->currency);
}
public function isGreaterThan(Money $other): bool
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException("Currency mismatch");
}
return $this->amount > $other->amount;
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
// Použití Money:
$price = new Money(100, 'EUR');
$shippingCost = new Money(10, 'EUR');
$tax = 0.21;
$subtotal = $price->add($shippingCost); // 110 EUR
$withTax = $subtotal->withTax($tax); // 133.1 EUR
$perPerson = $withTax->divide(2); // 66.55 EUR
$withDiscount = $withTax->multiply(0.9); // 119.79 EUR
// Původní hodnoty nezměněny:
echo $price->getAmount(); // 100
echo $subtotal->getAmount(); // 110
echo $withTax->getAmount(); // 133.1
// ============================================================================
// PŘÍKLAD 2: DATERANGE (Časové období)
// ============================================================================
class DateRange
{
private \DateTime $start;
private \DateTime $end;
public function __construct(\DateTime $start, \DateTime $end)
{
if ($start > $end) {
throw new \InvalidArgumentException("Start must be before end");
}
// Clone, aby vnější změny neovlivnily tento objekt
$this->start = clone $start;
$this->end = clone $end;
}
public function getStart(): \DateTime
{
// Clone, aby volající nemohl změnit vnitřní stav
return clone $this->start;
}
public function getEnd(): \DateTime
{
return clone $this->end;
}
public function withStart(\DateTime $start): self
{
return new self($start, $this->end);
}
public function withEnd(\DateTime $end): self
{
return new self($this->start, $end);
}
public function extend(int $days): self
{
$newEnd = clone $this->end;
$newEnd->modify("+{$days} days");
return new self($this->start, $newEnd);
}
public function contains(\DateTime $date): bool
{
return $date >= $this->start && $date <= $this->end;
}
public function overlaps(DateRange $other): bool
{
return $this->start <= $other->end && $other->start <= $this->end;
}
public function getDays(): int
{
return $this->start->diff($this->end)->days;
}
}
// Použití DateRange:
$campaign = new DateRange(
new \DateTime('2025-01-01'),
new \DateTime('2025-01-31')
);
$extended = $campaign->extend(7); // Prodloužím o 7 dní
echo $campaign->getDays(); // 30 (původní nezměněn)
echo $extended->getDays(); // 37 (nový objekt)
// ============================================================================
// PŘÍKLAD 3: ADDRESS (Adresa)
// ============================================================================
class Address
{
private string $street;
private string $city;
private string $zipCode;
private string $country;
public function __construct(
string $street,
string $city,
string $zipCode,
string $country
) {
$this->street = $street;
$this->city = $city;
$this->zipCode = $zipCode;
$this->country = $country;
}
public function getStreet(): string { return $this->street; }
public function getCity(): string { return $this->city; }
public function getZipCode(): string { return $this->zipCode; }
public function getCountry(): string { return $this->country; }
public function withStreet(string $street): self
{
return new self($street, $this->city, $this->zipCode, $this->country);
}
public function withCity(string $city): self
{
return new self($this->street, $city, $this->zipCode, $this->country);
}
public function withZipCode(string $zipCode): self
{
return new self($this->street, $this->city, $zipCode, $this->country);
}
public function withCountry(string $country): self
{
return new self($this->street, $this->city, $this->zipCode, $country);
}
public function toString(): string
{
return "{$this->street}, {$this->zipCode} {$this->city}, {$this->country}";
}
public function equals(Address $other): bool
{
return $this->street === $other->street
&& $this->city === $other->city
&& $this->zipCode === $other->zipCode
&& $this->country === $other->country;
}
}
// Použití Address:
$original = new Address(
'Václavské náměstí 1',
'Praha',
'110 00',
'Česká republika'
);
// Method chaining!
$updated = $original
->withStreet('Karlova 2')
->withCity('Brno')
->withZipCode('602 00');
echo $original->toString(); // Václavské náměstí 1, 110 00 Praha...
echo $updated->toString(); // Karlova 2, 602 00 Brno...
// ============================================================================
// PŘÍKLAD 4: EMAIL (E-mail s validací)
// ============================================================================
class Email
{
private string $value;
public function __construct(string $email)
{
$email = trim(strtolower($email));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email: {$email}");
}
$this->value = $email;
}
public function getValue(): string
{
return $this->value;
}
public function getDomain(): string
{
return substr($this->value, strpos($this->value, '@') + 1);
}
public function getLocalPart(): string
{
return substr($this->value, 0, strpos($this->value, '@'));
}
public function equals(Email $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}
// Použití Email:
$email = new Email('john.doe@example.com');
echo $email->getValue(); // john.doe@example.com
echo $email->getDomain(); // example.com
echo $email->getLocalPart(); // john.doe
$invalid = new Email('invalid'); // ❌ Exception!
// ============================================================================
// PŘÍKLAD 5: PERCENTAGE (Procenta 0-100)
// ============================================================================
class Percentage
{
private int $value;
public function __construct(int $value)
{
if ($value < 0 || $value > 100) {
throw new \InvalidArgumentException(
"Percentage must be 0-100, got: {$value}"
);
}
$this->value = $value;
}
public function getValue(): int
{
return $this->value;
}
public function applyTo(float $amount): float
{
return $amount * ($this->value / 100);
}
public function asDecimal(): float
{
return $this->value / 100;
}
public function increase(int $points): self
{
return new self($this->value + $points);
}
public function decrease(int $points): self
{
return new self($this->value - $points);
}
}
// Použití Percentage:
$discount = new Percentage(20);
$price = 1000;
echo $discount->applyTo($price); // 200 (20% z 1000)
echo $price - $discount->applyTo($price); // 800
$biggerDiscount = $discount->increase(10); // 30%
echo $biggerDiscount->getValue(); // 30
// ============================================================================
// KLÍČOVÁ PRAVIDLA PRO IMMUTABLE OBJEKTY
// ============================================================================
// PRAVIDLO 1: ✅ Private properties
class Good1
{
private string $value; // ✅ Nelze změnit zvenčí
}
class Bad1
{
public string $value; // ❌ Lze změnit: $obj->value = 'new'
}
// PRAVIDLO 2: ✅ Žádné settery
class Bad2
{
private string $value;
public function setValue(string $value) // ❌ Setter mění stav
{
$this->value = $value;
}
}
class Good2
{
private string $value;
public function withValue(string $value): self // ✅ Vrací nový objekt
{
return new self($value);
}
}
// PRAVIDLO 3: ✅ Clone mutable objekty (DateTime, arrays...)
class Bad3
{
private \DateTime $date;
public function getDate(): \DateTime
{
return $this->date; // ❌ Volající může změnit: $obj->getDate()->modify('+1 day')
}
}
class Good3
{
private \DateTime $date;
public function __construct(\DateTime $date)
{
$this->date = clone $date; // ✅ Uloží kopii
}
public function getDate(): \DateTime
{
return clone $this->date; // ✅ Vrací kopii
}
}
// PRAVIDLO 4: ✅ Pojmenování: with*() místo set*()
// Convention pro immutable objekty:
$newPrice = $price->withDiscount(10);
$newAddress = $address->withStreet('Nova 1');
$newDate = $dateRange->withEnd(new DateTime());
// ============================================================================
// KDY POUŽÍVAT IMMUTABLE?
// ============================================================================
/*
✅ ANO - Immutable jsou ideální pro:
──────────────────────────────────────
1. Value Objects (Money, Email, Percentage, Date...)
2. Configuration objekty
3. DTOs (Data Transfer Objects)
4. Domain Models (obzvlášť v DDD)
5. Objekty sdílené mezi vlákny
6. Cache keys
7. Event objekty
8. Malé objekty s málo properties
PŘÍKLAD: Configuration
*/
class DatabaseConfig
{
private string $host;
private int $port;
private string $database;
public function __construct(string $host, int $port, string $database)
{
$this->host = $host;
$this->port = $port;
$this->database = $database;
}
public function withHost(string $host): self
{
return new self($host, $this->port, $this->database);
}
public function withPort(int $port): self
{
return new self($this->host, $port, $this->database);
}
public function withDatabase(string $database): self
{
return new self($this->host, $this->port, $database);
}
}
/*
❌ NE - Mutable jsou lepší pro:
──────────────────────────────────────
1. Entity s velkým množstvím dat (Order, User s 50 properties)
2. Objekty s komplexními vztahy (Order s items, customer, payments...)
3. ORM entity (Doctrine, Eloquent)
4. Builder pattern
5. Výkonově kritické části (miliony objektů)
PŘÍKLAD: Builder (mutable je OK)
*/
class QueryBuilder
{
private string $select = '';
private string $from = '';
public function select(string $columns): self
{
$this->select = $columns; // Mutable - mění stav
return $this;
}
public function from(string $table): self
{
$this->from = $table;
return $this;
}
// Na konci vytvoříš immutable result
public function build(): Query
{
return new Query($this->select, $this->from); // Immutable
}
}
// ============================================================================
// PRAKTICKÝ TIP: HYBRID PŘÍSTUP
// ============================================================================
/*
Velké entity jsou mutable, ale používají immutable Value Objects uvnitř!
*/
class Order
{
private int $id;
private Customer $customer; // Mutable
private Money $total; // Immutable! ✅
private OrderStatus $status; // Immutable! ✅
private Email $email; // Immutable! ✅
private array $items;
// Mutable metody pro hlavní entitu
public function setCustomer(Customer $customer): void
{
$this->customer = $customer;
}
// Ale používáš immutable Value Objects uvnitř!
public function applyDiscount(Percentage $discount): void
{
// Money je immutable, takže vytváříš nový
$this->total = $this->total->multiply(
(100 - $discount->getValue()) / 100
);
}
public function changeStatus(OrderStatus $newStatus): void
{
// OrderStatus je immutable Value Object
$this->status = $newStatus;
}
public function updateEmail(Email $email): void
{
// Email je immutable Value Object
$this->email = $email;
}
}
// ============================================================================
// VÝHODY VS NEVÝHODY
// ============================================================================
/*
✅ VÝHODY:
──────────
• Bezpečnost - žádné nechtěné změny
• Snadné debugování - stav se nemění
• Thread-safe - lze sdílet mezi vlákny
• Hashable - lze použít jako key v mapách
• Cache-friendly - nemusíš sledovat změny
• Předvídatelné - víš, že se nemění
• Jednodušší reasoning - méně mentální zátěže
❌ NEVÝHODY:
────────────
• Více paměti - vytváříš nové objekty
• Pomalejší - new je náročnější než změna
• Více kódu - potřebuješ with*() metody
• Nehodí se pro velké entity
*/
// ============================================================================
// SHRNUTÍ - ZLATÁ PRAVIDLA
// ============================================================================
/*
1. ✅ Value Objects = VŽDY immutable
2. ✅ Malé objekty (< 5 properties) = immutable
3. ✅ Private properties + gettery
4. ✅ Žádné settery, použij with*()
5. ✅ Clone mutable objekty (DateTime, array objekty)
6. ✅ Validuj v constructoru
7. ✅ Používaj method chaining: $obj->withX()->withY()->withZ()
8. ❌ Velké entity (> 10 properties) = mutable OK
9. ❌ ORM entity = mutable OK
10. ❌ Buildery = mutable OK
CHECKLIST PRO IMMUTABLE TŘÍDU:
───────────────────────────────
□ Všechny properties private?
□ Žádné settery?
□ Všechny metody vracejí new self?
□ Clone pro DateTime a objekty?
□ Pojmenování with*()?
□ Validace v constructoru?
*/
// ============================================================================
// REAL-WORLD PŘÍKLAD: Order Value Objects
// ============================================================================
class OrderNumber
{
private string $value;
public function __construct(string $value)
{
if (!preg_match('/^ORD-\d{6}$/', $value)) {
throw new \InvalidArgumentException("Invalid order number format");
}
$this->value = $value;
}
public function getValue(): string { return $this->value; }
public function __toString(): string { return $this->value; }
}
class OrderStatus
{
private string $value;
private const NEW = 'new';
private const PAID = 'paid';
private const SHIPPED = 'shipped';
private const DELIVERED = 'delivered';
private static $valid = [self::NEW, self::PAID, self::SHIPPED, self::DELIVERED];
private function __construct(string $value)
{
if (!in_array($value, self::$valid)) {
throw new \InvalidArgumentException("Invalid status: {$value}");
}
$this->value = $value;
}
public static function new(): self { return new self(self::NEW); }
public static function paid(): self { return new self(self::PAID); }
public static function shipped(): self { return new self(self::SHIPPED); }
public static function delivered(): self { return new self(self::DELIVERED); }
public function isNew(): bool { return $this->value === self::NEW; }
public function canShip(): bool { return $this->value === self::PAID; }
public function getValue(): string { return $this->value; }
}
// Použití v Order entity:
class RealOrder
{
private OrderNumber $orderNumber;
private OrderStatus $status;
private Money $total;
private Email $customerEmail;
public function __construct(
OrderNumber $orderNumber,
Email $customerEmail,
Money $total
) {
$this->orderNumber = $orderNumber;
$this->customerEmail = $customerEmail;
$this->total = $total;
$this->status = OrderStatus::new();
}
public function markAsPaid(): void
{
$this->status = OrderStatus::paid();
}
public function ship(): void
{
if (!$this->status->canShip()) {
throw new \RuntimeException("Cannot ship unpaid order");
}
$this->status = OrderStatus::shipped();
}
public function applyDiscount(Percentage $discount): void
{
$this->total = $this->total->multiply(
1 - $discount->asDecimal()
);
}
}
// Vytvoření objednávky:
$order = new RealOrder(
new OrderNumber('ORD-123456'),
new Email('customer@example.com'),
new Money(1000, 'CZK')
);
$order->markAsPaid();
$order->applyDiscount(new Percentage(10));
$order->ship();
/*
═══════════════════════════════════════════════════════════════════════════
ZÁVĚR: Immutable objekty jsou mocný nástroj pro bezpečný, předvídatelný kód.
Používej je pro Value Objects a malé objekty. Pro velké entity kombinuj
mutable entitu s immutable Value Objects uvnitř.
═══════════════════════════════════════════════════════════════════════════
*/
Hotovo! Celé na jednu stránku pro Gist 🚀