Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
59.52% covered (warning)
59.52%
25 / 42
CRAP
79.18% covered (warning)
79.18%
194 / 245
PhpFileWriter
0.00% covered (danger)
0.00%
0 / 1
59.52% covered (warning)
59.52%
25 / 42
246.73
79.18% covered (warning)
79.18%
194 / 245
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 write
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
66.67%
8 / 12
 getPhpCode
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
16 / 16
 getParamAsCode
0.00% covered (danger)
0.00%
0 / 1
7.77
75.00% covered (warning)
75.00%
9 / 12
 getTypeAsCode
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
9 / 9
 getParamCode
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
 getMethodParametersCode
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
6 / 6
 toCamelCase
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getShortClassName
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 getClassNamespace
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
 useClasses
n/a
0 / 0
1
n/a
0 / 0
 useClass
0.00% covered (danger)
0.00%
0 / 1
21.89
80.00% covered (warning)
80.00%
28 / 35
 getIdentifier
0.00% covered (danger)
0.00%
0 / 1
4.84
62.50% covered (warning)
62.50%
5 / 8
 decreaseIndent
0.00% covered (danger)
0.00%
0 / 1
2.03
80.00% covered (warning)
80.00%
4 / 5
 increaseIndent
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 writeString
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 7
 writeln
0.00% covered (danger)
0.00%
0 / 1
6.02
91.67% covered (success)
91.67%
11 / 12
 eof
0.00% covered (danger)
0.00%
0 / 1
2.15
66.67% covered (warning)
66.67%
2 / 3
 setFileHeader
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 beginBlock
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 midBlock
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 endBlock
0.00% covered (danger)
0.00%
0 / 1
2.03
80.00% covered (warning)
80.00%
4 / 5
 comment
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 docComment
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 setClassName
0.00% covered (danger)
0.00%
0 / 1
2.06
75.00% covered (warning)
75.00%
3 / 4
 setNamespace
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 beginClass
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
5 / 5
 beginAbstractClass
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 5
 endClass
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 beginInterface
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 endInterface
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 beginTrait
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 endTrait
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 beginMethodOverride
0.00% covered (danger)
0.00%
0 / 1
5.06
86.67% covered (warning)
86.67%
13 / 15
 beginMethod
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 beginProtectedMethod
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 beginPrivateMethod
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 beginFinalMethod
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 beginStaticMethod
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 endMethod
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 writeAbstractMethod
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 writeInterfaceMethod
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 hasMethod
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
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]);
    }
}