thunderer /
Platenum
| 1 | <?php |
||
| 2 | declare(strict_types=1); |
||
| 3 | namespace Thunder\Platenum\Command; |
||
| 4 | |||
| 5 | use Composer\Autoload\ClassLoader; |
||
| 6 | use Thunder\Platenum\Enum\ConstantsEnumTrait; |
||
| 7 | use Thunder\Platenum\Enum\DocblockEnumTrait; |
||
| 8 | use Thunder\Platenum\Exception\PlatenumException; |
||
| 9 | use Thunder\Platenum\Enum\StaticEnumTrait; |
||
| 10 | |||
| 11 | /** |
||
| 12 | * @author Tomasz Kowalczyk <[email protected]> |
||
| 13 | * @codeCoverageIgnore |
||
| 14 | * @internal |
||
| 15 | */ |
||
| 16 | final class GenerateCommand |
||
| 17 | { |
||
| 18 | /** @var ClassLoader */ |
||
| 19 | private $classLoader; |
||
| 20 | |||
| 21 | public function __construct(ClassLoader $loader) |
||
| 22 | { |
||
| 23 | $this->classLoader = $loader; |
||
| 24 | } |
||
| 25 | |||
| 26 | /** |
||
| 27 | * @param int $argc |
||
| 28 | * @param string[] $argv |
||
| 29 | */ |
||
| 30 | public function execute(int $argc, array $argv): void |
||
| 31 | { |
||
| 32 | $this->writeln('Platenum (c) 2019 Tomasz Kowalczyk.'); |
||
| 33 | $this->writeln(''); |
||
| 34 | |||
| 35 | if($argc < 4) { |
||
| 36 | $this->writeln('usage: bin/generate <source> <class> MEMBER=value,MEMBER=value,...'); |
||
| 37 | $this->writeln('examples: bin/generate constants UserStatus ACTIVE=1,INACTIVE=2'); |
||
| 38 | $this->writeln(' bin/generate docblock PaymentType INTERNAL,EXTERNAL'); |
||
| 39 | $this->writeln(' bin/generate static "Project\\Namespace\\Currency" PLN=10,EUR=12,USD=14'); |
||
| 40 | exit(1); |
||
|
0 ignored issues
–
show
|
|||
| 41 | } |
||
| 42 | if(false === in_array($argv[1], ['constants', 'docblock', 'static'], true)) { |
||
| 43 | $this->writeln(sprintf('Unrecognized type `%s`. Allowed: `%s`.', $argv[1], 'constants,docblock,static')); |
||
| 44 | exit(1); |
||
|
0 ignored issues
–
show
|
|||
| 45 | } |
||
| 46 | |||
| 47 | $path = $this->computeClassPath($argv[2]); |
||
| 48 | $code = $this->generateClassCode($argv[1], $argv[2], $argv[3]); |
||
| 49 | |||
| 50 | if(file_exists($path)) { |
||
| 51 | $this->writeln("\e[0;31mFile already exists.\e[0m"); |
||
| 52 | return; |
||
| 53 | } |
||
| 54 | $this->writeFile($path, $code); |
||
| 55 | |||
| 56 | $this->writeln(''); |
||
| 57 | $this->writeln('--- BEGIN CLASS CODE ---'); |
||
| 58 | $this->writeln("\e[0;33m$code\e[0m"); |
||
| 59 | $this->writeln('--- END CLASS CODE ---'); |
||
| 60 | } |
||
| 61 | |||
| 62 | private function writeFile(string $path, string $content): void |
||
| 63 | { |
||
| 64 | if(!$path) { |
||
| 65 | return; |
||
| 66 | } |
||
| 67 | |||
| 68 | $directory = \dirname($path); |
||
| 69 | if(!file_exists($directory) && !\mkdir($directory, 0777, true) && !\is_dir($directory)) { |
||
| 70 | throw new PlatenumException(sprintf('Failed to create target directory `%s`.', $directory)); |
||
| 71 | } |
||
| 72 | |||
| 73 | $bytes = file_put_contents($path, $content); |
||
| 74 | if($bytes !== \strlen($content)) { |
||
| 75 | throw new PlatenumException(sprintf('Failed to write target file at path `%s`.', $path)); |
||
| 76 | } |
||
| 77 | } |
||
| 78 | |||
| 79 | private function computeClassPath(string $fqcn): string |
||
| 80 | { |
||
| 81 | $prefix = ''; |
||
| 82 | $path = ''; |
||
| 83 | /** @var string[] $paths */ |
||
| 84 | foreach($this->classLoader->getPrefixesPsr4() as $ns => $paths) { |
||
| 85 | if(0 === strpos($fqcn, $ns)) { |
||
| 86 | $prefix = $ns; |
||
| 87 | $path = (string)realpath($paths[0]); |
||
| 88 | break; |
||
| 89 | } |
||
| 90 | } |
||
| 91 | |||
| 92 | if(false === ($prefix && $path)) { |
||
| 93 | $this->writeln(sprintf("Namespace `\e[0;32m%s\e[0m` is not mapped in Composer autoloader.", $fqcn)); |
||
| 94 | $this->writeln('Generated code will be written below. No files will be written to disk.'); |
||
| 95 | return ''; |
||
| 96 | } |
||
| 97 | |||
| 98 | $fullPath = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, str_replace($prefix, '', $fqcn)).'.php'; |
||
| 99 | $this->writeln(sprintf("Namespace prefix `\e[0;32m%s\e[0m` is mapped to `\e[0;32m%s\e[0m`.", $prefix, $path)); |
||
| 100 | $this->writeln(sprintf("Class file will be written to path: `\e[0;32m%s\e[0m`", $fullPath)); |
||
| 101 | |||
| 102 | return $fullPath; |
||
| 103 | } |
||
| 104 | |||
| 105 | private function generateClassCode(string $type, string $fqcn, string $keys): string |
||
| 106 | { |
||
| 107 | $namespace = $fqcn; |
||
| 108 | $lastSlash = (int)strrpos($namespace, '\\'); |
||
| 109 | $class = substr($namespace, $lastSlash ? $lastSlash + 1 : 0); |
||
| 110 | $namespace = $lastSlash ? substr($namespace, 0, $lastSlash) : 'X'; |
||
| 111 | |||
| 112 | preg_match_all('~(?<key>[a-zA-Z]+)(=(?<value>[^,$]+),?)?~', $keys, $matches); |
||
| 113 | if(PREG_NO_ERROR !== preg_last_error()) { |
||
| 114 | throw new PlatenumException(sprintf('Failed to parse keys, `%s`.', preg_last_error())); |
||
| 115 | } |
||
| 116 | |||
| 117 | $index = 1; |
||
| 118 | $docblockEntries = []; |
||
| 119 | $constantsEntries = []; |
||
| 120 | $staticEntries = []; |
||
| 121 | /** @var array<string,array<int,string>> $matches */ |
||
| 122 | $count = \count($matches['key']); |
||
| 123 | for($i = 0; $i < $count; $i++) { |
||
| 124 | $key = $matches['key'][$i]; |
||
| 125 | $value = $matches['value'][$i] ?: $index++; |
||
| 126 | /** @psalm-suppress TypeDoesNotContainType */ |
||
| 127 | if(false === ctype_digit((string)$value)) { |
||
| 128 | $value = '\''.$value.'\''; |
||
| 129 | } |
||
| 130 | |||
| 131 | $constantsEntries[] = ' private const '.strtoupper($key).' = '.$value.';'; |
||
| 132 | $docblockEntries[] = ' * @method static static '.strtoupper($key).'()'; |
||
| 133 | $staticEntries[] = ' \''.strtoupper($key).'\' => '.$value.';'; |
||
| 134 | } |
||
| 135 | |||
| 136 | $values = [ |
||
| 137 | 'constants' => ['template' => static::CONSTANTS_TEMPLATE, 'trait' => ConstantsEnumTrait::class, 'members' => implode("\n", $constantsEntries)], |
||
| 138 | 'docblock' => ['template' => static::DOCBLOCK_TEMPLATE, 'trait' => DocblockEnumTrait::class, 'members' => ''], |
||
| 139 | 'static' => ['template' => static::STATIC_TEMPLATE, 'trait' => StaticEnumTrait::class, 'members' => implode("\n", $staticEntries)], |
||
| 140 | ]; |
||
| 141 | $replaces = [ |
||
| 142 | '<NS>' => $namespace, |
||
| 143 | '<CLASS>' => $class, |
||
| 144 | '<DOCBLOCK>' => implode("\n", $docblockEntries), |
||
| 145 | '<TRAIT>' => substr($values[$type]['trait'], (int)strrpos($values[$type]['trait'], "\\") + 1), |
||
| 146 | '<TRAIT_NS>' => $values[$type]['trait'], |
||
| 147 | '<MEMBERS>' => $values[$type]['members'], |
||
| 148 | ]; |
||
| 149 | |||
| 150 | /** @psalm-suppress InvalidArgument */ |
||
| 151 | return str_replace(array_keys($replaces), array_values($replaces), $values[$type]['template']); |
||
| 152 | } |
||
| 153 | |||
| 154 | private function writeln(string $message): void |
||
| 155 | { |
||
| 156 | echo $message."\n"; |
||
| 157 | } |
||
| 158 | |||
| 159 | private const CONSTANTS_TEMPLATE = <<<EOF |
||
| 160 | <?php |
||
| 161 | declare(strict_types=1); |
||
| 162 | namespace <NS>; |
||
| 163 | |||
| 164 | use <TRAIT_NS>; |
||
| 165 | |||
| 166 | /** |
||
| 167 | <DOCBLOCK> |
||
| 168 | */ |
||
| 169 | final class <CLASS> |
||
| 170 | { |
||
| 171 | use <TRAIT>; |
||
| 172 | |||
| 173 | <MEMBERS> |
||
| 174 | } |
||
| 175 | |||
| 176 | EOF; |
||
| 177 | |||
| 178 | private const DOCBLOCK_TEMPLATE = <<<EOF |
||
| 179 | <?php |
||
| 180 | declare(strict_types=1); |
||
| 181 | namespace <NS>; |
||
| 182 | |||
| 183 | use <TRAIT_NS>; |
||
| 184 | |||
| 185 | /** |
||
| 186 | <DOCBLOCK> |
||
| 187 | */ |
||
| 188 | final class <CLASS> |
||
| 189 | { |
||
| 190 | use <TRAIT>; |
||
| 191 | } |
||
| 192 | |||
| 193 | EOF; |
||
| 194 | |||
| 195 | private const STATIC_TEMPLATE = <<<EOF |
||
| 196 | <?php |
||
| 197 | declare(strict_types=1); |
||
| 198 | namespace <NS>; |
||
| 199 | |||
| 200 | use <TRAIT_NS>; |
||
| 201 | |||
| 202 | /** |
||
| 203 | <DOCBLOCK> |
||
| 204 | */ |
||
| 205 | final class <CLASS> |
||
| 206 | { |
||
| 207 | use <TRAIT>; |
||
| 208 | |||
| 209 | private static \$mapping = [ |
||
| 210 | <MEMBERS> |
||
| 211 | ]; |
||
| 212 | } |
||
| 213 | |||
| 214 | EOF; |
||
| 215 | } |
||
| 216 |
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.