Passed
Push — master ( 4505d6...4505d6 )
by Alec
02:17
created

Spinner   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Test Coverage

Coverage 99.27%

Importance

Changes 29
Bugs 0 Features 0
Metric Value
wmc 52
eloc 142
c 29
b 0
f 0
dl 0
loc 299
ccs 136
cts 137
cp 0.9927
rs 7.44

19 Methods

Rating   Name   Duplication   Size   Complexity  
A refineOutput() 0 7 2
A defaultSettings() 0 7 1
A __construct() 0 13 2
A progress() 0 4 1
A initJugglers() 0 12 3
A inline() 0 5 2
A begin() 0 13 3
A spin() 0 8 2
A setMessage() 0 19 6
B preparedStr() 0 32 7
A erase() 0 8 2
A end() 0 8 2
A refineSettings() 0 12 3
A interval() 0 3 1
A assertOutput() 0 9 5
A setProgress() 0 12 4
A message() 0 4 1
A assertSettings() 0 5 4
A getOutput() 0 3 1

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\Cursor;
6
use AlecRabbit\Spinner\Core\Adapters\EchoOutputAdapter;
7
use AlecRabbit\Spinner\Core\Contracts\Frames;
8
use AlecRabbit\Spinner\Core\Contracts\SpinnerInterface;
9
use AlecRabbit\Spinner\Core\Contracts\SpinnerOutputInterface;
10
use AlecRabbit\Spinner\Core\Contracts\StylesInterface;
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\typeOf;
17
use const AlecRabbit\ESC;
18
19
abstract class Spinner implements SpinnerInterface
20
{
21
    protected const EMPTY_STRING = Defaults::EMPTY;
22
23
    protected const INTERVAL = Defaults::DEFAULT_INTERVAL;
24
    protected const FRAMES = Frames::BASE;
25
    protected const STYLES = StylesInterface::STYLING_DISABLED;
26
27
    /** @var null|SpinnerOutputInterface */
28
    protected $output;
29
    /** @var Settings */
30
    protected $settings;
31
    /** @var bool */
32
    protected $inline = false;
33
    /** @var bool */
34
    protected $progressOrMessageUpdated = false;
35
    /** @var float */
36
    protected $interval;
37
    /** @var null|MessageJuggler */
38
    protected $messageJuggler;
39
    /** @var null|FrameJuggler */
40
    protected $frameJuggler;
41
    /** @var null|ProgressJuggler */
42
    protected $progressJuggler;
43
    /** @var string */
44
    protected $moveCursorBackSequence = '';
45
    /** @var string */
46
    protected $eraseBySpacesSequence = '';
47
    /** @var string */
48
    protected $spacer = Defaults::EMPTY;
49
    /** @var Style */
50
    protected $style;
51
52
    /**
53
     * Spinner constructor.
54
     *
55
     * @param null|string|Settings $messageOrSettings
56
     * @param null|false|SpinnerOutputInterface $output
57
     * @param null|int $color
58
     */
59 26
    public function __construct($messageOrSettings = null, $output = null, ?int $color = null)
60
    {
61 26
        $this->output = $this->refineOutput($output);
62 24
        $this->settings = $this->refineSettings($messageOrSettings);
63 23
        $this->interval = $this->settings->getInterval();
64 23
        $this->initJugglers();
65
        try {
66 23
            $this->style = new Style($this->settings->getStyles(), $color);
67 1
        } catch (\Throwable $e) {
68 1
            throw new \InvalidArgumentException(
69 1
                '[' . static::class . '] ' . $e->getMessage(),
70 1
                (int)$e->getCode(),
71 1
                $e
72
            );
73
        }
74 22
    }
75
76
    /**
77
     * @param null|false|SpinnerOutputInterface $output
78
     * @return null|SpinnerOutputInterface
79
     */
80 26
    protected function refineOutput($output): ?SpinnerOutputInterface
81
    {
82 26
        $this->assertOutput($output);
83 24
        if (false === $output) {
84 19
            return null;
85
        }
86 5
        return $output ?? new EchoOutputAdapter();
87
    }
88
89
    /**
90
     * @param mixed $output
91
     */
92 26
    protected function assertOutput($output): void
93
    {
94 26
        if (null !== $output && false !== $output && !$output instanceof SpinnerOutputInterface) {
95
            $typeOrValue =
96 2
                true === $output ? 'true' : typeOf($output);
97 2
            throw new \InvalidArgumentException(
98
                'Incorrect parameter: ' .
99
                '[null|false|' . SpinnerOutputInterface::class . '] expected'
100 2
                . ' "' . $typeOrValue . '" given.'
101
            );
102
        }
103 24
    }
104
105
    /**
106
     * @param null|string|Settings $settings
107
     * @return Settings
108
     */
109 24
    protected function refineSettings($settings): Settings
110
    {
111 24
        $this->assertSettings($settings);
112 23
        if (\is_string($settings)) {
113
            return
114 17
                $this->defaultSettings()->setMessage($settings);
115
        }
116 6
        if ($settings instanceof Settings) {
117 4
            return $this->defaultSettings()->merge($settings);
118
        }
119
        return
120 2
            $this->defaultSettings();
121
    }
122
123
    /**
124
     * @param mixed $settings
125
     */
126 24
    protected function assertSettings($settings): void
127
    {
128 24
        if (null !== $settings && !\is_string($settings) && !$settings instanceof Settings) {
129 1
            throw new \InvalidArgumentException(
130 1
                'Instance of [' . Settings::class . '] or string expected ' . typeOf($settings) . ' given.'
131
            );
132
        }
133 23
    }
134
135
    /**
136
     * @return Settings
137
     */
138 23
    protected function defaultSettings(): Settings
139
    {
140
        return
141 23
            (new Settings())
142 23
                ->setInterval(static::INTERVAL)
143 23
                ->setFrames(static::FRAMES)
144 23
                ->setStyles(static::STYLES);
145
    }
146
147 23
    protected function initJugglers(): void
148
    {
149 23
        $frames = $this->settings->getFrames();
150 23
        if (!empty($frames)) {
151 18
            $this->frameJuggler =
152 18
                new FrameJuggler(
153 18
                    $frames
154
                );
155
        }
156 23
        $message = $this->settings->getMessage();
157 23
        if (Defaults::EMPTY !== $message) {
158 19
            $this->setMessage($message, $this->settings->getMessageErasingLen());
159
        }
160 23
    }
161
162
    /**
163
     * @param null|string $message
164
     * @param null|int $erasingLength
165
     */
166 20
    protected function setMessage(?string $message, ?int $erasingLength = null): void
167
    {
168 20
        $firstInLine = true;
169 20
        if ($this->frameJuggler instanceof FrameJuggler) {
170 16
            $firstInLine = false;
171
        }
172 20
        if ($this->messageJuggler instanceof MessageJuggler) {
173 3
            if (null === $message) {
174
                $this->messageJuggler = null;
175
            } else {
176 3
                $this->messageJuggler->setMessage($message, $erasingLength);
177
            }
178 3
            $this->progressOrMessageUpdated = true;
179
        } else {
180 20
            $this->messageJuggler =
181 20
                null === $message ? null : new MessageJuggler($message, $erasingLength);
182
        }
183 20
        if ($this->messageJuggler instanceof MessageJuggler) {
184 20
            $this->messageJuggler->firstInLine($firstInLine);
185
        }
186 20
    }
187
188
    /** {@inheritDoc} */
189 18
    public function end(): string
190
    {
191 18
        if ($this->output instanceof SpinnerOutputInterface) {
192 3
            $this->erase();
193 3
            $this->output->write(Cursor::show());
194 3
            return self::EMPTY_STRING;
195
        }
196 15
        return $this->erase() . Cursor::show();
197
    }
198
199
    /** {@inheritDoc} */
200 18
    public function erase(): string
201
    {
202 18
        $str = $this->eraseBySpacesSequence . $this->moveCursorBackSequence;
203 18
        if ($this->output instanceof SpinnerOutputInterface) {
204 3
            $this->output->write($str);
205 3
            return self::EMPTY_STRING;
206
        }
207 15
        return $str;
208
    }
209
210
    /** {@inheritDoc} */
211 3
    public function getOutput(): ?SpinnerOutputInterface
212
    {
213 3
        return $this->output;
214
    }
215
216 8
    public function inline(bool $inline): SpinnerInterface
217
    {
218 8
        $this->inline = $inline;
219 8
        $this->spacer = $this->inline ? Defaults::ONE_SPACE_SYMBOL : Defaults::EMPTY;
220 8
        return $this;
221
    }
222
223 5
    public function interval(): float
224
    {
225 5
        return $this->interval;
226
    }
227
228
    /** {@inheritDoc} */
229 3
    public function message(?string $message = null, ?int $erasingLength = null): self
230
    {
231 3
        $this->setMessage($message, $erasingLength);
232 3
        return $this;
233
    }
234
235 7
    public function progress(?float $percent): self
236
    {
237 7
        $this->setProgress($percent);
238 7
        return $this;
239
    }
240
241
    /**
242
     * @param null|float $percent
243
     */
244 8
    protected function setProgress(?float $percent = null): void
245
    {
246 8
        if ($this->progressJuggler instanceof ProgressJuggler) {
247 7
            if (null === $percent) {
248 1
                $this->progressJuggler = null;
249
            } else {
250 7
                $this->progressJuggler->setProgress($percent);
251
            }
252 7
            $this->progressOrMessageUpdated = true;
253
        } else {
254 8
            $this->progressJuggler =
255 8
                null === $percent ? null : new ProgressJuggler($percent);
256
        }
257 8
    }
258
259
    /** {@inheritDoc} */
260 21
    public function begin(?float $percent = null): string
261
    {
262 21
        if (null === $percent) {
263 18
            $this->progressJuggler = null;
264
        } else {
265 4
            $this->setProgress($percent);
266
        }
267 21
        if ($this->output instanceof SpinnerOutputInterface) {
268 3
            $this->output->write(Cursor::hide());
269 3
            $this->spin();
270 3
            return self::EMPTY_STRING;
271
        }
272 18
        return Cursor::hide() . $this->spin();
273
    }
274
275
    /** {@inheritDoc} */
276 21
    public function spin(): string
277
    {
278 21
        if ($this->output instanceof SpinnerOutputInterface) {
279 3
            $this->output->write($this->preparedStr());
280 3
            return self::EMPTY_STRING;
281
        }
282
        return
283 18
            $this->preparedStr();
284
    }
285
286 21
    protected function preparedStr(): string
287
    {
288
        // TODO optimize performance
289 21
        $str = '';
290 21
        $erasingLength = 0;
291 21
        $erasingLengthDelta = 0;
292 21
        $eraseMessageTailBySpacesSequence = '';
293 21
        if ($this->frameJuggler instanceof FrameJuggler) {
294 16
            $str .= $this->style->spinner($this->frameJuggler->getFrame());
295 16
            $erasingLength += $this->frameJuggler->getFrameErasingLength();
296
        }
297 21
        if ($this->messageJuggler instanceof MessageJuggler) {
298 19
            $str .= $this->style->message($this->messageJuggler->getFrame());
299 19
            $erasingLength += $this->messageJuggler->getFrameErasingLength();
300 19
            $erasingLengthDelta = $this->messageJuggler->getErasingLengthDelta();
301 19
            if ($erasingLengthDelta > 0) {
302 3
                $erasingLength += $erasingLengthDelta;
303
            } else {
304 18
                $erasingLengthDelta = 0;
305
            }
306
        }
307 21
        if ($this->progressJuggler instanceof ProgressJuggler) {
308 8
            $str .= $this->style->percent($this->progressJuggler->getFrame());
309 8
            $erasingLength += $this->progressJuggler->getFrameErasingLength();
310
        }
311 21
        $erasingLength += $this->inline ? 1 : 0;
312 21
        $this->moveCursorBackSequence = ESC . "[{$erasingLength}D";
313 21
        $this->eraseBySpacesSequence = str_repeat(Defaults::ONE_SPACE_SYMBOL, $erasingLength);
314 21
        if ($erasingLengthDelta > 0) {
315 3
            $eraseMessageTailBySpacesSequence = str_repeat(Defaults::ONE_SPACE_SYMBOL, $erasingLengthDelta);
316
        }
317 21
        return $this->spacer . $str . $eraseMessageTailBySpacesSequence . $this->moveCursorBackSequence;
318
    }
319
}
320