diff --git a/.travis.yml b/.travis.yml index a9801d11..3f65a184 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,43 +3,60 @@ php: - 5.6 - 7.0 - 7.1 + - 7.2 -env: -matrix: - include: - - php: 5.6 - env: dependencies="--prefer-lowest --prefer-stable" - - php: 7.0 - env: coverage=on +before_install: + # turn off XDebug + - phpenv config-rm xdebug.ini || return 0 - allow_failures: - - php: 7.0 - env: coverage=on +install: + - travis_retry composer install --no-progress --prefer-dist script: - - vendor/bin/tester tests -s $coverageArgs - - php temp/code-checker/src/code-checker.php --short-arrays + - vendor/bin/tester tests -s after_failure: # Print *.actual content - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done -before_script: - # Install Nette Tester & Code Checker - - travis_retry composer update --no-interaction --prefer-dist $dependencies - - travis_retry composer create-project nette/code-checker temp/code-checker ~2.5 --no-interaction - - if [ "$coverage" == "on" ]; then coverageArgs="-p phpdbg --coverage ./coverage.xml --coverage-src ./src"; fi +jobs: + include: + - env: title="Lowest Dependencies" + install: + - travis_retry composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable + + + - stage: Code Standard Checker + php: 7.1 + install: + # Install Nette Code Checker + - travis_retry composer create-project nette/code-checker temp/code-checker ~2 --no-progress + # Install Nette Coding Standard + - travis_retry composer create-project nette/coding-standard temp/coding-standard --no-progress + + script: + - php temp/code-checker/src/code-checker.php --short-arrays + - php temp/coding-standard/ecs check src tests --config temp/coding-standard/coding-standard-php56.neon + + + - stage: Code Coverage + php: 7.1 + script: + - vendor/bin/tester -p phpdbg tests -s --coverage ./coverage.xml --coverage-src ./src + after_script: + - wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar + - php coveralls.phar --verbose --config tests/.coveralls.yml + + + allow_failures: + - stage: Code Coverage -after_script: - # Report Code Coverage - - > - if [ "$coverage" == "on" ]; then - wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar - && php coveralls.phar --verbose --config tests/.coveralls.yml - || true; fi sudo: false cache: directories: - $HOME/.composer/cache + +notifications: + email: false diff --git a/composer.json b/composer.json index 939aea9d..cb7cf8fa 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "nette/php-generator", - "description": "Nette PHP Generator", + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.1 features.", + "keywords": ["nette", "php", "code", "scaffolding"], "homepage": "https://nette.org", "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], "authors": [ diff --git a/readme.md b/readme.md index 4d8eeb5a..7b74a877 100644 --- a/readme.md +++ b/readme.md @@ -7,16 +7,28 @@ Nette PHP Generator [![Latest Stable Version](https://poser.pugx.org/nette/php-generator/v/stable)](https://github.com/nette/php-generator/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/php-generator/blob/master/license.md) + +Introduction +------------ + Generate PHP code, classes, namespaces etc. with a simple programmatical API. -Usage is very easy. In first, install it using Composer: +Documentation can be found on the [website](https://doc.nette.org/php-generator). + + +Installation +------------ + +The recommended way to install is via Composer: ``` composer require nette/php-generator ``` -Examples --------- +Usage +----- + +Usage is very easy. Let's start with a straightforward example of generating class: ```php $class = new Nette\PhpGenerator\ClassType('Demo'); @@ -120,6 +132,22 @@ class Demo } ``` +Tabs versus spaces +------------------ + +The generated code uses tabs for indentation, which makes it very easy to change it to any number of spaces: + +```php +use Nette\PhpGenerator\Helpers; + +$class = new Nette\PhpGenerator\ClassType('Demo'); +// ... + +echo Helpers::tabsToSpaces((string) $class); // 4 spaces indentation +echo Helpers::tabsToSpaces((string) $class, 2); // 2 spaces indentation +``` + + Literals -------- diff --git a/src/PhpGenerator/ClassType.php b/src/PhpGenerator/ClassType.php index a46c0c44..7bcbd7f8 100644 --- a/src/PhpGenerator/ClassType.php +++ b/src/PhpGenerator/ClassType.php @@ -23,7 +23,9 @@ class ClassType use Traits\CommentAware; const TYPE_CLASS = 'class'; + const TYPE_INTERFACE = 'interface'; + const TYPE_TRAIT = 'trait'; /** @var PhpNamespace|null */ @@ -47,7 +49,7 @@ class ClassType /** @var string[] */ private $implements = []; - /** @var string[] */ + /** @var array[] */ private $traits = []; /** @var Constant[] name => Constant */ diff --git a/src/PhpGenerator/Closure.php b/src/PhpGenerator/Closure.php index 7c50389b..5bbf1403 100644 --- a/src/PhpGenerator/Closure.php +++ b/src/PhpGenerator/Closure.php @@ -42,10 +42,14 @@ public function __toString() foreach ($this->uses as $param) { $uses[] = ($param->isReference() ? '&' : '') . '$' . $param->getName(); } + $useStr = strlen($tmp = implode(', ', $uses)) > Helpers::WRAP_LENGTH && count($uses) > 1 + ? "\n\t" . implode(",\n\t", $uses) . "\n" + : $tmp; + return 'function ' . ($this->returnReference ? '&' : '') . $this->parametersToString() - . ($this->uses ? ' use (' . implode(', ', $uses) . ')' : '') + . ($this->uses ? " use ($useStr)" : '') . $this->returnTypeToString() . " {\n" . Nette\Utils\Strings::indent(ltrim(rtrim($this->body) . "\n"), 1) . '}'; } diff --git a/src/PhpGenerator/Helpers.php b/src/PhpGenerator/Helpers.php index edc2e5b9..e2873293 100644 --- a/src/PhpGenerator/Helpers.php +++ b/src/PhpGenerator/Helpers.php @@ -18,8 +18,12 @@ class Helpers use Nette\StaticClass; const PHP_IDENT = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; + + const WRAP_LENGTH = 100; + + const INDENT_LENGTH = 4; + const MAX_DEPTH = 50; - const WRAP_LENGTH = 70; /** @@ -80,7 +84,7 @@ private static function _dump(&$var, $level = 0) } else { $out = ''; - $outAlt = "\n$space"; + $outWrapped = "\n$space"; $var[$marker] = true; $counter = 0; foreach ($var as $k => &$v) { @@ -88,12 +92,13 @@ private static function _dump(&$var, $level = 0) $item = ($k === $counter ? '' : self::_dump($k, $level + 1) . ' => ') . self::_dump($v, $level + 1); $counter = is_int($k) ? max($k + 1, $counter) : $counter; $out .= ($out === '' ? '' : ', ') . $item; - $outAlt .= "\t$item,\n$space"; + $outWrapped .= "\t$item,\n$space"; } } unset($var[$marker]); } - return '[' . (strpos($out, "\n") === false && strlen($out) < self::WRAP_LENGTH ? $out : $outAlt) . ']'; + $wrap = strpos($out, "\n") !== false || strlen($out) > self::WRAP_LENGTH - $level * self::INDENT_LENGTH; + return '[' . ($wrap ? $outWrapped : $out) . ']'; } elseif ($var instanceof \Serializable) { $var = serialize($var); @@ -181,11 +186,14 @@ public static function formatArgs($statement, array $args) if (!is_array($arg)) { throw new Nette\InvalidArgumentException('Argument must be an array.'); } - $sep = ''; + $items = []; foreach ($arg as $tmp) { - $res .= $sep . self::dump($tmp); - $sep = strlen($res) - strrpos($res, "\n") > self::WRAP_LENGTH ? ",\n\t" : ', '; + $items[] = self::dump($tmp); } + $res .= strlen($tmp = implode(', ', $items)) > self::WRAP_LENGTH && count($items) > 1 + ? "\n" . Nette\Utils\Strings::indent(implode(",\n", $items), 1) . "\n" + : $tmp; + } else { // $ -> :: $res .= substr($token, 0, -1) . self::formatMember(array_shift($args)); } @@ -280,4 +288,15 @@ public static function extractShortName($name) { return ($pos = strrpos($name, '\\')) === false ? $name : substr($name, $pos + 1); } + + + /** + * @param string + * @param int + * @return string + */ + public static function tabsToSpaces($s, $count = self::INDENT_LENGTH) + { + return str_replace("\t", str_repeat(' ', $count), $s); + } } diff --git a/src/PhpGenerator/Method.php b/src/PhpGenerator/Method.php index 7f3f75b5..c4e6ac3f 100644 --- a/src/PhpGenerator/Method.php +++ b/src/PhpGenerator/Method.php @@ -75,11 +75,14 @@ public function __toString() . 'function ' . ($this->returnReference ? '&' : '') . $this->name - . $this->parametersToString() + . ($params = $this->parametersToString()) . $this->returnTypeToString() . ($this->abstract || $this->body === false ? ';' - : "\n{\n" . Nette\Utils\Strings::indent(ltrim(rtrim($this->body) . "\n"), 1) . '}'); + : (strpos($params, "\n") === false ? "\n" : ' ') + . "{\n" + . Nette\Utils\Strings::indent(ltrim(rtrim($this->body) . "\n"), 1) + . '}'); } diff --git a/src/PhpGenerator/Traits/FunctionLike.php b/src/PhpGenerator/Traits/FunctionLike.php index 71c8e1c4..e10456e5 100644 --- a/src/PhpGenerator/Traits/FunctionLike.php +++ b/src/PhpGenerator/Traits/FunctionLike.php @@ -216,7 +216,10 @@ protected function parametersToString() . '$' . $param->getName() . ($param->hasDefaultValue() && !$variadic ? ' = ' . Helpers::dump($param->defaultValue) : ''); } - return '(' . implode(', ', $params) . ')'; + + return strlen($tmp = implode(', ', $params)) > Helpers::WRAP_LENGTH && count($params) > 1 + ? "(\n\t" . implode(",\n\t", $params) . "\n)" + : "($tmp)"; } diff --git a/tests/PhpGenerator/ClassType.from.php7.phpt b/tests/PhpGenerator/ClassType.from.php7.phpt index 9c253999..41f8e596 100644 --- a/tests/PhpGenerator/ClassType.from.php7.phpt +++ b/tests/PhpGenerator/ClassType.from.php7.phpt @@ -22,6 +22,7 @@ abstract class Class1 $res[] = ClassType::from(new class { public $a; + private $b; diff --git a/tests/PhpGenerator/ClassType.from.php71.phpt b/tests/PhpGenerator/ClassType.from.php71.phpt index 1172efce..3ed151d9 100644 --- a/tests/PhpGenerator/ClassType.from.php71.phpt +++ b/tests/PhpGenerator/ClassType.from.php71.phpt @@ -10,24 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; - -/** */ -class ClassA -{ - public function func1(A $a, ?B $b, ?C $c = null, D $d = null, E $e, ?int $i = 1, ?array $arr = []) - { - } - - - public function func2(): ?stdClass - { - } - - - public function func3(): void - { - } -} +require __DIR__ . '/fixtures/classes.php71.php'; $res[] = ClassType::from(ClassA::class); diff --git a/tests/PhpGenerator/ClassType.from.phpt b/tests/PhpGenerator/ClassType.from.phpt index ef51061f..d398b007 100644 --- a/tests/PhpGenerator/ClassType.from.phpt +++ b/tests/PhpGenerator/ClassType.from.phpt @@ -4,79 +4,18 @@ * Test: Nette\PhpGenerator generator. */ -namespace Abc; - use Nette\PhpGenerator\ClassType; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +require __DIR__ . '/fixtures/classes.php'; - -/** - * Interface - * @author John Doe - */ -interface Interface1 -{ - public function func1(); -} - -interface Interface2 -{ -} - -abstract class Class1 implements Interface1 -{ - /** @return Class1 */ - public function func1() - { - } - - - abstract protected function func2(); -} - -class Class2 extends Class1 implements Interface2 -{ - /** - * Public - * @var int - */ - public $public; - - /** @var int */ - protected $protected = 10; - - private $private = []; - - static public $static; - - - /** - * Func3 - * @return Class1 - */ - private function &func3(array $a = [], Class2 $b = null, \Abc\Unknown $c, \Xyz\Unknown $d, callable $e, $f = Unknown::ABC, $g) - { - } - - - final public function func2() - { - } -} - -class Class3 -{ - public $prop1; -} - -$res[] = ClassType::from(Interface1::class); -$res[] = ClassType::from(Interface2::class); -$res[] = ClassType::from(Class1::class); -$res[] = ClassType::from(new \ReflectionClass(Class2::class)); -$obj = new Class3; +$res[] = ClassType::from(Abc\Interface1::class); +$res[] = ClassType::from(Abc\Interface2::class); +$res[] = ClassType::from(Abc\Class1::class); +$res[] = ClassType::from(new \ReflectionClass(Abc\Class2::class)); +$obj = new Abc\Class3; $obj->prop2 = 1; $res[] = ClassType::from(new \ReflectionObject($obj)); diff --git a/tests/PhpGenerator/ClassType.inheritance.phpt b/tests/PhpGenerator/ClassType.inheritance.phpt index 499b0e95..52bf7e67 100644 --- a/tests/PhpGenerator/ClassType.inheritance.phpt +++ b/tests/PhpGenerator/ClassType.inheritance.phpt @@ -10,7 +10,9 @@ require __DIR__ . '/../bootstrap.php'; class A { public $a; + protected $b; + private $c; @@ -23,7 +25,9 @@ class A class B extends A { public $d; + protected $e; + private $f; diff --git a/tests/PhpGenerator/Closure.long.phpt b/tests/PhpGenerator/Closure.long.phpt new file mode 100644 index 00000000..9ce974f6 --- /dev/null +++ b/tests/PhpGenerator/Closure.long.phpt @@ -0,0 +1,49 @@ +setBody('return null;'); + +for ($name = 'abcde'; $name < 'abcdr'; $name++) { + $function->addParameter($name); + $function->addUse($name); +} + +Assert::match( +'function ( + $abcde, + $abcdf, + $abcdg, + $abcdh, + $abcdi, + $abcdj, + $abcdk, + $abcdl, + $abcdm, + $abcdn, + $abcdo, + $abcdp, + $abcdq +) use ( + $abcde, + $abcdf, + $abcdg, + $abcdh, + $abcdi, + $abcdj, + $abcdk, + $abcdl, + $abcdm, + $abcdn, + $abcdo, + $abcdp, + $abcdq +) { + return null; +}', (string) $function); diff --git a/tests/PhpGenerator/Helpers.dump().phpt b/tests/PhpGenerator/Helpers.dump().phpt index 1b4210cf..1b54b4c8 100644 --- a/tests/PhpGenerator/Helpers.dump().phpt +++ b/tests/PhpGenerator/Helpers.dump().phpt @@ -27,6 +27,7 @@ Assert::same('false', Helpers::dump(false)); Assert::same("''", Helpers::dump('')); Assert::same("'Hello'", Helpers::dump('Hello')); +Assert::same('"\t\n\t"', Helpers::dump("\t\n\t")); Assert::same("'I\xc3\xb1t\xc3\xabrn\xc3\xa2ti\xc3\xb4n\xc3\xa0liz\xc3\xa6ti\xc3\xb8n'", Helpers::dump("I\xc3\xb1t\xc3\xabrn\xc3\xa2ti\xc3\xb4n\xc3\xa0liz\xc3\xa6ti\xc3\xb8n")); // Iñtërnâtiônàlizætiøn Assert::same('"\rHello \$"', Helpers::dump("\rHello $")); Assert::same("'He\\llo'", Helpers::dump('He\llo')); @@ -37,7 +38,12 @@ Assert::same('[$s]', Helpers::dump([new PhpLiteral('$s')])); Assert::same('[1, 2, 3]', Helpers::dump([1, 2, 3])); Assert::same("['a', 7 => 'b', 'c', '9a' => 'd', 'e']", Helpers::dump(['a', 7 => 'b', 'c', '9a' => 'd', 9 => 'e'])); -Assert::same("[\n\t[\n\t\t'a',\n\t\t'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong',\n\t],\n]", Helpers::dump([['a', 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong']])); +Assert::match("[ + [ + 'a', + 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong', + ], +]", Helpers::dump([['a', 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong']])); Assert::same("['a' => 1, [\"\\r\" => \"\\r\", 2], 3]", Helpers::dump(['a' => 1, ["\r" => "\r", 2], 3])); Assert::same("(object) [\n\t'a' => 1,\n\t'b' => 2,\n]", Helpers::dump((object) ['a' => 1, 'b' => 2])); @@ -47,7 +53,9 @@ Assert::same("(object) [\n\t'a' => (object) [\n\t\t'b' => 2,\n\t],\n]", Helpers: class Test { public $a = 1; + protected $b = 2; + private $c = 3; } @@ -58,6 +66,7 @@ Assert::equal(new Test, eval('return ' . Helpers::dump(new Test) . ';')); class Test2 extends Test { public $d = 5; + private $c = 4; diff --git a/tests/PhpGenerator/Helpers.format.phpt b/tests/PhpGenerator/Helpers.format.phpt index 87f0a9eb..46c50a44 100644 --- a/tests/PhpGenerator/Helpers.format.phpt +++ b/tests/PhpGenerator/Helpers.format.phpt @@ -20,9 +20,37 @@ Assert::same('func(1 ? 2 : 3)', Helpers::formatArgs('func(1 \? 2 : 3)', [])); Assert::same('func([1, 2])', Helpers::formatArgs('func(?)', [[1, 2]])); Assert::same('func(1, 2)', Helpers::formatArgs('func(...?)', [[1, 2]])); Assert::same('func(1, 2)', Helpers::formatArgs('func(?*)', [[1, 2]])); // old way -Assert::same( - "func(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,\n\t27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,\n\t45, 46, 47, 48, 49, 50)", - Helpers::formatArgs('func(?*)', [range(10, 50)]) +Assert::match( +'func( + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36 +)', + Helpers::formatArgs('func(?*)', [range(10, 36)]) ); Assert::exception(function () { diff --git a/tests/PhpGenerator/Helpers.tabsToSpaces().phpt b/tests/PhpGenerator/Helpers.tabsToSpaces().phpt new file mode 100644 index 00000000..90ccd2c8 --- /dev/null +++ b/tests/PhpGenerator/Helpers.tabsToSpaces().phpt @@ -0,0 +1,14 @@ +setBody('return null;'); + +for ($name = 'a'; $name < 'm'; $name++) { + $method->addParameter($name)->setTypeHint('string'); +} + +Assert::match( +'function create( + string $a, + string $b, + string $c, + string $d, + string $e, + string $f, + string $g, + string $h, + string $i, + string $j, + string $k, + string $l +) { + return null; +} +', (string) $method); diff --git a/tests/PhpGenerator/fixtures/classes.php b/tests/PhpGenerator/fixtures/classes.php new file mode 100644 index 00000000..e438b7da --- /dev/null +++ b/tests/PhpGenerator/fixtures/classes.php @@ -0,0 +1,67 @@ +