diff --git a/readme.md b/readme.md index e9307e03..9e126cd6 100644 --- a/readme.md +++ b/readme.md @@ -2,11 +2,340 @@ Nette PHP Generator =================== [![Downloads this Month](https://img.shields.io/packagist/dm/nette/php-generator.svg)](https://packagist.org/packages/nette/php-generator) -[![Build Status](https://travis-ci.org/nette/php-generator.svg?branch=master)](https://travis-ci.org/nette/php-generator) -[![Coverage Status](https://coveralls.io/repos/github/nette/php-generator/badge.svg?branch=master)](https://coveralls.io/github/nette/php-generator?branch=master) +[![Build Status](https://travis-ci.org/nette/php-generator.svg?branch=v2.6)](https://travis-ci.org/nette/php-generator) +[![Coverage Status](https://coveralls.io/repos/github/nette/php-generator/badge.svg?branch=v2.6)](https://coveralls.io/github/nette/php-generator?branch=v2.6) [![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) -Generate PHP code with a simple programmatical API. +Generate PHP code, classes, namespaces etc. with a simple programmatical API. -[Sample PHP code definition](https://github.com/nette/php-generator/blob/master/tests/PhpGenerator/ClassType.phpt) → [sample output](https://github.com/nette/php-generator/blob/master/tests/PhpGenerator/ClassType.expect) with magic ```__toString()``` method. +Usage is very easy. In first, install it using Composer: + +``` +composer require nette/php-generator +``` + +Examples +-------- + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); + +$class + ->setAbstract() + ->setFinal() + ->setExtends('ParentClass') + ->addImplement('Countable') + ->addTrait('Nette\SmartObject') + ->addComment("Description of class.\nSecond line\n") + ->addComment('@property-read Nette\Forms\Form $form'); + +$class->addConstant('ID', 123); + +$class->addProperty('items', [1, 2, 3]) + ->setVisibility('private') + ->setStatic() + ->addComment('@var int[]'); + +$method = $class->addMethod('count') + ->addComment('Count it.') + ->addComment('@return int') + ->setFinal() + ->setVisibility('protected') + ->setBody('return count($items ?: $this->items);'); + +$method->addParameter('items', []) // $items = [] + ->setReference() // &$items = [] + ->setTypeHint('array'); // array &$items = [] +``` + +To generate PHP code simply cast to string or use echo: + +```php +echo $class; +``` + +It will render this result: + +```php +/** + * Description of class. + * Second line + * + * @property-read Nette\Forms\Form $form + */ +abstract final class Demo extends ParentClass implements Countable +{ + use Nette\SmartObject; + + const ID = 123; + + /** @var int[] */ + private static $items = [1, 2, 3]; + + /** + * Count it. + * @return int + */ + final protected function count(array &$items = []) + { + return count($items ?: $this->items); + } + +} +``` + +PHP Generator supports all new PHP 7.1 features: + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); + +$class->addConstant('ID', 123) + ->setVisibility('private'); // constant visiblity + +$method = $class->addMethod('getValue') + ->setReturnType('int') // method return type + ->setReturnNullable() // nullable return type + ->setBody('return count($this->items);'); + +$method->addParameter('id') + ->setTypeHint('int') // scalar type hint + ->setNullable(); // nullable type hint + +echo $class; +``` + +Result: + +```php +class Demo +{ + private const ID = 123; + + public function getValue(?int $id): ?int + { + return count($this->items); + } + +} +``` + +Literals +-------- + +You can pass any PHP code to property or parameter default values via `Nette\PhpGenerator\PhpLiteral`: + +```php +use Nette\PhpGenerator\PhpLiteral; + +$class = new Nette\PhpGenerator\ClassType('Demo'); + +$class->addProperty('foo', new PhpLiteral('Iterator::SELF_FIRST')); + +$class->addMethod('bar') + ->addParameter('id', new PhpLiteral('1 + 2')); + +echo $class; +``` + +Result: + +```php +class Demo +{ + public $foo = Iterator::SELF_FIRST; + + public function bar($id = 1 + 2) + { + } + +} +``` + +Interface or trait +------------------ + +```php +$class = new Nette\PhpGenerator\ClassType('DemoInterface'); +$class->setType('interface'); +$class->setType('trait'); // or trait +``` + +Trait resolutions and visibility +-------------------------------- + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); +$class->addTrait('SmartObject', ['sayHello as protected']); +echo $class; +``` + +Result: + +```php +class Demo +{ + use SmartObject { + sayHello as protected; + } +} +``` + +Anonymous class +--------------- + +```php +$class = new Nette\PhpGenerator\ClassType(NULL); +$class->addMethod('__construct') + ->addParameter('foo'); + +echo '$obj = new class ($val) ' . $class . ';'; +``` + +Result: + +```php +$obj = new class ($val) { + + public function __construct($foo) + { + } +}; +``` + +Global function +--------------- + +```php +$function = new Nette\PhpGenerator\GlobalFunction('foo'); +$function->setBody('return $a + $b;'); +$function->addParameter('a'); +$function->addParameter('b'); +echo $function; +``` + +Result: + +```php +function foo($a, $b) +{ + return $a + $b; +} +``` + +Closure +------- + +```php +$closure = new Nette\PhpGenerator\Closure; +$closure->setBody('return $a + $b;'); +$closure->addParameter('a'); +$closure->addParameter('b'); +$closure->addUse('c') + ->setReference(); +echo $closure; +``` + +Result: + +```php +function ($a, $b) use (&$c) { + return $a + $b; +} +``` + +Method body generator +--------------------- + +You can use special placeholders for handy way to generate method or function body. + +Simple placeholders: + +```php +$str = 'any string'; +$num = 3; +$function = new Nette\PhpGenerator\GlobalFunction('foo'); +$function->addBody('$a = strlen(?, ?);', [$str, $num]); +$function->addBody('return $a \? 10 : ?;', [$num]); // escaping +echo $function; +``` + +Result: + +```php +function foo() +{ + $a = strlen('any string', 3); + return $a ? 10 : 3; +} +``` + +Variadic placeholder: + +```php +$items = [1, 2, 3]; +$function = new Nette\PhpGenerator\GlobalFunction('foo'); +$function->setBody('myfunc(...?);', [$items]); +echo $function; +``` + +Result: + +```php +function foo() +{ + myfunc(1, 2, 3); +} +``` + + +Namespace +--------- + +```php +$namespace = new Nette\PhpGenerator\PhpNamespace('Foo'); +$namespace->addUse('Bar\AliasedClass'); + +$class = $namespace->addClass('Demo'); +$class->addImplement('Foo\A') // resolves to A + ->addTrait('Bar\AliasedClass'); // resolves to AliasedClass + +$method = $class->addMethod('method'); +$method->addParameter('arg') + ->setTypeHint('Bar\OtherClass'); // resolves to \Bar\OtherClass + +echo $namespace; +``` + +Result: + +```php +namespace Foo; + +use Bar\AliasedClass; + +class Demo implements A +{ + use AliasedClass; + + public function method(\Bar\OtherClass $arg) + { + } + +} +``` + +Factories +--------- + +Another common use case is to create class or method form existing ones: + +```php +$class = Nette\PhpGenerator\ClassType::from(PDO::class); + +$function = Nette\PhpGenerator\GlobalFunction::from('trim'); + +$closure = Nette\PhpGenerator\Closure::from( + function (stdClass $a, $b = NULL) {} +); +``` diff --git a/src/PhpGenerator/ClassType.php b/src/PhpGenerator/ClassType.php index 9ea46e33..f47fe3ab 100644 --- a/src/PhpGenerator/ClassType.php +++ b/src/PhpGenerator/ClassType.php @@ -229,12 +229,13 @@ public function isAbstract() * @param string|string[] * @return static */ - public function setExtends($types) + public function setExtends($names) { - if (!is_string($types) && !(is_array($types) && Nette\Utils\Arrays::every($types, 'is_string'))) { + if (!is_string($names) && !is_array($names)) { throw new Nette\InvalidArgumentException('Argument must be string or string[].'); } - $this->extends = $types; + $this->validate((array) $names); + $this->extends = $names; return $this; } @@ -252,10 +253,11 @@ public function getExtends() * @param string * @return static */ - public function addExtend($type) + public function addExtend($name) { + $this->validate([$name]); $this->extends = (array) $this->extends; - $this->extends[] = (string) $type; + $this->extends[] = $name; return $this; } @@ -264,9 +266,10 @@ public function addExtend($type) * @param string[] * @return static */ - public function setImplements(array $types) + public function setImplements(array $names) { - $this->implements = $types; + $this->validate($names); + $this->implements = $names; return $this; } @@ -284,9 +287,10 @@ public function getImplements() * @param string * @return static */ - public function addImplement($type) + public function addImplement($name) { - $this->implements[] = (string) $type; + $this->validate([$name]); + $this->implements[] = $name; return $this; } @@ -295,9 +299,10 @@ public function addImplement($type) * @param string[] * @return static */ - public function setTraits(array $traits) + public function setTraits(array $names) { - $this->traits = array_fill_keys($traits, []); + $this->validate($names); + $this->traits = array_fill_keys($names, []); return $this; } @@ -315,9 +320,10 @@ public function getTraits() * @param string * @return static */ - public function addTrait($trait, array $resolutions = []) + public function addTrait($name, array $resolutions = []) { - $this->traits[$trait] = $resolutions; + $this->validate([$name]); + $this->traits[$name] = $resolutions; return $this; } @@ -492,4 +498,14 @@ public function addMethod($name) return $this->methods[$name] = $method; } + + private function validate(array $names) + { + foreach ($names as $name) { + if (!Helpers::isNamespaceIdentifier($name, TRUE)) { + throw new Nette\InvalidArgumentException("Value '$name' is not valid class name."); + } + } + } + } diff --git a/src/PhpGenerator/Closure.php b/src/PhpGenerator/Closure.php index 81b3da8e..c8f44553 100644 --- a/src/PhpGenerator/Closure.php +++ b/src/PhpGenerator/Closure.php @@ -57,6 +57,11 @@ public function __toString() */ public function setUses(array $uses) { + foreach ($uses as $use) { + if (!$use instanceof Parameter) { + throw new Nette\InvalidArgumentException('Argument must be Nette\PhpGenerator\Parameter[].'); + } + } $this->uses = $uses; return $this; } diff --git a/src/PhpGenerator/Helpers.php b/src/PhpGenerator/Helpers.php index 356d1e44..c33e36b7 100644 --- a/src/PhpGenerator/Helpers.php +++ b/src/PhpGenerator/Helpers.php @@ -244,9 +244,10 @@ public static function isIdentifier($value) /** * @return bool */ - public static function isNamespace($value) + public static function isNamespaceIdentifier($value, $allowLeadingSlash = FALSE) { - return is_string($value) && preg_match('#^' . Helpers::PHP_IDENT . '(\\\\' . Helpers::PHP_IDENT . ')*\z#', $value); + $re = '#^' . ($allowLeadingSlash ? '\\\\?' : '') . Helpers::PHP_IDENT . '(\\\\' . Helpers::PHP_IDENT . ')*\z#'; + return is_string($value) && preg_match($re, $value); } diff --git a/src/PhpGenerator/Method.php b/src/PhpGenerator/Method.php index 707bb647..e550bb48 100644 --- a/src/PhpGenerator/Method.php +++ b/src/PhpGenerator/Method.php @@ -107,9 +107,9 @@ public function getBody() * @param bool * @return static */ - public function setStatic($val) + public function setStatic($state = TRUE) { - $this->static = (bool) $val; + $this->static = (bool) $state; return $this; } @@ -127,9 +127,9 @@ public function isStatic() * @param bool * @return static */ - public function setFinal($val) + public function setFinal($state = TRUE) { - $this->final = (bool) $val; + $this->final = (bool) $state; return $this; } @@ -147,9 +147,9 @@ public function isFinal() * @param bool * @return static */ - public function setAbstract($val) + public function setAbstract($state = TRUE) { - $this->abstract = (bool) $val; + $this->abstract = (bool) $state; return $this; } diff --git a/src/PhpGenerator/PhpNamespace.php b/src/PhpGenerator/PhpNamespace.php index 00234dc9..b9303865 100644 --- a/src/PhpGenerator/PhpNamespace.php +++ b/src/PhpGenerator/PhpNamespace.php @@ -47,7 +47,7 @@ class PhpNamespace */ public function __construct($name = NULL) { - if ($name && !Helpers::isNamespace($name)) { + if ($name && !Helpers::isNamespaceIdentifier($name)) { throw new Nette\InvalidArgumentException("Value '$name' is not valid name."); } $this->name = (string) $name; diff --git a/src/PhpGenerator/Traits/FunctionLike.php b/src/PhpGenerator/Traits/FunctionLike.php index a84f0d04..ac834ecb 100644 --- a/src/PhpGenerator/Traits/FunctionLike.php +++ b/src/PhpGenerator/Traits/FunctionLike.php @@ -34,7 +34,7 @@ trait FunctionLike private $returnReference = FALSE; /** @var bool */ - private $returnNullable; + private $returnNullable = FALSE; /** @var PhpNamespace|NULL */ private $namespace; @@ -115,9 +115,9 @@ public function addParameter($name, $defaultValue = NULL) * @param bool * @return static */ - public function setVariadic($val) + public function setVariadic($state = TRUE) { - $this->variadic = (bool) $val; + $this->variadic = (bool) $state; return $this; } @@ -155,9 +155,9 @@ public function getReturnType() * @param bool * @return static */ - public function setReturnReference($val) + public function setReturnReference($state = TRUE) { - $this->returnReference = (bool) $val; + $this->returnReference = (bool) $state; return $this; } @@ -175,9 +175,9 @@ public function getReturnReference() * @param bool * @return static */ - public function setReturnNullable($val) + public function setReturnNullable($state = TRUE) { - $this->returnNullable = (bool) $val; + $this->returnNullable = (bool) $state; return $this; } diff --git a/tests/PhpGenerator/ClassType.interface.phpt b/tests/PhpGenerator/ClassType.interface.phpt index f69b634f..f197141d 100644 --- a/tests/PhpGenerator/ClassType.interface.phpt +++ b/tests/PhpGenerator/ClassType.interface.phpt @@ -18,6 +18,8 @@ $interface ->addExtend('ITwo') ->addComment('Description of interface'); +Assert::same(['IOne', 'ITwo'], $interface->getExtends()); + $interface->addMethod('getForm'); Assert::matchFile(__DIR__ . '/ClassType.interface.expect', (string) $interface); diff --git a/tests/PhpGenerator/ClassType.phpt b/tests/PhpGenerator/ClassType.phpt index a75fa3cf..a74e1d8f 100644 --- a/tests/PhpGenerator/ClassType.phpt +++ b/tests/PhpGenerator/ClassType.phpt @@ -13,13 +13,19 @@ require __DIR__ . '/../bootstrap.php'; $class = new ClassType('Example'); + +Assert::false($class->isFinal()); +Assert::false($class->isAbstract()); +Assert::same([], $class->getExtends()); +Assert::same([], $class->getTraits()); + $class ->setAbstract(TRUE) ->setFinal(TRUE) ->setExtends('ParentClass') ->addImplement('IExample') ->addImplement('IOne') - ->addTrait('ObjectTrait') + ->setTraits(['ObjectTrait']) ->addTrait('AnotherTrait', ['sayHello as protected']) ->addComment("Description of class.\nThis is example\n") ->addComment('@property-read Nette\Forms\Form $form') @@ -28,6 +34,13 @@ $class Assert::same(['ROLE' => 'admin', 'ACTIVE' => FALSE], $class->getConsts()); +Assert::true($class->isFinal()); +Assert::true($class->isAbstract()); +Assert::same('ParentClass', $class->getExtends()); +Assert::same(['ObjectTrait', 'AnotherTrait'], $class->getTraits()); +Assert::count(2, $class->getConstants()); +Assert::type(Nette\PhpGenerator\Constant::class, $class->getConstants()['ROLE']); + $class->addConstant('FORCE_ARRAY', new PhpLiteral('Nette\Utils\Json::FORCE_ARRAY')) ->setVisibility('private') ->addComment('Commented'); @@ -43,6 +56,8 @@ $p = $class->addProperty('sections', ['first' => TRUE]) ->setStatic(TRUE); Assert::same($p, $class->getProperty('sections')); +Assert::true($p->isStatic()); +Assert::null($p->getVisibility()); $m = $class->addMethod('getHandle') ->addComment('Returns file handle.') @@ -51,14 +66,27 @@ $m = $class->addMethod('getHandle') ->setBody('return $this->?;', ['handle']); Assert::same($m, $class->getMethod('getHandle')); - -$class->addMethod('getSections') +Assert::true($m->isFinal()); +Assert::false($m->isStatic()); +Assert::false($m->isAbstract()); +Assert::false($m->getReturnReference()); +Assert::same('public', $m->getVisibility()); +Assert::same('return $this->handle;', $m->getBody()); + +$m = $class->addMethod('getSections') ->setStatic(TRUE) ->setVisibility('protected') ->setReturnReference(TRUE) ->addBody('$mode = ?;', [123]) - ->addBody('return self::$sections;') - ->addParameter('mode', new PhpLiteral('self::ORDER')); + ->addBody('return self::$sections;'); +$m->addParameter('mode', new PhpLiteral('self::ORDER')); + +Assert::false($m->isFinal()); +Assert::true($m->isStatic()); +Assert::true($m->getReturnReference()); +Assert::false($m->getReturnNullable()); +Assert::null($m->getReturnType()); +Assert::same('protected', $m->getVisibility()); $method = $class->addMethod('show') ->setAbstract(TRUE); @@ -87,3 +115,9 @@ $parameters = $method->getParameters(); Assert::count(2, $parameters); $method->setParameters(array_values($parameters)); Assert::same($parameters, $method->getParameters()); + + +Assert::exception(function () { + $class = new ClassType; + $class->addMethod('method')->setVisibility('unknown'); +}, Nette\InvalidArgumentException::class, 'Argument must be public|protected|private.'); diff --git a/tests/PhpGenerator/Closure.phpt b/tests/PhpGenerator/Closure.phpt index f2886f22..f5d7c07f 100644 --- a/tests/PhpGenerator/Closure.phpt +++ b/tests/PhpGenerator/Closure.phpt @@ -24,6 +24,24 @@ Assert::match( }', (string) $function); +$uses = $function->getUses(); +Assert::count(2, $uses); +Assert::type(Nette\PhpGenerator\Parameter::class, $uses[0]); +Assert::type(Nette\PhpGenerator\Parameter::class, $uses[1]); + +$uses = $function->setUses([$uses[0]]); + +Assert::match( +'function &($a, $b) use ($this) { + return $a + $b; +}', (string) $function); + +Assert::exception(function () { + $function = new Closure; + $function->setUses([123]); +}, Nette\InvalidArgumentException::class); + + $closure = function (stdClass $a, $b = NULL) {}; $function = Closure::from($closure); Assert::match( diff --git a/tests/PhpGenerator/Helpers.isNamespace.phpt b/tests/PhpGenerator/Helpers.isNamespace.phpt deleted file mode 100644 index 4c0bacd6..00000000 --- a/tests/PhpGenerator/Helpers.isNamespace.phpt +++ /dev/null @@ -1,17 +0,0 @@ -unresolveName('A')); Assert::same('A', $namespace->unresolveName('foo\A')); $namespace->addUse('Bar\C'); +Assert::same(['C' => 'Bar\\C'], $namespace->getUses()); Assert::same('\Bar', $namespace->unresolveName('Bar')); Assert::same('C', $namespace->unresolveName('\bar\C')); @@ -50,6 +51,9 @@ Assert::same($namespace, $classA->getNamespace()); $interfaceB = $namespace->addInterface('B'); Assert::same($namespace, $interfaceB->getNamespace()); +Assert::count(2, $namespace->getClasses()); +Assert::type(Nette\PhpGenerator\ClassType::class, $namespace->getClasses()['A']); + Assert::exception(function () use ($namespace) { $traitC = $namespace->addTrait('C'); Assert::same($namespace, $traitC->getNamespace()); diff --git a/tests/PhpGenerator/invalidNames.phpt b/tests/PhpGenerator/invalidNames.phpt index fea9335c..b0b49a32 100644 --- a/tests/PhpGenerator/invalidNames.phpt +++ b/tests/PhpGenerator/invalidNames.phpt @@ -58,6 +58,36 @@ Assert::exception(function () { }, Nette\InvalidArgumentException::class); +$class = new Nette\PhpGenerator\ClassType('Abc'); +Assert::exception(function () use ($class) { + $class->setExtends('*'); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + +Assert::exception(function () use ($class) { + $class->setExtends(['A', '*']); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + +Assert::exception(function () use ($class) { + $class->addExtend('*'); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + +Assert::exception(function () use ($class) { + $class->setImplements(['A', '*']); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + +Assert::exception(function () use ($class) { + $class->addImplement('*'); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + +Assert::exception(function () use ($class) { + $class->setTraits(['A', '*']); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + +Assert::exception(function () use ($class) { + $class->addTrait('*'); +}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); + + Assert::noError(function () { new Nette\PhpGenerator\Property('Iñtërnâtiônàlizætiøn'); });