Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
72 / 72 |
|
100.00% |
13 / 13 |
CRAP | |
100.00% |
1 / 1 |
| Sections | |
100.00% |
72 / 72 |
|
100.00% |
13 / 13 |
27 | |
100.00% |
1 / 1 |
| capture | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
| append | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| prepend | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| rewrite | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
| writing | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| nest | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
| enterLayout | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getOr | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| has | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| assign | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| write | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| assertWritable | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| position | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Boiler; |
| 6 | |
| 7 | use Celema\Boiler\Exception\LogicException; |
| 8 | use Closure; |
| 9 | |
| 10 | /** |
| 11 | * The sections captured during a render, shared by the rendered template, |
| 12 | * its includes, and its layouts. |
| 13 | * |
| 14 | * @internal |
| 15 | */ |
| 16 | final class Sections |
| 17 | { |
| 18 | /** @var array<string, Section> */ |
| 19 | private array $sections = []; |
| 20 | |
| 21 | /** |
| 22 | * The position of the include that is rendering, as the layout level and |
| 23 | * call number of each enclosing include; empty for the rendered template. |
| 24 | * |
| 25 | * @var list<int> |
| 26 | */ |
| 27 | private array $include = []; |
| 28 | |
| 29 | /** |
| 30 | * How many layouts deep the rendering template is, counted on from the |
| 31 | * level of the template that included it. |
| 32 | */ |
| 33 | private int $level = 0; |
| 34 | |
| 35 | /** Numbers appends and includes in call order. */ |
| 36 | private int $calls = 0; |
| 37 | |
| 38 | /** |
| 39 | * Where and at which position the main content of each section was |
| 40 | * captured. |
| 41 | * |
| 42 | * @var array<string, array{Location, list<int>}> |
| 43 | */ |
| 44 | private array $captured = []; |
| 45 | |
| 46 | /** |
| 47 | * How many discarded defaults are open. While one is, nothing reaches the |
| 48 | * sections, so a default that is not used adds nothing, not even through |
| 49 | * the templates it includes. |
| 50 | */ |
| 51 | private int $muted = 0; |
| 52 | |
| 53 | /** |
| 54 | * The sections that open `section()`, `append()`, and `prepend()` blocks |
| 55 | * write, innermost last. Such a section cannot be printed before they |
| 56 | * close, as its content is not complete yet. |
| 57 | * |
| 58 | * @var list<string> |
| 59 | */ |
| 60 | private array $writing = []; |
| 61 | |
| 62 | /** |
| 63 | * The sections that open `rewrite()` blocks rewrite, innermost last. Such |
| 64 | * a section cannot be written before they close, as the rewrite replaces |
| 65 | * what it read. |
| 66 | * |
| 67 | * @var list<string> |
| 68 | */ |
| 69 | private array $rewriting = []; |
| 70 | |
| 71 | /** |
| 72 | * Opens a capture of the main content and returns what closes it with the |
| 73 | * captured output and where `section()` opened it. |
| 74 | * |
| 75 | * A layout's capture is a default: a template it wraps renders first, so |
| 76 | * when one of those captured the section already, the layout's capture is |
| 77 | * discarded. Any other second capture fails instead of replacing the |
| 78 | * first. |
| 79 | * |
| 80 | * @return Closure(string, Location): void |
| 81 | */ |
| 82 | public function capture(string $name): Closure |
| 83 | { |
| 84 | if ($this->muted > 0) { |
| 85 | return $this->write($name); |
| 86 | } |
| 87 | |
| 88 | $position = $this->position(); |
| 89 | $captured = $this->captured[$name] ?? null; |
| 90 | |
| 91 | if ($captured !== null && Section::wraps($position, $captured[1])) { |
| 92 | $close = $this->write($name, function (): void { |
| 93 | $this->muted--; |
| 94 | }); |
| 95 | $this->muted++; |
| 96 | |
| 97 | return $close; |
| 98 | } |
| 99 | |
| 100 | return $this->write($name, function (string $content, Location $location) use ($name, $position): void { |
| 101 | $this->assign($name, $content, $location, $position); |
| 102 | }); |
| 103 | } |
| 104 | |
| 105 | /** @return Closure(string, Location): void */ |
| 106 | public function append(string $name): Closure |
| 107 | { |
| 108 | return $this->write($name, function (string $content) use ($name): void { |
| 109 | if ($this->muted === 0) { |
| 110 | ($this->sections[$name] ??= new Section())->append($content, $this->position()); |
| 111 | } |
| 112 | }); |
| 113 | } |
| 114 | |
| 115 | /** @return Closure(string, Location): void */ |
| 116 | public function prepend(string $name): Closure |
| 117 | { |
| 118 | return $this->write($name, function (string $content) use ($name): void { |
| 119 | if ($this->muted === 0) { |
| 120 | ($this->sections[$name] ??= new Section())->prepend($content); |
| 121 | } |
| 122 | }); |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Opens a rewrite, inside which `yield()` returns the section's content so |
| 127 | * far, and returns what closes it. The output replaces that content, |
| 128 | * appended and prepended content included, and counts as a capture at the |
| 129 | * rewriting template. When both are blank, the section stays as it was, |
| 130 | * so a default passed to `yield()` still applies. |
| 131 | * |
| 132 | * @return Closure(string, Location): void |
| 133 | */ |
| 134 | public function rewrite(string $name): Closure |
| 135 | { |
| 136 | $this->assertWritable($name); |
| 137 | $this->rewriting[] = $name; |
| 138 | |
| 139 | return function (string $content, Location $location) use ($name): void { |
| 140 | array_pop($this->rewriting); |
| 141 | $current = isset($this->sections[$name]) ? $this->sections[$name]->get() : ''; |
| 142 | |
| 143 | if ($this->muted > 0 || trim($content) === '' && trim($current) === '') { |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | $section = new Section(); |
| 148 | $section->setValue($content); |
| 149 | $this->sections[$name] = $section; |
| 150 | $this->captured[$name] = [$location, $this->position()]; |
| 151 | }; |
| 152 | } |
| 153 | |
| 154 | /** Whether an open `section()`, `append()`, or `prepend()` block writes the section. */ |
| 155 | public function writing(string $name): bool |
| 156 | { |
| 157 | return in_array($name, $this->writing, true); |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * Renders an included template. Its appends and those of its own layouts |
| 162 | * stay together at the place of the include among the caller's. |
| 163 | * |
| 164 | * @param Closure(): string $render |
| 165 | */ |
| 166 | public function nest(Closure $render): string |
| 167 | { |
| 168 | $include = $this->include; |
| 169 | $level = $this->level; |
| 170 | $muted = $this->muted; |
| 171 | $writing = $this->writing; |
| 172 | $rewriting = $this->rewriting; |
| 173 | $this->include = [...$include, $level, ++$this->calls]; |
| 174 | |
| 175 | try { |
| 176 | return $render(); |
| 177 | } finally { |
| 178 | $this->include = $include; |
| 179 | $this->level = $level; |
| 180 | // A failed include that the caller catches may leave blocks open. |
| 181 | $this->muted = $muted; |
| 182 | $this->writing = $writing; |
| 183 | $this->rewriting = $rewriting; |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | /** Moves on to the next layout of the rendering template. */ |
| 188 | public function enterLayout(): void |
| 189 | { |
| 190 | $this->level++; |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * The default stands in for main content that was never captured. A |
| 195 | * closure runs only then, and before the additions are collected, so |
| 196 | * those it makes to this section count. |
| 197 | * |
| 198 | * @param string|Closure(): string $default |
| 199 | */ |
| 200 | public function getOr(string $name, string|Closure $default): string |
| 201 | { |
| 202 | if (isset($this->captured[$name])) { |
| 203 | return $this->sections[$name]->get(); |
| 204 | } |
| 205 | |
| 206 | $default = is_string($default) ? $default : $default(); |
| 207 | |
| 208 | return ($this->sections[$name] ?? null)?->get($default) ?? $default; |
| 209 | } |
| 210 | |
| 211 | public function has(string $name): bool |
| 212 | { |
| 213 | return isset($this->sections[$name]); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Sets only the main content; prepended and appended content stays. |
| 218 | * |
| 219 | * @param list<int> $position |
| 220 | */ |
| 221 | private function assign(string $name, string $content, Location $location, array $position): void |
| 222 | { |
| 223 | if (isset($this->captured[$name])) { |
| 224 | throw new LogicException( |
| 225 | "Section `{$name}` was already captured at {$this->captured[$name][0]}; " |
| 226 | . 'add to it with append() or prepend(), or capture defaults in a layout', |
| 227 | location: $location, |
| 228 | ); |
| 229 | } |
| 230 | |
| 231 | $this->captured[$name] = [$location, $position]; |
| 232 | ($this->sections[$name] ??= new Section())->setValue($content); |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Registers an open block that writes the section and returns what closes |
| 237 | * it, which runs $close once the block is no longer open. |
| 238 | * |
| 239 | * @param ?Closure(string, Location): void $close |
| 240 | * |
| 241 | * @return Closure(string, Location): void |
| 242 | */ |
| 243 | private function write(string $name, ?Closure $close = null): Closure |
| 244 | { |
| 245 | $this->assertWritable($name); |
| 246 | $this->writing[] = $name; |
| 247 | |
| 248 | return function (string $content, Location $location) use ($close): void { |
| 249 | array_pop($this->writing); |
| 250 | |
| 251 | if ($close !== null) { |
| 252 | $close($content, $location); |
| 253 | } |
| 254 | }; |
| 255 | } |
| 256 | |
| 257 | private function assertWritable(string $name): void |
| 258 | { |
| 259 | if (in_array($name, $this->rewriting, true)) { |
| 260 | throw new LogicException( |
| 261 | "Section `{$name}` is being rewritten; add to it before the rewrite() block or print the content inside it", |
| 262 | ); |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | /** @return list<int> */ |
| 267 | private function position(): array |
| 268 | { |
| 269 | return [...$this->include, $this->level, ++$this->calls]; |
| 270 | } |
| 271 | } |