Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
SlotRenderer
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
4 / 4
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 renderSlot
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 wrapException
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Boiler;
6
7use Celemas\Boiler\Exception\LogicException;
8use Celemas\Boiler\Exception\RenderException;
9use Celemas\Boiler\Exception\RuntimeException;
10use Closure;
11use Stringable;
12use Throwable;
13
14/** @internal */
15final class SlotRenderer
16{
17    public function __construct(
18        private readonly Closure|Slot $slot,
19        private readonly Context $context,
20        private readonly Location $location,
21    ) {}
22
23    /** @param array<array-key, mixed> $data */
24    public function render(array $data): string
25    {
26        $level = ob_get_level();
27
28        try {
29            ob_start();
30
31            try {
32                /** @psalm-suppress MixedAssignment slot may echo, return markup, or both */
33                $returned = $this->renderSlot($data);
34            } catch (RenderException $e) {
35                throw $e;
36            } catch (RuntimeException|LogicException $e) {
37                if ($e->location() !== null) {
38                    throw $e;
39                }
40
41                throw $this->wrapException($e);
42            } catch (Throwable $e) {
43                throw $this->wrapException($e);
44            }
45
46            $captured = (string) ob_get_clean();
47
48            return is_string($returned) || $returned instanceof Stringable
49                ? $captured . (string) $returned
50                : $captured;
51        } finally {
52            while (ob_get_level() > $level) {
53                ob_end_clean();
54            }
55        }
56    }
57
58    /** @param array<array-key, mixed> $data */
59    private function renderSlot(array $data): mixed
60    {
61        if ($this->slot instanceof Closure) {
62            return ($this->slot)($data);
63        }
64
65        $this->context->insert($this->slot->path(), array_merge($this->slot->context(), $data));
66
67        return null;
68    }
69
70    private function wrapException(Throwable $exception): RuntimeException
71    {
72        $code = $exception->getCode();
73        $location = Location::fromThrowable($this->location->path, $exception);
74
75        return new RuntimeException(
76            $exception->getMessage(),
77            is_int($code) ? $code : 0,
78            $exception,
79            $location->line === null ? $this->location : $location,
80        );
81    }
82}