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

JsDumper::getMasks()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.5546
c 0
b 0
f 0
cc 7
nc 5
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
    public function __construct(string $libPath, string $baseDir = null)
35
    {
36
        $this->fs = new Filesystem();
0 ignored issues
show
Bug introduced by
The property fs does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
37
        $this->baseDir = $baseDir;
38
        $this->libPath = $this->normalizePath($libPath);
39
    }
40
41
    public function dumpLibrarySources()
42
    {
43
        $disclaimer = self::DISCLAIMER;
44
        $sourceCode = file_get_contents(__DIR__ . '/../../res/js/Enum.js');
45
        $this->baseDir && $this->fs->mkdir($this->baseDir);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->baseDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
46
        $this->fs->dumpFile($this->libPath, "$disclaimer\n\n$sourceCode");
47
    }
48
49
    /**
50
     * @param class-string<EnumInterface> $enumFqcn
1 ignored issue
show
Documentation introduced by
The doc-type class-string<EnumInterface> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
51
     */
52
    public function dumpEnumToFile(string $enumFqcn, string $path)
53
    {
54
        !file_exists($this->libPath) && $this->dumpLibrarySources();
55
        $this->baseDir && $this->fs->mkdir($this->baseDir);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->baseDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
56
57
        $path = $this->normalizePath($path);
58
59
        $disclaimer = self::DISCLAIMER;
60
        $this->fs->dumpFile($path, "$disclaimer\n\n");
61
62
        $code = '';
63
        $code .= $this->dumpImports($path, $enumFqcn);
64
        $code .= $this->dumpEnumClass($enumFqcn);
65
        // End file with export
66
        $code .= "\nexport default {$this->getShortName($enumFqcn)}\n";
67
68
        // Dump to file:
69
        $this->fs->appendToFile($path, $code);
70
    }
71
72
    /**
73
     * @param class-string<EnumInterface> $enumFqcn
1 ignored issue
show
Documentation introduced by
The doc-type class-string<EnumInterface> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
74
     */
75
    public function dumpEnumClass(string $enumFqcn): string
76
    {
77
        $code = '';
78
        $code .= $this->startClass($enumFqcn);
79
        $code .= $this->generateEnumerableValues($enumFqcn);
80
        $code .= $this->generateMasks($enumFqcn);
81
        $code .= $this->generateReadables($enumFqcn);
82
        $code .= "}\n"; // End class
83
84
        return $code;
85
    }
86
87
    private function dumpImports(string $path, string $enumFqcn): string
88
    {
89
        $relativeLibPath = preg_replace('#.js$#', '', rtrim(
90
            $this->fs->makePathRelative(realpath($this->libPath), realpath(\dirname($path))),
91
            DIRECTORY_SEPARATOR
92
        ));
93
94
        if (is_a($enumFqcn, FlaggedEnum::class, true)) {
95
            return "import { FlaggedEnum } from '$relativeLibPath';\n\n";
96
        }
97
98
        if (is_a($enumFqcn, ReadableEnumInterface::class, true)) {
99
            return "import { ReadableEnum } from '$relativeLibPath';\n\n";
100
        }
101
102
        return "import Enum from '$relativeLibPath';\n\n";
103
    }
104
105
    private function startClass(string $enumFqcn): string
106
    {
107
        $shortName = $this->getShortName($enumFqcn);
108
        $baseClass = 'Enum';
109
110
        if (is_a($enumFqcn, FlaggedEnum::class, true)) {
111
            $baseClass = 'FlaggedEnum';
112
        } elseif (is_a($enumFqcn, ReadableEnumInterface::class, true)) {
113
            $baseClass = 'ReadableEnum';
114
        }
115
116
        return "class $shortName extends $baseClass {\n";
117
    }
118
119
    private function generateEnumerableValues(string $enumFqcn): string
120
    {
121
        $code = '';
122 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...
123
            $jsValue = \is_string($value) ? "'$value'" : $value;
124
            $code .= "  static $constant = $jsValue\n";
125
        }
126
127
        return $code;
128
    }
129
130
    private function generateMasks(string $enumFqcn)
131
    {
132
        if (!is_a($enumFqcn, FlaggedEnum::class, true)) {
133
            return '';
134
        }
135
136
        $code = "\n  // Named masks\n";
137 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...
138
            $jsValue = \is_string($value) ? "'$value'" : $value;
139
            $code .= "  static $constant = $jsValue\n";
140
        }
141
142
        return $code;
143
    }
144
145
    private function generateReadables(string $enumFqcn): string
146
    {
147
        if (!is_a($enumFqcn, ReadableEnumInterface::class, true)) {
148
            return '';
149
        }
150
151
        $shortName = $this->getShortName($enumFqcn);
152
        // Get readable entries
153
        $readablesCode = '';
154
        $readables = $enumFqcn::readables();
155
156
        // Generate all values
157 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...
158
            if (!$readable = $readables[$value] ?? false) {
159
                continue;
160
            }
161
162
            $readablesCode .=
163
                    <<<JS
164
165
      [{$shortName}.{$constant}]: '{$readable}',
166
JS;
167
        }
