๐ The setup
You run a small pancake house. Every order is an immutable Pancake โ once placed, you can't mutate it; you clone a new one with the change. Customers build their order step by step: add a topping, size it up, bake it, calculate the bill.
You'll use clone with to return new instances without a constructor-spaghetti, and the pipe operator to compose it all into one readable pipeline.
๐งฑ Starter code
pancake.php โ starter
<?php
final class Pancake
{
public function __construct(
public readonly string $base = 'plain',
public readonly array $toppings = [],
public readonly string $size = 'medium',
public readonly bool $isBaked = false,
) {}
public function describe(): string
{
$t = $this->toppings ? implode(' + ', $this->toppings) : 'no toppings';
$state = $this->isBaked ? '๐ฅ cooked' : '๐ง raw batter';
return "{$this->size} {$this->base} [{$t}] โ {$state}";
}
}
// Today's mission:
// Build a large 'apple' pancake with 'syrup', 'cinnamon' and 'bacon',
// bake it, print the description, then print the price.
// Do it all with |> and `clone with`. No intermediate variables!
๐ฏ Your challenges
- Immutable modifiers. Add three methods using
clone with:withTopping(string $t): selfโ returns a new pancake with$tappended totoppings.withSize(string $s): selfโ returns a new pancake at that size.bake(): selfโ returns a baked pancake.
- Compose with pipes. Using only the pipe operator
|>and your new methods, build today's mission order. No temporary variables. End the chain with->describe()andechothe result. - Add a bill. Add a
priceEuros(): intmethod. Rules: base โฌ6, +โฌ1 per topping, +โฌ3 forlarge, +โฌ2 if baked (it's real cooking!). Extend your pipeline to also print the price on a new line. - ๐ Bonus โ first-class callables. Use the first-class callable syntax (e.g.
strtoupper(...)) somewhere in your pipe to transform the description. Hint: not every step needs an arrow function. - ๐ Group ordering. Make an array of three pancakes and use
array_map()inside a pipe to print the full menu in one expression.
๐ก Hint โ clone with & pipe syntax
// clone with: pass an array of overrides as clone's second argument
public function withTopping(string $t): self
{
return clone($this, [
'toppings' => [...$this->toppings, $t],
]);
}
// pipe operator: right side is a callable, receives left side as arg
$shout = 'hello' |> strtoupper(...); // SHOUT becomes 'HELLO'
// realistic pipe โ note the parens around each fn(...)
$result = new Pancake('apple')
|> (fn(Pancake $p) => $p->withTopping('syrup'))
|> (fn(Pancake $p) => $p->bake());
โ Show one possible solution
Answer both questions correctly to unlock the solution.
1. Given $result = 'hello' |> strtoupper(...); โ what value does $result hold?
2. You write return clone($this, ['isBaked' => true]); inside a readonly class. What happens?
<?php
final class Pancake
{
public function __construct(
public readonly string $base = 'plain',
public readonly array $toppings = [],
public readonly string $size = 'medium',
public readonly bool $isBaked = false,
) {}
public function withTopping(string $t): self
{
return clone($this, ['toppings' => [...$this->toppings, $t]]);
}
public function withSize(string $s): self
{
return clone($this, ['size' => $s]);
}
public function bake(): self
{
return clone($this, ['isBaked' => true]);
}
public function priceEuros(): int
{
return 6
+ count($this->toppings)
+ ($this->size === 'large' ? 3 : 0)
+ ($this->isBaked ? 2 : 0);
}
public function describe(): string
{
$t = $this->toppings ? implode(' + ', $this->toppings) : 'no toppings';
$state = $this->isBaked ? '๐ฅ cooked' : '๐ง raw batter';
return "{$this->size} {$this->base} [{$t}] โ {$state}";
}
}
$order = new Pancake('apple')
|> (fn(Pancake $p) => $p->withTopping('syrup'))
|> (fn(Pancake $p) => $p->withTopping('cinnamon'))
|> (fn(Pancake $p) => $p->withTopping('bacon'))
|> (fn(Pancake $p) => $p->withSize('large'))
|> (fn(Pancake $p) => $p->bake());
// bonus: first-class callable on a built-in fits naturally on the right of |>
echo ($order->describe() |> strtoupper(...)) . PHP_EOL;
echo 'โฌ' . $order->priceEuros() . PHP_EOL;
๐งช Think about it: the original object is never mutated. Does
clone with still trigger constructor logic? What happens if a readonly property references another object โ is the clone deep or shallow?