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

tempestphp / tempest-framework / 14161923512

30 Mar 2025 01:41PM UTC coverage: 80.964% (+0.2%) from 80.716%
14161923512

push

github

web-flow
ci: prevent coveralls failures from failing tests (#1104)

11058 of 13658 relevant lines covered (80.96%)

100.68 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
use function Tempest\Support\arr;
21

22
final class InteractiveComponentRenderer
23
{
24
    private array $afterRenderCallbacks = [];
25

26
    private array $validationErrors = [];
27

28
    private bool $shouldRerender = true;
29

30
    public function render(Console $console, InteractiveConsoleComponent $component, array $validation = []): mixed
×
31
    {
32
        $clone = clone $this;
×
33

34
        return $clone->renderComponent($console, $component, $validation);
×
35
    }
36

37
    private function renderComponent(Console $console, InteractiveConsoleComponent $component, array $validation = []): mixed
×
38
    {
39
        $terminal = $this->createTerminal($console);
×
40

41
        $fibers = [
×
42
            new Fiber(fn () => $this->applyKey($component, $console, $validation)),
×
43
            new Fiber(fn () => $this->renderFrames($component, $terminal)),
×
44
        ];
×
45

46
        try {
47
            while ($fibers !== []) {
×
48
                foreach ($fibers as $key => $fiber) {
×
49
                    if (! $fiber->isStarted()) {
×
50
                        $fiber->start();
×
51
                    }
52

53
                    $fiber->resume();
×
54

55
                    if ($fiber->isTerminated()) {
×
56
                        unset($fibers[$key]);
×
57

58
                        if (! is_null($return = $fiber->getReturn())) {
×
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
        } finally {
71
            $this->closeTerminal($terminal);
×
72
        }
73

74
        return null;
×
75
    }
76

77
    private function applyKey(InteractiveConsoleComponent $component, Console $console, array $validation): mixed
×
78
    {
79
        [$keyBindings, $inputHandlers] = $this->resolveHandlers($component);
×
80

81
        while (true) {
×
82
            while ($callback = array_shift($this->afterRenderCallbacks)) {
×
83
                $callback($component);
×
84
            }
85

86
            usleep(50);
×
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
            // Otherwise, we will re-render after processing the key.
97
            $this->shouldRerender = true;
×
98

99
            if ($component->getState() === ComponentState::BLOCKED) {
×
100
                continue;
×
101
            }
102

103
            /** @var MethodReflector[] $handlersForKey */
104
            $handlersForKey = $keyBindings[$key] ?? [];
×
105

106
            // If we have multiple handlers, we put the ones that return nothing
107
            // first because the ones that return something will be overridden otherwise.
108
            usort($handlersForKey, fn (MethodReflector $a, MethodReflector $b) => $b->getReturnType()->equals('void') <=> $a->getReturnType()->equals('void'));
×
109

110
            // CTRL+C and CTRL+D means we exit the CLI, but only if there is no custom
111
            // handler. When we exit, we want one last render to display pretty
112
            // styles, so we will throw the exception in the next loop.
113
            if ($handlersForKey === [] && ($key === Key::CTRL_C->value || $key === Key::CTRL_D->value)) {
×
114
                $component->setState(ComponentState::CANCELLED);
×
115
                $this->afterRenderCallbacks[] = fn () => throw new InterruptException();
×
116
                $this->shouldRerender = true;
×
117
                Fiber::suspend();
×
118

119
                continue;
×
120
            }
121

122
            $return = null;
×
123

124
            // If we have handlers for that key, apply them.
125
            foreach ($handlersForKey as $handler) {
×
126
                $return ??= $handler->invokeArgs($component);
×
127
            }
128

129
            // If we didn't have any handler for the key,
130
            // we call catch-all handlers.
131
            if ($handlersForKey === []) {
×
132
                foreach ($inputHandlers as $handler) {
×
133
                    $return ??= $handler->invokeArgs($component, [$key]);
×
134
                }
135
            }
136

137
            // If nothing's returned, we can continue waiting for the next key press
138
            if ($return === null) {
×
139
                Fiber::suspend();
×
140

141
                continue;
×
142
            }
143

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

147
            $failingRule = $this->validate($return, $validation);
×
148

149
            // If invalid, we'll remember the validation message and continue
150
            if ($failingRule !== null) {
×
151
                $component->setState(ComponentState::ERROR);
×
152
                $this->validationErrors[] = $failingRule->message();
×
153
                Fiber::suspend();
×
154

155
                continue;
×
156
            }
157

158
            // The component is done, we can re-render and return.
159
            $component->setState(ComponentState::DONE);
×
160
            Fiber::suspend();
×
161

162
            // If valid, we can return
163
            return $return;
×
164
        }
165
    }
166

167
    private function renderFrames(InteractiveConsoleComponent $component, Terminal $terminal): mixed
×
168
    {
169
        while (true) {
×
170
            usleep(100);
×
171

172
            // If there are no updates,
173
            // we won't spend time re-rendering the same frame
174
            if (! $this->shouldRerender) {
×
175
                Fiber::suspend();
×
176

177
                continue;
×
178
            }
179

180
            // Rerender the frames, it could be one or more
181
            $frames = $terminal->render(
×
182
                component: $component,
×
183
                validationErrors: $this->validationErrors,
×
184
            );
×
185

186
            // Looping over the frames will display them
187
            // (this happens within the Terminal class, might need to refactor)
188
            // We suspend between each frame to allow key press interruptions
189
            foreach ($frames as $frame) {
×
190
                Fiber::suspend();
×
191
            }
192

193
            $return = $frames->getReturn();
×
194

195
            // Everything's rerendered
196
            $this->shouldRerender = false;
×
197

198
            if ($return !== null) {
×
199
                return $return;
×
200
            }
201
        }
202
    }
203

204
    private function resolveHandlers(InteractiveConsoleComponent $component): array
×
205
    {
206
        /** @var \Tempest\Reflection\MethodReflector[][] $keyBindings */
207
        $keyBindings = [];
×
208

209
        $inputHandlers = [];
×
210

211
        foreach (new ClassReflector($component)->getPublicMethods() as $method) {
×
212
            foreach ($method->getAttributes(HandlesKey::class) as $handlesKey) {
×
213
                if ($handlesKey->key === null) {
×
214
                    $inputHandlers[] = $method;
×
215
                } else {
216
                    $keyBindings[$handlesKey->key->value][] = $method;
×
217
                }
218
            }
219
        }
220

221
        return [$keyBindings, $inputHandlers];
×
222
    }
223

224
    /**
225
     * @param \Tempest\Validation\Rule[] $validation
226
     */
227
    private function validate(mixed $value, array $validation): ?Rule
×
228
    {
229
        return new Validator()->validateValue($value, $validation)[0] ?? null;
×
230
    }
231

232
    public function isComponentSupported(Console $console, InteractiveConsoleComponent $component): bool
×
233
    {
234
        if (! arr($component->extensions ?? [])->every(fn (string $ext) => extension_loaded($ext))) {
×
235
            return false;
×
236
        }
237

238
        if (! Terminal::supportsTty()) {
×
239
            return false;
×
240
        }
241

242
        return true;
×
243
    }
244

245
    private function createTerminal(Console $console): Terminal
×
246
    {
247
        $terminal = new Terminal($console);
×
248
        $terminal->cursor->clearAfter();
×
249
        stream_set_blocking(STDIN, false);
×
250

251
        return $terminal;
×
252
    }
253

254
    private function closeTerminal(Terminal $terminal): void
×
255
    {
256
        $terminal->placeCursorToEnd();
×
257
        $terminal->switchToNormalMode();
×
258
        stream_set_blocking(STDIN, true);
×
259
    }
260
}
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

© 2025 Coveralls, Inc