168
169
        if (is_a($enumFqcn, FlaggedEnum::class, true)) {
170
            $readablesCode .= "\n\n      // Named masks";
171 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...
172
                if (!$readable = $readables[$value] ?? false) {
173
                    continue;
174
                }
175
176
                $readablesCode .=
177
                    <<<JS
178
179
      [{$shortName}.{$constant}]: '{$readable}',
180
JS;
181
            }
182
        }
183
184
        // Generate readables method:
185
        return <<<JS
186
187
  static get readables() {
188
    return {{$readablesCode}
189
    };
190
  }
191
192
JS;
193
    }
194
195
    /**
196
     * @param class-string<EnumInterface> $enumFqcn
1 ignored issue
show
Documentation introduced by
The doc-type class-string<EnumInterface> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
197
     */
198
    private function getEnumerableValues(string $enumFqcn): array
199
    {
200
        $constants = $this->getConstants($enumFqcn);
201
202
        $enumerableValues = [];
203
        foreach ($constants as $constant) {
204
            $value = \constant("$enumFqcn::$constant");
205
206
            if (is_a($enumFqcn, FlaggedEnum::class, true)) {
207
                // Continue if not a bit flag:
208
                if (!(\is_int($value) && 0 === ($value & $value - 1) && $value > 0)) {
209
                    continue;
210
                }
211
            } elseif (!\is_int($value) && !\is_string($value)) {
212
                // Continue if not an int or string:
213
                continue;
214
            }
215
216
            $enumerableValues[$constant] = $value;
217
        }
218
219
        return $enumerableValues;
220
    }
221
222
    /**
223
     * @param class-string<FlaggedEnum> $enumFqcn
1 ignored issue
show
Documentation introduced by
The doc-type class-string<FlaggedEnum> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
224
     */
225
    private function getMasks(string $enumFqcn): array
226
    {
227
        if (!is_a($enumFqcn, FlaggedEnum::class, true)) {
228
            return [];
229
        }
230
231
        $constants = $this->getConstants($enumFqcn);
232
233
        $masks = [];
234
        foreach ($constants as $constant) {
235
            $value = \constant("$enumFqcn::$constant");
236
237
            // Continue if it's not part of the flagged enum bitmask:
238
            if (!\is_int($value) || $value <= 0 || !$enumFqcn::accepts($value)) {
239
                continue;
240
            }
241
242
            // Continue it's a single bit flag:
243
            if (\in_array($value, $enumFqcn::values(), true)) {
244
                continue;
245
            }
246
247
            $masks[$constant] = $value;
248
        }
249
250
        return $masks;
251
    }
252
253
    /**
254
     * @param class-string<EnumInterface> $enumFqcn
1 ignored issue
show
Documentation introduced by
The doc-type class-string<EnumInterface> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
255
     */
256
    private function getShortName(string $enumFqcn): string
257
    {
258
        static $cache = [];
259
260
        return $cache[$enumFqcn] = $cache[$enumFqcn] ?? (new \ReflectionClass($enumFqcn))->getShortName();
261
    }
262
263
    /**
264
     * @param class-string<EnumInterface> $enumFqcn
1 ignored issue
show
Documentation introduced by
The doc-type class-string<EnumInterface> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
265
     */
266
    private function getConstants(string $enumFqcn): array
267
    {
268
        $r = new \ReflectionClass($enumFqcn);
269
        $constants = array_filter(
270
            $r->getConstants(),
271 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...
272
                if (PHP_VERSION_ID >= 70100) {
273
                    // ReflectionClass::getReflectionConstant() is only available since PHP 7.1
274
                    $rConstant = $r->getReflectionConstant($k);
275
                    $public = $rConstant->isPublic();
276
                    $value = $rConstant->getValue();
277
                } else {
278
                    $public = true;
279
                    $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...
280
                }
281
282
                // Only keep public constants, for which value matches enumerable values set:
283
                return $public && $enumFqcn::accepts($value);
284
            },
285
            ARRAY_FILTER_USE_KEY
286
        );
287
288
        $constants = array_flip($constants);
289
290
        return $constants;
291
    }
292
293
    public function normalizePath(string $path): string
294
    {
295
        if (!$this->baseDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->baseDir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
296
            return $path;
297
        }
298
299
        if ($this->fs->isAbsolutePath($path)) {
300
            return $path;
301
        }
302
303
        if (0 === strpos($path, './')) {
304
            return $path;
305
        }
306
307
        return rtrim($this->baseDir, DIRECTORY_SEPARATOR) . '/' . $path;
308
    }
309
}
310