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

JsDumper::generateReadables()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 49

Duplication

Lines 22
Ratio 44.9 %

Importance

Changes 0
Metric Value
dl 22
loc 49
rs 8.1793
c 0
b 0
f 0
cc 7
nc 7
nop 1
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