• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 11310141859

12 Oct 2024 06:15PM UTC coverage: 82.1% (-0.03%) from 82.134%
11310141859

push

github

web-flow
chore: local defined variables cleanup (#580)

1 of 1 new or added line in 1 file covered. (100.0%)

62 existing lines in 6 files now uncovered.

6770 of 8246 relevant lines covered (82.1%)

38.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Tempest/Console/src/Components/InteractiveComponentRenderer.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Console\Components;
6

7
use Fiber;
8
use Tempest\Console\Console;
9
use Tempest\Console\Exceptions\InterruptException;
10
use Tempest\Console\HandlesKey;
11
use Tempest\Console\InteractiveConsoleComponent;
12
use Tempest\Console\Key;
13
use Tempest\Console\Terminal\Terminal;
14
use Tempest\Reflection\ClassReflector;
15
use Tempest\Reflection\MethodReflector;
16
use Tempest\Validation\Exceptions\InvalidValueException;
17
use Tempest\Validation\Rule;
18
use Tempest\Validation\Validator;
19

20
final class InteractiveComponentRenderer
21
{
22
    private array $validationErrors = [];
23

24
    private bool $shouldRerender = true;
25

26
    public function render(Console $console, InteractiveConsoleComponent $component, array $validation = []): mixed
×
27
    {
28
        $clone = clone $this;
×
29

30
        return $clone->renderComponent($console, $component, $validation);
×
31
    }
32

33
    private function renderComponent(Console $console, InteractiveConsoleComponent $component, array $validation = []): mixed
×
34
    {
35
        $terminal = $this->createTerminal($console);
×
36

37
        $fibers = [
×
38
            new Fiber(fn () => $this->applyKey($component, $console, $validation)),
×
39
            new Fiber(fn () => $this->renderFrames($component, $terminal)),
×
40
        ];
×
41

42
        try {
43
            while ($fibers !== []) {
×
44
                foreach ($fibers as $key => $fiber) {
×
45
                    if (! $fiber->isStarted()) {
×
46
                        $fiber->start();
×
47
                    }
48

49
                    $fiber->resume();
×
50

51
                    if ($fiber->isTerminated()) {
×
52
                        unset($fibers[$key]);
×
53

54
                        $return = $fiber->getReturn();
×
55

56
                        if ($return !== null) {
×
57
                            $this->closeTerminal($terminal);
×
58

59
                            return $return;
×
60
                        }
61
                    }
62
                }
63

64
                // If we're running within a fiber, we'll suspend here as well so that the parent can continue
65
                // This is needed for our testing helper
66
                if (Fiber::getCurrent() !== null) {
×
67
                    Fiber::suspend();
×
68
                }
69
            }
70
        } catch (InterruptException $interruptException) {
×
71
            $this->closeTerminal($terminal);
×
72

73
            throw $interruptException;
×
74
        }
75

76
        $this->closeTerminal($terminal);
×
77

78
        return null;
×
79
    }
80

81
    private function applyKey(InteractiveConsoleComponent $component, Console $console, array $validation): mixed
×
82
    {
83
        [$keyBindings, $inputHandlers] = $this->resolveHandlers($component);
×
84

85
        while (true) {
×
86
            usleep(100);
×
87
            $key = $console->read(16);
×
88

89
            // If there's no keypress, continue
90
            if ($key === '') {
×
91
                Fiber::suspend();
×
92

93
                continue;
×
94
            }
95

96
            // If ctrl+c or ctrl+d, we'll exit
97
            if ($key === Key::CTRL_C->value || $key === Key::CTRL_D->value) {
×
98
                throw new InterruptException();
×
99
            }
100

101
            $this->shouldRerender = true;
×
102

103
            $return = null;
×
104

105
            /** @var MethodReflector $handler */
106
            if ($handlersForKey = $keyBindings[$key] ?? null) {
×
107
                // Apply specific key handlers
108
                foreach ($handlersForKey as $handler) {
×
109
                    $return ??= $handler->invokeArgs($component);
×
110
                }
111
            } else {
112
                // Apply catch-all key handlers
113
                foreach ($inputHandlers as $handler) {
×
114
                    $return ??= $handler->invokeArgs($component, [$key]);
×
115
                }
116
            }
117

118
            // If nothing's returned, we can continue waiting for the next key press
119
            if ($return === null) {
×
120
                Fiber::suspend();
×
121

122
                continue;
×
123
            }
124

125
            // If something's returned, we'll need to validate the result
126
            $this->validationErrors = [];
×
127

128
            $failingRule = $this->validate($return, $validation);
×
129

130
            // If invalid, we'll remember the validation message and continue
131
            if ($failingRule !== null) {
×
132
                $this->validationErrors[] = '<error>' . $failingRule->message() . '</error>';
×
133
                Fiber::suspend();
×
134

135
                continue;
×
136
            }
137

138
            // If valid, we can return
139
            return $return;
×
140
        }
141
    }
142

143
    private function renderFrames(InteractiveConsoleComponent $component, Terminal $terminal): mixed
×
144
    {
145
        while (true) {
×
146
            usleep(100);
×
147

148
            // If there are no updates,
149
            // we won't spend time re-rendering the same frame
150
            if (! $this->shouldRerender) {
×
151
                Fiber::suspend();
×
152

153
                continue;
×
154
            }
155

156
            // Rerender the frames, it could be one or more
157
            $frames = $terminal->render(
×
158
                component: $component,
×
159
                footerLines: $this->validationErrors,
×
160
            );
×
161

162
            // Looping over the frames will display them
163
            // (this happens within the Terminal class, might need to refactor)
164
            // We suspend between each frame to allow key press interruptions
165
            foreach ($frames as $frame) {
×
166
                Fiber::suspend();
×
167
            }
168

169
            $return = $frames->getReturn();
×
170

171
            // Everything's rerendered
172
            $this->shouldRerender = false;
×
173

174
            if ($return !== null) {
×
175
                return $return;
×
176
            }
177
        }
178
    }
179

180
    private function resolveHandlers(InteractiveConsoleComponent $component): array
×
181
    {
182
        /** @var \Tempest\Reflection\MethodReflector[][] $keyBindings */
183
        $keyBindings = [];
×
184

185
        $inputHandlers = [];
×
186

187
        foreach ((new ClassReflector($component))->getPublicMethods() as $method) {
×
188
            foreach ($method->getAttributes(HandlesKey::class) as $handlesKey) {
×
189
                if ($handlesKey->key === null) {
×
190
                    $inputHandlers[] = $method;
×
191
                } else {
192
                    $keyBindings[$handlesKey->key->value][] = $method;
×
193
                }
194
            }
195
        }
196

197
        return [$keyBindings, $inputHandlers];
×
198
    }
199

200
    /**
201
     * @param \Tempest\Validation\Rule[] $validation
202
     */
203
    private function validate(mixed $value, array $validation): ?Rule
×
204
    {
205
        $validator = new Validator();
×
206

207
        try {
208
            $validator->validateValue($value, $validation);
×
209
        } catch (InvalidValueException $invalidValueException) {
×
210
            return $invalidValueException->failingRules[0];
×
211
        }
212

213
        return null;
×
214
    }
215

216
    private function createTerminal(Console $console): Terminal
×
217
    {
218
        $terminal = new Terminal($console);
×
219
        $terminal->cursor->clearAfter();
×
220
        stream_set_blocking(STDIN, false);
×
221

222
        return $terminal;
×
223
    }
224

225
    private function closeTerminal(Terminal $terminal): void
×
226
    {
227
        $terminal->cursor->moveDown()->clearLine()->moveUp();
×
228
        $terminal->switchToNormalMode();
×
UNCOV
229
        stream_set_blocking(STDIN, true);
×
230
    }
231
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc