Passed
Push — master ( 43b6bd...c3bc4f )
by Alec
02:37
created

Spinner   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 43
Bugs 0 Features 0
Metric Value
wmc 44
eloc 129
c 43
b 0
f 0
dl 0
loc 270
ccs 129
cts 129
cp 1
rs 8.8798

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
A calcEraseSequence() 0 15 2
A progress() 0 4 1
A initJugglers() 0 11 3
A inline() 0 5 1
A begin() 0 17 5
A spin() 0 8 2
A setMessage() 0 15 4
A erase() 0 11 3
A end() 0 13 4
A updateInlineSpacerProperties() 0 4 3
A refineColor() 0 6 3
A setProgress() 0 12 4
A message() 0 4 1
A getOutput() 0 3 1
A last() 0 11 3
A prepareLastOutput() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like Spinner often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Spinner, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\Spinner\Core;
4
5
use AlecRabbit\Cli\Tools\Core\Terminal;
6
use AlecRabbit\Cli\Tools\Cursor;
7
use AlecRabbit\Spinner\Core\Coloring\Colors;
8
use AlecRabbit\Spinner\Core\Contracts\OutputInterface;
9
use AlecRabbit\Spinner\Core\Contracts\SpinnerInterface;
10
use AlecRabbit\Spinner\Core\Jugglers\Contracts\JugglerInterface;
11
use AlecRabbit\Spinner\Core\Jugglers\FrameJuggler;
12
use AlecRabbit\Spinner\Core\Jugglers\MessageJuggler;
13
use AlecRabbit\Spinner\Core\Jugglers\ProgressJuggler;
14
use AlecRabbit\Spinner\Settings\Contracts\Defaults;
15
use AlecRabbit\Spinner\Settings\Settings;
16
use function AlecRabbit\Helpers\wcswidth;
17
use const AlecRabbit\ESC;
18
use const AlecRabbit\NO_COLOR_TERMINAL;
19
20
abstract class Spinner extends SpinnerCore
21
{
22
    /** @var Settings */
23
    protected $settings;
24
    /** @var bool */
25
    protected $inline = false;
26
    /** @var null|MessageJuggler */
27
    protected $messageJuggler;
28
    /** @var null|FrameJuggler */
29
    protected $frameJuggler;
30
    /** @var null|ProgressJuggler */
31
    protected $progressJuggler;
32
    /** @var string */
33
    protected $moveCursorBackSequence = self::EMPTY_STRING;
34
    /** @var string */
35
    protected $eraseBySpacesSequence = self::EMPTY_STRING;
36
    /** @var int */
37
    protected $previousErasingWidth = 0;
38
    /** @var Colors */
39
    protected $coloring;
40
    /** @var null[]|JugglerInterface[] */
41
    protected $jugglers = [];
42
    /** @var string */
43
    protected $lastOutput = self::EMPTY_STRING;
44
    /** @var string */
45
    protected $inlineSpacer = self::EMPTY_STRING;
46
    /** @var bool */
47
    protected $hideCursor;
48
    /** @var int */
49
    protected $inlineSpacerWidth;
50
    /** @var string */
51
    protected $spacerValue;
52
53
    /**
54
     * Spinner constructor.
55
     *
56
     * @param null|string|Settings $messageOrSettings
57
     * @param null|false|OutputInterface $output
58
     * @param null|int $color
59
     */
60 30
    public function __construct($messageOrSettings = null, $output = null, ?int $color = null)
61
    {
62 30
        $this->output = $this->refineOutput($output);
63 28
        $this->settings = $this->refineSettings($messageOrSettings);
64 27
        $this->interval = $this->settings->getInterval();
65 27
        $this->hideCursor = $this->settings->isHideCursor();
66 27
        $this->enabled = $this->settings->isEnabled();
67 27
        $this->spacerValue = $this->settings->getInlineSpacer();
68 27
        $this->updateInlineSpacerProperties();
69 27
        $this->coloring = new Colors($this->settings->getStyles(), $this->refineColor($color));
70 27
        $jugglerOrder = $this->settings->getJugglersOrder();
71 27
        $this->jugglers = [
72 27
            $jugglerOrder[0] => &$this->frameJuggler,
73 27
            $jugglerOrder[1] => &$this->messageJuggler,
74 27
            $jugglerOrder[2] => &$this->progressJuggler,
75
        ];
76 27
        ksort($this->jugglers);
77 27
        $this->initJugglers();
78 27
    }
79
80
    /**
81
     * @param null|int $color
82
     * @return int
83
     */
84 27
    protected function refineColor(?int $color): int
85
    {
86 27
        if (null === $color && $this->output instanceof OutputInterface) {
87 1
            $color = Terminal::colorSupport($this->output->getStream());
88
        }
89 27
        return $color ?? NO_COLOR_TERMINAL;
90
    }
91
92 27
    protected function initJugglers(): void
93
    {
94 27
        $frames = $this->settings->getFrames();
95 27
        if (!empty($frames)) {
96 21
            $this->frameJuggler =
97 21
                new FrameJuggler($this->settings, $this->coloring->getFrameStyles());
98
        }
99
100 27
        $message = $this->settings->getMessage();
101 27
        if (self::EMPTY_STRING !== $message) {
102 23
            $this->setMessage($message);
103
        }
104 27
    }
105
106
    /**
107
     * @param null|string $message
108
     */
109 24
    protected function setMessage(?string $message): void
110
    {
111 24
        $this->settings->setMessage($message);
112
113 24
        if ($this->messageJuggler instanceof MessageJuggler) {
114 6
            if (null === $message) {
115 3
                $this->messageJuggler = null;
116
            } else {
117 6
                $this->messageJuggler->setMessage($message);
118
            }
119
        } else {
120 24
            $this->messageJuggler =
121 24
                null === $message ?
122 1
                    null :
123 24
                    new MessageJuggler($this->settings, $this->coloring->getMessageStyles());
124
        }
125 24
    }
126
127
    /** {@inheritDoc} */
128 23
    public function end(?string $finalMessage = null): string
129
    {
130 23
        if (!$this->enabled) {
131 1
            return self::EMPTY_STRING;
132
        }
133 23
        $finalMessage = (string)$finalMessage;
134 23
        $showCursor = $this->hideCursor ? Cursor::show() : self::EMPTY_STRING;
135 23
        if ($this->output instanceof OutputInterface) {
136 3
            $this->erase();
137 3
            $this->output->write($showCursor . $this->inlineSpacer . $finalMessage);
138 3
            return self::EMPTY_STRING;
139
        }
140 20
        return $this->erase() . $showCursor . $this->inlineSpacer . $finalMessage;
141
    }
142
143
    /** {@inheritDoc} */
144 23
    public function erase(): string
145
    {
146 23
        if (!$this->enabled) {
147 1
            return self::EMPTY_STRING;
148
        }
149 23
        $str = $this->eraseBySpacesSequence . $this->moveCursorBackSequence;
150 23
        if ($this->output instanceof OutputInterface) {
151 3
            $this->output->write($str);
152 3
            return self::EMPTY_STRING;
153
        }
154 20
        return $str;
155
    }
156
157
    /** {@inheritDoc} */
158 5
    public function getOutput(): ?OutputInterface
159
    {
160 5
        return $this->output;
161
    }
162
163 8
    public function inline(bool $inline): SpinnerInterface
164
    {
165 8
        $this->inline = $inline;
166 8
        $this->updateInlineSpacerProperties();
167 8
        return $this;
168
    }
169
170
    /** {@inheritDoc} */
171 6
    public function message(?string $message): self
172
    {
173 6
        $this->setMessage($message);
174 6
        return $this;
175
    }
176
177 11
    public function progress(?float $percent): self
178
    {
179 11
        $this->setProgress($percent);
180 11
        return $this;
181
    }
182
183
    /**
184
     * @param null|float $percent
185
     */
186 12
    protected function setProgress(?float $percent = null): void
187
    {
188 12
        $this->settings->setInitialPercent($percent);
189 12
        if ($this->progressJuggler instanceof ProgressJuggler) {
190 11
            if (null === $percent) {
191 2
                $this->progressJuggler = null;
192
            } else {
193 11
                $this->progressJuggler->setProgress($percent);
194
            }
195
        } else {
196 12
            $this->progressJuggler =
197 12
                null === $percent ? null : new ProgressJuggler($this->settings, $this->coloring->getProgressStyles());
198
        }
199 12
    }
200
201
    /** {@inheritDoc} */
202 26
    public function begin(?float $percent = null): string
203
    {
204 26
        if (!$this->enabled) {
205 1
            return self::EMPTY_STRING;
206
        }
207 26
        if (null === $percent) {
208 22
            $this->progressJuggler = null;
209
        } else {
210 5
            $this->setProgress($percent);
211
        }
212 26
        $hideCursor = $this->hideCursor ? Cursor::hide() : self::EMPTY_STRING;
213 26
        if ($this->output instanceof OutputInterface) {
214 3
            $this->output->write($hideCursor);
215 3
            $this->spin();
216 3
            return self::EMPTY_STRING;
217
        }
218 23
        return $hideCursor . $this->spin();
219
    }
220
221
    /** {@inheritDoc} */
222 26
    public function spin(): string
223
    {
224 26
        if (!$this->enabled) {
225 1
            return self::EMPTY_STRING;
226
        }
227 26
        $this->lastOutput = $this->prepareLastOutput();
228
        return
229 26
            $this->last();
230
    }
231
232 26
    protected function prepareLastOutput(): string
233
    {
234
//        $start = hrtime(true);
235 26
        $str = '';
236 26
        $erasingWidth = 0;
237 26
        foreach ($this->jugglers as $juggler) {
238 26
            if ($juggler instanceof JugglerInterface) {
239 25
                $str .= $juggler->getStyledFrame();
240 25
                $erasingWidth += $juggler->getFrameErasingWidth();
241
            }
242
        }
243 26
        $erasingWidth += $this->inlineSpacerWidth;
244 26
        $eraseTailBySpacesSequence = $this->calcEraseSequence($erasingWidth);
245
246 26
        $str = $this->inlineSpacer . $str . $eraseTailBySpacesSequence . $this->moveCursorBackSequence;
247
//        dump(hrtime(true) - $start);
248 26
        return $str;
249
    }
250
251
    /** {@inheritDoc} */
252 26
    public function last(): string
253
    {
254 26
        if (!$this->enabled) {
255 1
            return self::EMPTY_STRING;
256
        }
257 26
        if ($this->output instanceof OutputInterface) {
258 3
            $this->output->write($this->lastOutput);
259 3
            return self::EMPTY_STRING;
260
        }
261
        return
262 23
            $this->lastOutput;
263
    }
264
265 27
    protected function updateInlineSpacerProperties(): void
266
    {
267 27
        $this->inlineSpacer = $this->inline ? $this->spacerValue : self::EMPTY_STRING;
268 27
        $this->inlineSpacerWidth = $this->inline ? wcswidth($this->inlineSpacer) : 0;
269 27
    }
270
271
    /**
272
     * @param int $erasingWidth
273
     * @return string
274
     */
275 26
    protected function calcEraseSequence(int $erasingWidth): string
276
    {
277 26
        $eraseTailBySpacesSequence = '';
278
279 26
        $erasingWidthDelta = $this->previousErasingWidth - $erasingWidth;
280 26
        $this->previousErasingWidth = $erasingWidth;
281
282 26
        if ($erasingWidthDelta > 0) {
283 7
            $erasingWidth += $erasingWidthDelta;
284 7
            $eraseTailBySpacesSequence = str_repeat(Defaults::ONE_SPACE_SYMBOL, $erasingWidthDelta);
285
        }
286 26
        $this->eraseBySpacesSequence = str_repeat(Defaults::ONE_SPACE_SYMBOL, $erasingWidth);
287 26
        $this->moveCursorBackSequence = ESC . "[{$erasingWidth}D";
288
289 26
        return $eraseTailBySpacesSequence;
290
    }
291
}
292