Completed
Pull Request — master (#89)
by Maxime
01:49
created

JsDumper   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 293
Duplicated Lines 15.02 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 50
lcom 1
cbo 1
dl 44
loc 293
rs 8.4
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A dumpLibrarySources() 0 7 2
A dumpEnumToFile() 0 19 3
A dumpEnumClass() 0 11 1
A dumpImports() 0 17 3
A startClass() 0 13 3
A generateEnumerableValues() 4 10 3
A generateMasks() 4 14 4
B generateReadables() 22 49 7
B getEnumerableValues() 0 23 8
B getMasks() 0 27 7
A getShortName() 0 6 1
A getConstants() 14 26 3
A normalizePath() 0 16 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like JsDumper 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 JsDumper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the "elao/enum" package.
5
 *
6
 * Copyright (C) Elao
7
 *
8
 * @author Elao <[email protected]>
9
 */
10
11
namespace Elao\Enum\JsDumper;
12
13
use Elao\Enum\FlaggedEnum;
14
use Elao\Enum\ReadableEnumInterface;
15
use Symfony\Component\Filesystem\Filesystem;
16
17
/**
18
 * @internal
19
 */
20
class JsDumper
21
{
22
    const DISCLAIMER = <<<JS
23
/*
24
 * This file was generated by the "elao/enum" PHP package.
25
 * The code inside belongs to you and can be altered, but no BC promise is guaranteed.
26
 */
27
JS;
28
    /** @var string */
29
    private $libPath;
30
31
    /** @var string|null */
32
    private $baseDir;
33
34
    /** @var Filesystem */
35
    private $fs;
36
37
    public function __construct(string $libPath, string $baseDir = null)
38
    {
39
        $this->fs = new Filesystem();
40
        $this->baseDir = $baseDir;
41
        $this->libPath = $this->normalizePath($libPath);
42
    }
43
44
    public function dumpLibrarySources()
45
    {
46
        $disclaimer = self::DISCLAIMER;
47
        $sourceCode = file_get_contents(__DIR__ . '/../../res/js/Enum.js');
48
        $this->baseDir !== null && $this->fs->mkdir($this->baseDir);
49
        $this->fs->dumpFile($this->libPath, "$disclaimer\n\n$sourceCode");
50
    }
51
52
    /**
53
     * @param class-string<EnumInterface> $enumFqcn
54
     */
55
    public function dumpEnumToFile(string $enumFqcn, string $path)
56
    {
57
        !file_exists($this->libPath) && $this->dumpLibrarySources();
58
        $this->baseDir !== null && $this->fs->mkdir($this->baseDir);
59
60
        $path = $this->normalizePath($path);
61
62
        $disclaimer = self::DISCLAIMER;
63
        $this->fs->dumpFile($path, "$disclaimer\n\n");
64
65
        $code = '';
66
        $code .= $this->dumpImports($path, $enumFqcn);
67
        $code .= $this->dumpEnumClass($enumFqcn);
68
        // End file with export
69
        $code .= "\nexport default {$this->getShortName($enumFqcn)}\n";
70
71
        // Dump to file:
72
        $this->fs->appendToFile($path, $code);
73
    }
74
75
    /**
76
     * @param class-string<EnumInterface> $enumFqcn
77
     */
78
    public function dumpEnumClass(string $enumFqcn): string
79
    {
80
        $code = '';
81
        $code .= $this->startClass($enumFqcn);
82
        $code .= $this->generateEnumerableValues($enumFqcn);
83
        $code .= $this->generateMasks($enumFqcn);
84
        $code .= $this->generateReadables($enumFqcn);
85
        $code .= "}\n"; // End class
86
87
        return $code;
88
    }
89
90
    private function dumpImports(string $path, string $enumFqcn): string
91
    {
92
        $relativeLibPath = preg_replace('#.js$#', '', rtrim(
93
            $this->fs->makePathRelative(realpath($this->libPath), realpath(\dirname($path))),
94
            DIRECTORY_SEPARATOR
95
        ));
96
97
        if (is_a($enumFqcn, FlaggedEnum::class, true)) {
98
            return "import { FlaggedEnum } from '$relativeLibPath';\n\n";
99
        }
100
101
        if (is_a($enumFqcn, ReadableEnumInterface::class, true)) {
102
            return "import { ReadableEnum } from '$relativeLibPath';\n\n";
103
        }
104
105
        return "import Enum from '$relativeLibPath';\n\n";
106
    }
107
108
    private function startClass(string $enumFqcn): string
109
    {
110
        $shortName = $this->getShortName($enumFqcn);
111
        $baseClass = 'Enum';
112
113
        if (is_a($enumFqcn, FlaggedEnum::class, true)) {
114
            $baseClass = 'FlaggedEnum';
115
        } elseif (is_a($enumFqcn, ReadableEnumInterface::class, true)) {
116
            $baseClass = 'ReadableEnum';
117
        }
118
119
        return "class $shortName extends $baseClass {\n";
120
    }
121
122
    private function generateEnumerableValues(string $enumFqcn): string
123
    {
124
        $code = '';
125 View Code Duplication
        foreach ($this->getEnumerableValues($enumFqcn) as $constant => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
126
            $jsValue = \is_string($value) ? "'$value'" : $value;
127
            $code .= "  static $constant = $jsValue\n";
128
        }
129
130
        return $code;
131
    }
132
133
    private function generateMasks(string $enumFqcn)
134
    {
135
        if (!is_a($enumFqcn, FlaggedEnum::class, true)) {
136
            return '';
137
        }
138
139
        $code = "\n  // Named masks\n";
140 View Code Duplication
        foreach ($this->getMasks($enumFqcn) as $constant => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
141
            $jsValue = \is_string($value) ? "'$value'" : $value;
142
            $code .= "  static $constant = $jsValue\n";
143
        }
144
145
        return $code;
146
    }
147
148
    private function generateReadables(string $enumFqcn): string
149
    {
150
        if (!is_a($enumFqcn, ReadableEnumInterface::class, true)) {
151
            return '';
152
        }
153
154
        $shortName = $this->getShortName($enumFqcn);
155
        // Get readable entries
156
        $readablesCode = '';
157
        $readables = $enumFqcn::readables();
158
159
        // Generate all values
160 View Code Duplication
        foreach ($this->getEnumerableValues($enumFqcn) as $constant => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
161
            if (!$readable = $readables[$value] ?? false) {
162
                continue;
163
            }
164
165
            $readablesCode .=
166
                    <<<JS
167
168
      [{$shortName}.{$constant}]: '{$readable}',
169
JS;
170
        }
171
172
        if (is_a($enumFqcn, FlaggedEnum::class, true)) {
173
            $readablesCode .= "\n\n      // Named masks";
174 View Code Duplication
            foreach ($this->getMasks($enumFqcn) as $constant => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
                if (!$readable = $readables[$value] ?? false) {
176
                    continue;
177
                }
178
179
                $readablesCode .=
180
                    <<<JS
181
182
      [{$shortName}.{$constant}]: '{$readable}',
183
JS;
184
            }
185
        }
186
187
        // Generate readables method:
188
        return <<<JS
189
190
  static get readables() {
191
    return {{$readablesCode}
192
    };
193
  }
194
195
JS;
196
    }
197
198
    /**
199
     * @param class-string<EnumInterface> $enumFqcn
200
     */
201
    private function getEnumerableValues(string $enumFqcn): array
202
    {
203
        $constants = $this->getConstants($enumFqcn);
204
205
        $enumerableValues = [];
206
        foreach ($constants as $constant) {
207
            $value = \constant("$enumFqcn::$constant");
208
209
            if (is_a($enumFqcn, FlaggedEnum::class, true)) {
210
                // Continue if not a bit flag:
211
                if (!(\is_int($value) && 0 === ($value & $value - 1) && $value > 0)) {
212
                    continue;
213
                }
214
            } elseif (!\is_int($value) && !\is_string($value)) {
215
                // Continue if not an int or string:
216
                continue;
217
            }
218
219
            $enumerableValues[$constant] = $value;
220
        }
221
222
        return $enumerableValues;
223
    }
224
225
    /**
226
     * @param class-string<FlaggedEnum> $enumFqcn
227
     */
228
    private function getMasks(string $enumFqcn): array
229
    {
230
        if (!is_a($enumFqcn, FlaggedEnum::class, true)) {
231
            return [];
232
        }
233
234
        $constants = $this->getConstants($enumFqcn);
235
236
        $masks = [];
237
        foreach ($constants as $constant) {
238
            $value = \constant("$enumFqcn::$constant");
239
240
            // Continue if it's not part of the flagged enum bitmask:
241
            if (!\is_int($value) || $value <= 0 || !$enumFqcn::accepts($value)) {
242
                continue;
243
            }
244
245
            // Continue it's a single bit flag:
246
            if (\in_array($value, $enumFqcn::values(), true)) {
247
                continue;
248
            }
249
250
            $masks[$constant] = $value;
251
        }
252
253
        return $masks;
254
    }
255
256
    /**
257
     * @param class-string<EnumInterface> $enumFqcn
258
     */
259
    private function getShortName(string $enumFqcn): string
260
    {
261
        static $cache = [];
262
263
        return $cache[$enumFqcn] = $cache[$enumFqcn] ?? (new \ReflectionClass($enumFqcn))->getShortName();
264
    }
265
266
    /**
267
     * @param class-string<EnumInterface> $enumFqcn
268
     */
269
    private function getConstants(string $enumFqcn): array
270
    {
271
        $r = new \ReflectionClass($enumFqcn);
272
        $constants = array_filter(
273
            $r->getConstants(),
274 View Code Duplication
            static function (string $k) use ($r, $enumFqcn) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
275
                if (PHP_VERSION_ID >= 70100) {
276
                    // ReflectionClass::getReflectionConstant() is only available since PHP 7.1
277
                    $rConstant = $r->getReflectionConstant($k);
278
                    $public = $rConstant->isPublic();
279
                    $value = $rConstant->getValue();
280
                } else {
281
                    $public = true;
282
                    $value = \constant("{$r->getName()}::$k");
0 ignored issues
show
Bug introduced by
Consider using $r->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
283
                }
284
285
                // Only keep public constants, for which value matches enumerable values set:
286
                return $public && $enumFqcn::accepts($value);
287
            },
288
            ARRAY_FILTER_USE_KEY
289
        );
290
291
        $constants = array_flip($constants);
292
293
        return $constants;
294
    }
295
296
    public function normalizePath(string $path): string
297
    {
298
        if (null === $this->baseDir) {
299
            return $path;
300
        }
301
302
        if ($this->fs->isAbsolutePath($path)) {
303
            return $path;
304
        }
305
306
        if (0 === strpos($path, './')) {
307
            return $path;
308
        }
309
310
        return rtrim($this->baseDir, DIRECTORY_SEPARATOR) . '/' . $path;
311
    }
312
}
313