/ Gists / Immutable pattern
On gists

Immutable pattern

PHP PHP Patterns

readme.md Raw #

<?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 🚀