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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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"); |
|
|
|
|
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
|
|
|
|
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.