Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
59.52% |
25 / 42 |
CRAP | |
79.18% |
194 / 245 |
PhpFileWriter | |
0.00% |
0 / 1 |
|
59.52% |
25 / 42 |
246.73 | |
79.18% |
194 / 245 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
write | |
0.00% |
0 / 1 |
3.33 | |
66.67% |
8 / 12 |
|||
getPhpCode | |
100.00% |
1 / 1 |
5 | |
100.00% |
16 / 16 |
|||
getParamAsCode | |
0.00% |
0 / 1 |
7.77 | |
75.00% |
9 / 12 |
|||
getTypeAsCode | |
100.00% |
1 / 1 |
7 | |
100.00% |
9 / 9 |
|||
getParamCode | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
getMethodParametersCode | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
toCamelCase | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getShortClassName | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
getClassNamespace | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
useClasses | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
useClass | |
0.00% |
0 / 1 |
21.89 | |
80.00% |
28 / 35 |
|||
getIdentifier | |
0.00% |
0 / 1 |
4.84 | |
62.50% |
5 / 8 |
|||
decreaseIndent | |
0.00% |
0 / 1 |
2.03 | |
80.00% |
4 / 5 |
|||
increaseIndent | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
writeString | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 7 |
|||
writeln | |
0.00% |
0 / 1 |
6.02 | |
91.67% |
11 / 12 |
|||
eof | |
0.00% |
0 / 1 |
2.15 | |
66.67% |
2 / 3 |
|||
setFileHeader | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
beginBlock | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
midBlock | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
endBlock | |
0.00% |
0 / 1 |
2.03 | |
80.00% |
4 / 5 |
|||
comment | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
docComment | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
setClassName | |
0.00% |
0 / 1 |
2.06 | |
75.00% |
3 / 4 |
|||
setNamespace | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
beginClass | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
beginAbstractClass | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 5 |
|||
endClass | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
beginInterface | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
endInterface | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
beginTrait | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
endTrait | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
beginMethodOverride | |
0.00% |
0 / 1 |
5.06 | |
86.67% |
13 / 15 |
|||
beginMethod | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
beginProtectedMethod | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
beginPrivateMethod | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
beginFinalMethod | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
beginStaticMethod | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
endMethod | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
writeAbstractMethod | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
writeInterfaceMethod | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
hasMethod | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
<?php declare(strict_types = 1); | |
/* | |
* Copyright (c) 2017-2019, Josef Kufner <josef@kufner.cz> | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
*/ | |
namespace Smalldb\PhpFileWriter; | |
/** | |
* Write PHP files in a convenient way. | |
*/ | |
class PhpFileWriter | |
{ | |
private string $indent = ""; | |
private int $indentDepth = 0; | |
private string $buffer = ''; | |
private ?string $headerComment = null; | |
private ?string $fileNamespace = null; | |
/** @var string[] */ | |
private array $useAliases = []; | |
/** @var int[] */ | |
private array $usedAliasesCounter = []; | |
/** @var int[] */ | |
private array $usedIdentifiersCounter = []; | |
/** @var string[] */ | |
private array $definedMethodNames = []; | |
private bool $skipNextEmptyLine = false; | |
/** | |
* PhpFileWriter constructor. | |
*/ | |
public function __construct() | |
{ | |
} | |
public function write(string $filename) | |
{ | |
$this->eof(); | |
$dir = dirname($filename); | |
if (!is_writable($dir)) { | |
throw new \RuntimeException("Target directory is not writable: " . $dir); | |
} | |
$tmpFilename = tempnam($dir, '.' . basename($filename) . '.'); | |
try { | |
file_put_contents($tmpFilename, $this->getPhpCode()); | |
chmod($tmpFilename, 0444 & ~umask()); | |
rename($tmpFilename, $filename); | |
} | |
catch(\Throwable $up) { | |
unlink($tmpFilename); | |
throw $up; | |
} | |
} | |
public function getPhpCode(): string | |
{ | |
$this->eof(); | |
$code = "<?php declare(strict_types = 1);\n"; | |
$code .= $this->headerComment; | |
$code .= "\n"; | |
if ($this->fileNamespace) { | |
$code .= "namespace " . $this->fileNamespace . ";\n\n"; | |
} | |
ksort($this->useAliases); | |
foreach ($this->useAliases as $className => $alias) { | |
if ($this->getShortClassName($className) === $alias) { | |
if ($this->getClassNamespace($className) !== $this->fileNamespace) { | |
$code .= "use $className;\n"; | |
} | |
} else { | |
$code .= "use $className as $alias;\n"; | |
} | |
} | |
$code .= "\n"; | |
$code .= $this->buffer; | |
$code .= "\n"; | |
return $code; | |
} | |
/** | |
* @throws \ReflectionException | |
*/ | |
public function getParamAsCode(\ReflectionParameter $param): string | |
{ | |
$code = '$' . $param->name; | |
if ($param->isPassedByReference()) { | |
$code = '& ' . $code; | |
} | |
if ($param->isVariadic()) { | |
$code = '... ' . $code; | |
} | |
if (($type = $param->getType()) !== null && ($typehint = $this->getTypeAsCode($type)) !== '') { | |
$code = $typehint . ' ' . $code; | |
} | |
if ($param->isDefaultValueAvailable()) { | |
if ($param->isDefaultValueConstant()) { | |
$code .= ' = ' . $param->getDefaultValueConstantName(); | |
} else { | |
$code .= ' = ' . var_export($param->getDefaultValue(), true); | |
} | |
} | |
return $code; | |
} | |
public function getTypeAsCode(?\ReflectionType $typeReflection): string | |
{ | |
if ($typeReflection === null || !($typeReflection instanceof \ReflectionNamedType)) { | |
return ''; | |
} else { | |
$className = $typeReflection->getName(); | |
if (class_exists($className) || interface_exists($className)) { | |
$type = $this->useClass($className); | |
} else { | |
$type = $className; | |
} | |
if ($typeReflection->allowsNull()) { | |
$type = '?' . $type; | |
} | |
return $type; | |
} | |
} | |
public function getParamCode(?\ReflectionType $type, string $name): string | |
{ | |
$typehint = $this->getTypeAsCode($type); | |
return $typehint === '' ? '$' . $name : $typehint . ' $' . $name; | |
} | |
/** | |
* @return [string[],string[]] | |
*/ | |
public function getMethodParametersCode(\ReflectionMethod $method): array | |
{ | |
$argMethod = []; | |
$argCall = []; | |
foreach ($method->getParameters() as $param) { | |
$argMethod[$param->name] = $this->getParamAsCode($param); | |
$argCall[$param->name] = '$' . $param->name; | |
} | |
return [$argMethod, $argCall]; | |
} | |
public static function toCamelCase(string $identifier): string | |
{ | |
return str_replace('_', '', ucwords($identifier, '_')); | |
} | |
public static function getShortClassName(string $fqcn): string | |
{ | |
$lastSlashPos = strrpos($fqcn, '\\'); | |
return $lastSlashPos === false ? $fqcn : substr($fqcn, $lastSlashPos + 1); | |
} | |
public static function getClassNamespace(string $fqcn): string | |
{ | |
$lastSlashPos = strrpos($fqcn, '\\'); | |
return $lastSlashPos === false ? '' : substr($fqcn, $fqcn[0] == '/' ? 1 : 0, $lastSlashPos); | |
} | |
public function useClasses(array $fqcnList): array | |
{ | |
return array_map(function ($fqcn) { return $this->useClass($fqcn); }, $fqcnList); | |
} | |
public function useClass(string $fqcn, ?string $useAlias = null): string | |
{ | |
if ($fqcn === '') { | |
return ''; | |
} | |
$isNullable = ($fqcn[0] === '?'); | |
if ($isNullable) { | |
$fqcn = substr($fqcn, 1); | |
$prefix = '?'; | |
} else { | |
$prefix = ''; | |
} | |
// Don't alias primitive types | |
switch ($fqcn) { | |
case 'self': | |
case 'static': | |
case 'array': | |
case 'callable': | |
case 'bool': | |
case 'float': | |
case 'int': | |
case 'string': | |
case 'iterable': | |
case 'object': | |
return $prefix . $fqcn; | |
} | |
if ($useAlias !== null) { | |
if (isset($this->useAliases[$fqcn])) { | |
throw new \InvalidArgumentException("The class $fqcn already has an alias."); | |
} | |
$this->useAliases[$fqcn] = $useAlias; | |
return $useAlias; | |
} else if (isset($this->useAliases[$fqcn])) { | |
return $prefix . $this->useAliases[$fqcn]; | |
} else { | |
$alias = $this->getShortClassName($fqcn); | |
if (isset($this->usedAliasesCounter[$alias])) { | |
$alias .= '_' . ($this->usedAliasesCounter[$alias]++); | |
} else if (preg_match('/_[0-9]+$/', $alias)) { | |
$this->usedAliasesCounter[$alias] = 1; | |
$alias .= '_0'; | |
} else { | |
$this->usedAliasesCounter[$alias] = 1; | |
} | |
$this->useAliases[$fqcn] = $alias; | |
return $prefix . $alias; | |
} | |
} | |
public function getIdentifier(string $name, string $suffix = null): string | |
{ | |
$identifier = (string) preg_replace('/[^a-zA-Z0-9]/', '_', $name . ($suffix === null ? '' : '_' . $suffix)); | |
if (isset($this->usedIdentifiersCounter[$identifier])) { | |
$identifier .= '_' . ($this->usedIdentifiersCounter[$identifier]++); | |
} else if (preg_match('/_[0-9]+$/', $identifier)) { | |
$this->usedIdentifiersCounter[$identifier] = 1; | |
$identifier .= '_0'; | |
} else { | |
$this->usedIdentifiersCounter[$identifier] = 1; | |
} | |
return $identifier; | |
} | |
private function decreaseIndent(): void | |
{ | |
if ($this->indentDepth <= 0) { | |
throw new \LogicException("Indentation level reached zero. No block left to end when generating a PHP file."); | |
} | |
$this->indentDepth--; | |
$this->indent = str_repeat("\t", $this->indentDepth); | |
} | |
private function increaseIndent(): void | |
{ | |
$this->indent = $this->indent . "\t"; | |
$this->indentDepth++; | |
} | |
private bool $lineIndented = false; | |
public function writeString(string $string = '', ...$args): self | |
{ | |
if ($string !== '') { | |
if (!$this->lineIndented) { | |
$this->buffer .= $this->indent; | |
$this->lineIndented = true; | |
} | |
if (count($args) > 0) { | |
$this->buffer .= vsprintf($string, array_map(function($v) { return var_export($v, true); }, $args)); | |
} else { | |
$this->buffer .= $string; | |
} | |
} | |
return $this; | |
} | |
public function writeln(string $string = '', ...$args): self | |
{ | |
if ($this->skipNextEmptyLine) { | |
$this->skipNextEmptyLine = false; | |
if ($string === '') { | |
return $this; | |
} | |
} | |
if ($string !== '') { | |
if (!$this->lineIndented) { | |
$this->buffer .= $this->indent; | |
} | |
if (count($args) > 0) { | |
$this->buffer .= vsprintf($string, array_map(function($v) { return var_export($v, true); }, $args)); | |
} else { | |
$this->buffer .= $string; | |
} | |
} | |
$this->buffer .= "\n"; | |
$this->lineIndented = false; | |
return $this; | |
} | |
public function eof(): self | |
{ | |
if ($this->indentDepth !== 0) { | |
throw new \LogicException("Block not closed when generating a PHP file."); | |
} | |
return $this; | |
} | |
public function setFileHeader(string $generator_name): self | |
{ | |
$this->headerComment = "//" . str_replace("\n", "\n// ", "\n" | |
. "Generated by $generator_name.\n" | |
. "" | |
. "Do NOT edit! All changes will be lost!\n" | |
. "\n"); | |
return $this; | |
} | |
public function beginBlock(string $statement = '', ...$args): self | |
{ | |
if ($statement === '') { | |
$this->writeln("{"); | |
} else { | |
$this->writeln("$statement {", ...$args); | |
} | |
$this->increaseIndent(); | |
return $this; | |
} | |
public function midBlock(string $statement, ...$args): self | |
{ | |
$this->decreaseIndent(); | |
$this->writeln("} $statement {", ...$args); | |
$this->increaseIndent(); | |
return $this; | |
} | |
public function endBlock(string $suffix = '', ...$args): self | |
{ | |
$this->decreaseIndent(); | |
if ($suffix === '') { | |
$this->writeln("}"); | |
} else { | |
$this->writeln("}$suffix", ...$args); | |
} | |
return $this; | |
} | |
public function comment(string $comment): self | |
{ | |
$this->writeln("// ".str_replace("\n", "\n// ", $comment)); | |
return $this; | |
} | |
public function docComment(string $comment): self | |
{ | |
$this->writeln(''); | |
$this->writeln("/**\n" . $this->indent . " * " | |
. str_replace("\n", "\n" . $this->indent . " * ", $comment) | |
. "\n" . $this->indent . " */"); | |
$this->skipNextEmptyLine = true; | |
return $this; | |
} | |
public function setClassName(string $className): self | |
{ | |
if (isset($this->useAliases[$className])) { | |
throw new \LogicException('Class name already used as an alias: ' . $className); | |
} | |
$this->usedAliasesCounter[$className] = 1; | |
return $this; | |
} | |
public function setNamespace(string $namespace): self | |
{ | |
$this->fileNamespace = $namespace; | |
return $this; | |
} | |
public function beginClass(string $classname, ?string $extends = null, array $implements = []): self | |
{ | |
$this->writeln("class $classname" | |
. ($extends ? " extends " . $extends : '') | |
. ($implements ? " implements " . join(', ', $implements) : '')); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function beginAbstractClass(string $classname, ?string $extends = null, array $implements = []): self | |
{ | |
$this->writeln("abstract class $classname" | |
. ($extends ? " extends " . $extends : '') | |
. ($implements ? " implements " . join(', ', $implements) : '')); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function endClass(): self | |
{ | |
$this->endBlock(); | |
return $this; | |
} | |
public function beginInterface(string $classname, array $extends = []): self | |
{ | |
$this->writeln("interface $classname" | |
. ($extends ? " extends " . join(', ', $extends) : '')); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function endInterface(): self | |
{ | |
$this->endBlock(); | |
return $this; | |
} | |
public function beginTrait(string $classname): self | |
{ | |
$this->writeln("trait $classname"); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function endTrait(): self | |
{ | |
$this->endBlock(); | |
return $this; | |
} | |
public function beginMethodOverride(\ReflectionMethod $parentMethod, array & $parentCallArgs = null): self | |
{ | |
$methodName = $parentMethod->getName(); | |
if ($parentMethod->isFinal()) { | |
throw new \InvalidArgumentException("Cannot override final method: " . $methodName); | |
} | |
if ($parentMethod->isPublic()) { | |
$mods = 'public'; | |
} else { | |
$mods = 'protected'; | |
} | |
if ($parentMethod->isStatic()) { | |
$mods .= ' static'; | |
} | |
[$parentArgs, $parentCallArgs] = $this->getMethodParametersCode($parentMethod); | |
$returnType = $this->getTypeAsCode($parentMethod->getReturnType()); | |
$this->definedMethodNames[$methodName] = $methodName; | |
$this->writeln(''); | |
$this->writeln("$mods function $methodName(".join(', ', $parentArgs).")".($returnType === '' ? '' : ": $returnType")); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function beginMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("public function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType")); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function beginProtectedMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("protected function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType")); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function beginPrivateMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("private function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType")); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function beginFinalMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("public final function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType")); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function beginStaticMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("public static function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType")); | |
$this->beginBlock(); | |
return $this; | |
} | |
public function endMethod(): self | |
{ | |
$this->endBlock(); | |
$this->writeln(''); | |
return $this; | |
} | |
public function writeAbstractMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("public abstract function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType") . ";"); | |
return $this; | |
} | |
public function writeInterfaceMethod(string $name, array $args = [], string $returnType = ''): self | |
{ | |
$this->definedMethodNames[$name] = $name; | |
$this->writeln(''); | |
$this->writeln("public function $name(".join(', ', $args).")".($returnType === '' ? '' : ": $returnType") . ";"); | |
return $this; | |
} | |
public function hasMethod(string $methodName) | |
{ | |
return isset($this->definedMethodNames[$methodName]); | |
} | |
} |