Skip to content

Instantly share code, notes, and snippets.

@Kcko
Created October 24, 2025 12:57
Show Gist options
  • Select an option

  • Save Kcko/9e98ecbdf060a6a8538a2c85a30cae26 to your computer and use it in GitHub Desktop.

Select an option

Save Kcko/9e98ecbdf060a6a8538a2c85a30cae26 to your computer and use it in GitHub Desktop.

Revisions

  1. Kcko revised this gist Oct 24, 2025. No changes.
  2. Kcko created this gist Oct 24, 2025.
    805 changes: 805 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,805 @@
    ```php
    <?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('[email protected]');

    echo $email->getValue(); // [email protected]
    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('[email protected]'),
    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 🚀