🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,47 @@
{
"name": "nette/php-generator",
"description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.",
"keywords": ["nette", "php", "code", "scaffolding"],
"homepage": "https://nette.org",
"license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"require": {
"php": "8.1 - 8.5",
"nette/utils": "^4.0.6"
},
"require-dev": {
"nette/tester": "^2.4",
"nikic/php-parser": "^5.0",
"tracy/tracy": "^2.8",
"phpstan/phpstan-nette": "^2.0@stable",
"jetbrains/phpstorm-attributes": "^1.2"
},
"suggest": {
"nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()"
},
"autoload": {
"classmap": ["src/"],
"psr-4": {
"Nette\\": "src"
}
},
"minimum-stability": "dev",
"scripts": {
"phpstan": "phpstan analyse",
"tester": "tester tests -s"
},
"extra": {
"branch-alias": {
"dev-master": "4.2-dev"
}
}
}
@@ -0,0 +1,60 @@
Licenses
========
Good news! You may use Nette Framework under the terms of either
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
The BSD License is recommended for most projects. It is easy to understand and it
places almost no restrictions on what you can do with the framework. If the GPL
fits better to your project, you can use the framework under this license.
You don't have to notify anyone which license you are using. You can freely
use Nette Framework in commercial projects as long as the copyright header
remains intact.
Please be advised that the name "Nette Framework" is a protected trademark and its
usage has some limitations. So please do not use word "Nette" in the name of your
project or top-level domain, and choose a name that stands on its own merits.
If your stuff is good, it will not take long to establish a reputation for yourselves.
New BSD License
---------------
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of "Nette Framework" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as is" and
any express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed. In no event shall the copyright owner or contributors be liable for
any direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
GNU General Public License
--------------------------
GPL licenses are very very long, so instead of including them here we offer
you URLs with full text:
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of a PHP attribute.
*/
final class Attribute
{
private string $name;
/** @var mixed[] */
private array $args;
/** @param mixed[] $args */
public function __construct(string $name, array $args)
{
if (!Helpers::isNamespaceIdentifier($name)) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid attribute name.");
}
$this->name = $name;
$this->args = $args;
}
public function getName(): string
{
return $this->name;
}
/** @return mixed[] */
public function getArguments(): array
{
return $this->args;
}
}
@@ -0,0 +1,148 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use function array_map, is_object, strtolower;
/**
* Base definition of class, interface, trait or enum type.
*/
abstract class ClassLike
{
use Traits\CommentAware;
use Traits\AttributeAware;
#[\Deprecated('Use Visibility::Public')]
public const VisibilityPublic = Visibility::Public,
VISIBILITY_PUBLIC = Visibility::Public;
#[\Deprecated('Use Visibility::Protected')]
public const VisibilityProtected = Visibility::Protected,
VISIBILITY_PROTECTED = Visibility::Protected;
#[\Deprecated('Use Visibility::Private')]
public const VisibilityPrivate = Visibility::Private,
VISIBILITY_PRIVATE = Visibility::Private;
private ?PhpNamespace $namespace;
private ?string $name;
public static function from(string|object $class, bool $withBodies = false): static
{
$instance = (new Factory)
->fromClassReflection(new \ReflectionClass($class), $withBodies);
if (!$instance instanceof static) {
$class = is_object($class) ? $class::class : $class;
throw new Nette\InvalidArgumentException("$class cannot be represented with " . static::class . '. Call ' . $instance::class . '::' . __FUNCTION__ . '() or ' . __METHOD__ . '() instead.');
}
return $instance;
}
public static function fromCode(string $code): static
{
$instance = (new Factory)
->fromClassCode($code);
if (!$instance instanceof static) {
throw new Nette\InvalidArgumentException('Provided code cannot be represented with ' . static::class . '. Call ' . $instance::class . '::' . __FUNCTION__ . '() or ' . __METHOD__ . '() instead.');
}
return $instance;
}
public function __construct(string $name, ?PhpNamespace $namespace = null)
{
$this->setName($name);
$this->namespace = $namespace;
}
public function __toString(): string
{
return (new Printer)->printClass($this, $this->namespace);
}
/** @deprecated an object can be in multiple namespaces */
public function getNamespace(): ?PhpNamespace
{
return $this->namespace;
}
public function setName(?string $name): static
{
if ($name !== null && (!Helpers::isIdentifier($name) || isset(Helpers::Keywords[strtolower($name)]))) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid class name.");
}
$this->name = $name;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function isClass(): bool
{
return $this instanceof ClassType;
}
public function isInterface(): bool
{
return $this instanceof InterfaceType;
}
public function isTrait(): bool
{
return $this instanceof TraitType;
}
public function isEnum(): bool
{
return $this instanceof EnumType;
}
/** @param string[] $names */
protected function validateNames(array $names): void
{
foreach ($names as $name) {
if (!Helpers::isNamespaceIdentifier($name, allowLeadingSlash: true)) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid class name.");
}
}
}
public function validate(): void
{
}
public function __clone(): void
{
$this->attributes = array_map(fn($attr) => clone $attr, $this->attributes);
}
}
@@ -0,0 +1,124 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use const PHP_VERSION_ID;
final class ClassManipulator
{
public function __construct(
private ClassType $class,
) {
}
/**
* Inherits property from parent class.
*/
public function inheritProperty(string $name, bool $returnIfExists = false): Property
{
if ($this->class->hasProperty($name)) {
return $returnIfExists
? $this->class->getProperty($name)
: throw new Nette\InvalidStateException("Cannot inherit property '$name', because it already exists.");
}
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
foreach ($parents as $parent) {
try {
$rp = new \ReflectionProperty($parent, $name);
} catch (\ReflectionException) {
continue;
}
return $this->implementProperty($rp);
}
throw new Nette\InvalidStateException("Property '$name' has not been found in any ancestor: " . implode(', ', $parents));
}
/**
* Inherits method from parent class or interface.
*/
public function inheritMethod(string $name, bool $returnIfExists = false): Method
{
if ($this->class->hasMethod($name)) {
return $returnIfExists
? $this->class->getMethod($name)
: throw new Nette\InvalidStateException("Cannot inherit method '$name', because it already exists.");
}
$parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()]
?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set.");
foreach ($parents as $parent) {
try {
$rm = new \ReflectionMethod($parent, $name);
} catch (\ReflectionException) {
continue;
}
return $this->implementMethod($rm);
}
throw new Nette\InvalidStateException("Method '$name' has not been found in any ancestor: " . implode(', ', $parents));
}
/**
* Implements all methods from the given interface or abstract class.
*/
public function implement(string $name): void
{
$definition = new \ReflectionClass($name);
if ($definition->isInterface()) {
$this->class->addImplement($name);
} elseif ($definition->isAbstract()) {
$this->class->setExtends($name);
} else {
throw new Nette\InvalidArgumentException("'$name' is not an interface or abstract class.");
}
foreach ($definition->getMethods() as $method) {
if (!$this->class->hasMethod($method->getName()) && $method->isAbstract()) {
$this->implementMethod($method);
}
}
if (PHP_VERSION_ID >= 80400) {
foreach ($definition->getProperties() as $property) {
if (!$this->class->hasProperty($property->getName()) && $property->isAbstract()) {
$this->implementProperty($property);
}
}
}
}
private function implementMethod(\ReflectionMethod $rm): Method
{
$method = (new Factory)->fromMethodReflection($rm);
$method->setAbstract(false);
$this->class->addMember($method);
return $method;
}
private function implementProperty(\ReflectionProperty $rp): Property
{
$property = (new Factory)->fromPropertyReflection($rp);
$property->setHooks([])->setAbstract(false);
$this->class->addMember($property);
return $property;
}
}
@@ -0,0 +1,198 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use function array_diff, array_map, strtolower;
/**
* Definition of a class with properties, methods, constants, traits and PHP attributes.
*/
final class ClassType extends ClassLike
{
use Traits\ConstantsAware;
use Traits\MethodsAware;
use Traits\PropertiesAware;
use Traits\TraitsAware;
#[\Deprecated]
public const
TYPE_CLASS = 'class',
TYPE_INTERFACE = 'interface',
TYPE_TRAIT = 'trait',
TYPE_ENUM = 'enum';
private bool $final = false;
private bool $abstract = false;
private ?string $extends = null;
private bool $readOnly = false;
/** @var string[] */
private array $implements = [];
public function __construct(?string $name = null, ?PhpNamespace $namespace = null)
{
if ($name === null) {
parent::__construct('foo', $namespace);
$this->setName(null);
} else {
parent::__construct($name, $namespace);
}
}
public function setFinal(bool $state = true): static
{
$this->final = $state;
return $this;
}
public function isFinal(): bool
{
return $this->final;
}
public function setAbstract(bool $state = true): static
{
$this->abstract = $state;
return $this;
}
public function isAbstract(): bool
{
return $this->abstract;
}
public function setReadOnly(bool $state = true): static
{
$this->readOnly = $state;
return $this;
}
public function isReadOnly(): bool
{
return $this->readOnly;
}
public function setExtends(?string $name): static
{
if ($name) {
$this->validateNames([$name]);
}
$this->extends = $name;
return $this;
}
public function getExtends(): ?string
{
return $this->extends;
}
/**
* @param string[] $names
*/
public function setImplements(array $names): static
{
$this->validateNames($names);
$this->implements = $names;
return $this;
}
/** @return string[] */
public function getImplements(): array
{
return $this->implements;
}
public function addImplement(string $name): static
{
$this->validateNames([$name]);
$this->implements[] = $name;
return $this;
}
public function removeImplement(string $name): static
{
$this->implements = array_diff($this->implements, [$name]);
return $this;
}
public function addMember(Method|Property|Constant|TraitUse $member, bool $overwrite = false): static
{
$name = $member->getName();
[$type, $n] = match (true) {
$member instanceof Constant => ['consts', $name],
$member instanceof Method => ['methods', strtolower($name)],
$member instanceof Property => ['properties', $name],
$member instanceof TraitUse => ['traits', $name],
};
if (!$overwrite && isset($this->$type[$n])) {
throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists.");
}
$this->$type[$n] = $member;
return $this;
}
/**
* @deprecated use ClassManipulator::inheritProperty()
*/
public function inheritProperty(string $name, bool $returnIfExists = false): Property
{
return (new ClassManipulator($this))->inheritProperty($name, $returnIfExists);
}
/**
* @deprecated use ClassManipulator::inheritMethod()
*/
public function inheritMethod(string $name, bool $returnIfExists = false): Method
{
return (new ClassManipulator($this))->inheritMethod($name, $returnIfExists);
}
/** @throws Nette\InvalidStateException */
public function validate(): void
{
$name = $this->getName();
if ($name === null && ($this->abstract || $this->final)) {
throw new Nette\InvalidStateException('Anonymous class cannot be abstract or final.');
} elseif ($this->abstract && $this->final) {
throw new Nette\InvalidStateException("Class '$name' cannot be abstract and final at the same time.");
}
}
public function __clone(): void
{
parent::__clone();
$clone = fn($item) => clone $item;
$this->consts = array_map($clone, $this->consts);
$this->methods = array_map($clone, $this->methods);
$this->properties = array_map($clone, $this->properties);
$this->traits = array_map($clone, $this->traits);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Definition of a closure.
*/
final class Closure
{
use Traits\FunctionLike;
use Traits\AttributeAware;
/** @var Parameter[] */
private array $uses = [];
public static function from(\Closure $closure): self
{
return (new Factory)->fromFunctionReflection(new \ReflectionFunction($closure));
}
public function __toString(): string
{
return (new Printer)->printClosure($this);
}
/**
* Replaces all uses.
* @param Parameter[] $uses
*/
public function setUses(array $uses): static
{
(function (Parameter ...$uses) {})(...$uses);
$this->uses = $uses;
return $this;
}
/** @return Parameter[] */
public function getUses(): array
{
return $this->uses;
}
public function addUse(string $name): Parameter
{
return $this->uses[] = new Parameter($name);
}
public function __clone(): void
{
$this->parameters = array_map(fn($param) => clone $param, $this->parameters);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Definition of a class constant.
*/
final class Constant
{
use Traits\NameAware;
use Traits\VisibilityAware;
use Traits\CommentAware;
use Traits\AttributeAware;
private mixed $value;
private bool $final = false;
private ?string $type = null;
public function setValue(mixed $val): static
{
$this->value = $val;
return $this;
}
public function getValue(): mixed
{
return $this->value;
}
public function setFinal(bool $state = true): static
{
$this->final = $state;
return $this;
}
public function isFinal(): bool
{
return $this->final;
}
public function setType(?string $type): static
{
Helpers::validateType($type);
$this->type = $type;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
}
@@ -0,0 +1,288 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use function addcslashes, array_keys, array_shift, count, dechex, implode, in_array, is_array, is_int, is_object, is_resource, is_string, ltrim, method_exists, ord, preg_match, preg_replace, preg_replace_callback, preg_split, range, serialize, str_contains, str_pad, str_repeat, str_replace, strlen, strrpos, strtoupper, substr, trim, unserialize, var_export;
use const PREG_SPLIT_DELIM_CAPTURE, STR_PAD_LEFT;
/**
* Generates a PHP representation of a variable.
*/
final class Dumper
{
private const IndentLength = 4;
public int $maxDepth = 50;
public int $wrapLength = 120;
public string $indentation = "\t";
public bool $customObjects = true;
/**
* Returns a PHP representation of a variable.
*/
public function dump(mixed $var, int $column = 0): string
{
return $this->dumpVar($var, [], 0, $column);
}
/** @param array<mixed[]|object> $parents */
private function dumpVar(mixed $var, array $parents = [], int $level = 0, int $column = 0): string
{
if ($var === null) {
return 'null';
} elseif (is_string($var)) {
return $this->dumpString($var);
} elseif (is_array($var)) {
return $this->dumpArray($var, $parents, $level, $column);
} elseif ($var instanceof Literal) {
return $this->dumpLiteral($var, $level);
} elseif (is_object($var)) {
return $this->dumpObject($var, $parents, $level, $column);
} elseif (is_resource($var)) {
throw new Nette\InvalidStateException('Cannot dump value of type resource.');
} else {
return var_export($var, return: true);
}
}
private function dumpString(string $s): string
{
$special = [
"\r" => '\r',
"\n" => '\n',
"\t" => '\t',
"\e" => '\e',
'\\' => '\\\\',
];
$utf8 = preg_match('##u', $s);
$escaped = preg_replace_callback(
$utf8 ? '#[\p{C}\\\]#u' : '#[\x00-\x1F\x7F-\xFF\\\]#',
fn($m) => $special[$m[0]] ?? (strlen($m[0]) === 1
? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT)
: '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}'),
$s,
);
return $s === str_replace('\\\\', '\\', $escaped)
? "'" . preg_replace('#\'|\\\(?=[\'\\\]|$)#D', '\\\$0', $s) . "'"
: '"' . addcslashes($escaped, '"$') . '"';
}
private static function utf8Ord(string $c): int
{
$ord0 = ord($c[0]);
return match (true) {
$ord0 < 0x80 => $ord0,
$ord0 < 0xE0 => ($ord0 << 6) + ord($c[1]) - 0x3080,
$ord0 < 0xF0 => ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080,
default => ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080,
};
}
/**
* @param mixed[] $var
* @param array<mixed[]|object> $parents
*/
private function dumpArray(array $var, array $parents, int $level, int $column): string
{
if (empty($var)) {
return '[]';
} elseif ($level > $this->maxDepth || in_array($var, $parents, strict: true)) {
throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.');
}
$parents[] = $var;
$hideKeys = is_int(($keys = array_keys($var))[0]) && $keys === range($keys[0], $keys[0] + count($var) - 1);
$pairs = [];
foreach ($var as $k => $v) {
$keyPart = $hideKeys && ($k !== $keys[0] || $k === 0)
? ''
: $this->dumpVar($k) . ' => ';
$pairs[] = $keyPart . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart) + 1); // 1 = comma after item
}
$line = '[' . implode(', ', $pairs) . ']';
$space = str_repeat($this->indentation, $level);
return !str_contains($line, "\n") && $level * self::IndentLength + $column + strlen($line) <= $this->wrapLength
? $line
: "[\n$space" . $this->indentation . implode(",\n$space" . $this->indentation, $pairs) . ",\n$space]";
}
/** @param array<mixed[]|object> $parents */
private function dumpObject(object $var, array $parents, int $level, int $column): string
{
if ($level > $this->maxDepth || in_array($var, $parents, strict: true)) {
throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.');
} elseif ((new \ReflectionObject($var))->isAnonymous()) {
throw new Nette\InvalidStateException('Cannot dump an instance of an anonymous class.');
}
$class = $var::class;
$parents[] = $var;
if ($class === \stdClass::class) {
$var = (array) $var;
return '(object) ' . $this->dumpArray($var, $parents, $level, $column + 10);
} elseif ($class === \DateTime::class || $class === \DateTimeImmutable::class) {
return $this->format(
"new \\$class(?, new \\DateTimeZone(?))",
$var->format('Y-m-d H:i:s.u'),
$var->getTimeZone()->getName(),
);
} elseif ($var instanceof \UnitEnum) {
return '\\' . $var::class . '::' . $var->name;
} elseif ($var instanceof \Closure) {
$inner = Nette\Utils\Callback::unwrap($var);
if (Nette\Utils\Callback::isStatic($inner)) {
return implode('::', (array) $inner) . '(...)';
}
throw new Nette\InvalidStateException('Cannot dump object of type Closure.');
} elseif ($this->customObjects) {
return $this->dumpCustomObject($var, $parents, $level);
} else {
throw new Nette\InvalidStateException("Cannot dump object of type $class.");
}
}
/** @param array<mixed[]|object> $parents */
private function dumpCustomObject(object $var, array $parents, int $level): string
{
$class = $var::class;
$space = str_repeat($this->indentation, $level);
$out = "\n";
if (method_exists($var, '__serialize')) {
$arr = $var->__serialize();
} else {
$arr = (array) $var;
if (method_exists($var, '__sleep')) {
foreach ($var->__sleep() as $v) {
$props[$v] = $props["\x00*\x00$v"] = $props["\x00$class\x00$v"] = true;
}
}
}
foreach ($arr as $k => $v) {
if (!isset($props) || isset($props[$k])) {
$out .= $space . $this->indentation
. ($keyPart = $this->dumpVar($k) . ' => ')
. $this->dumpVar($v, $parents, $level + 1, strlen($keyPart))
. ",\n";
}
}
return '\\' . self::class . "::createObject(\\$class::class, [$out$space])";
}
private function dumpLiteral(Literal $var, int $level): string
{
$s = $var->formatWith($this);
$s = Nette\Utils\Strings::unixNewLines($s);
$s = Nette\Utils\Strings::indent(trim($s), $level, $this->indentation);
return ltrim($s, $this->indentation);
}
/**
* Generates PHP statement. Supports placeholders: ? \? $? ->? ::? ...? ...?: ?*
*/
public function format(string $statement, mixed ...$args): string
{
$tokens = preg_split('#(\.\.\.\?:?|\$\?|->\?|::\?|\\\\\?|\?\*|\?(?!\w))#', $statement, -1, PREG_SPLIT_DELIM_CAPTURE);
$res = '';
foreach ($tokens as $n => $token) {
if ($n % 2 === 0) {
$res .= $token;
} elseif ($token === '\?') {
$res .= '?';
} elseif (!$args) {
throw new Nette\InvalidArgumentException('Insufficient number of arguments.');
} elseif ($token === '?') {
$res .= $this->dump(array_shift($args), strlen($res) - strrpos($res, "\n"));
} elseif ($token === '...?' || $token === '...?:' || $token === '?*') {
$arg = array_shift($args);
if (!is_array($arg)) {
throw new Nette\InvalidArgumentException('Argument must be an array.');
}
$res .= $this->dumpArguments($arg, strlen($res) - strrpos($res, "\n"), $token === '...?:');
} else { // $ -> ::
$arg = array_shift($args);
if ($arg instanceof Literal || !Helpers::isIdentifier($arg)) {
$arg = '{' . $this->dumpVar($arg) . '}';
}
$res .= substr($token, 0, -1) . $arg;
}
}
if ($args) {
throw new Nette\InvalidArgumentException('Insufficient number of placeholders.');
}
return $res;
}
/** @param mixed[] $args */
private function dumpArguments(array $args, int $column, bool $named): string
{
$pairs = [];
foreach ($args as $k => $v) {
$name = $named && !is_int($k) ? $k . ': ' : '';
$pairs[] = $name . $this->dumpVar($v, [$args], 0, $column + strlen($name) + 1); // 1 = ) after args
}
$line = implode(', ', $pairs);
return count($args) < 2 || (!str_contains($line, "\n") && $column + strlen($line) <= $this->wrapLength)
? $line
: "\n" . $this->indentation . implode(",\n" . $this->indentation, $pairs) . ",\n";
}
/**
* @param mixed[] $props
* @internal
*/
public static function createObject(string $class, array $props): object
{
if (method_exists($class, '__serialize')) {
$obj = (new \ReflectionClass($class))->newInstanceWithoutConstructor();
$obj->__unserialize($props);
return $obj;
}
return unserialize('O' . substr(serialize($class), 1, -1) . substr(serialize($props), 1));
}
}
@@ -0,0 +1,36 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Definition of an enum case.
*/
final class EnumCase
{
use Traits\NameAware;
use Traits\CommentAware;
use Traits\AttributeAware;
private string|int|Literal|null $value = null;
public function setValue(string|int|Literal|null $val): static
{
$this->value = $val;
return $this;
}
public function getValue(): string|int|Literal|null
{
return $this->value;
}
}
@@ -0,0 +1,148 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of an enum with cases, methods, constants and traits.
*/
final class EnumType extends ClassLike
{
use Traits\ConstantsAware;
use Traits\MethodsAware;
use Traits\TraitsAware;
/** @var string[] */
private array $implements = [];
/** @var array<string, EnumCase> */
private array $cases = [];
private ?string $type = null;
public function setType(?string $type): static
{
$this->type = $type;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
/**
* @param string[] $names
*/
public function setImplements(array $names): static
{
$this->validateNames($names);
$this->implements = $names;
return $this;
}
/** @return string[] */
public function getImplements(): array
{
return $this->implements;
}
public function addImplement(string $name): static
{
$this->validateNames([$name]);
$this->implements[] = $name;
return $this;
}
public function removeImplement(string $name): static
{
$this->implements = array_diff($this->implements, [$name]);
return $this;
}
/**
* Sets cases to enum
* @param EnumCase[] $cases
*/
public function setCases(array $cases): static
{
(function (EnumCase ...$cases) {})(...$cases);
$this->cases = [];
foreach ($cases as $case) {
$this->cases[$case->getName()] = $case;
}
return $this;
}
/** @return EnumCase[] */
public function getCases(): array
{
return $this->cases;
}
/** Adds case to enum */
public function addCase(string $name, string|int|Literal|null $value = null, bool $overwrite = false): EnumCase
{
if (!$overwrite && isset($this->cases[$name])) {
throw new Nette\InvalidStateException("Cannot add cases '$name', because it already exists.");
}
return $this->cases[$name] = (new EnumCase($name))
->setValue($value);
}
public function removeCase(string $name): static
{
unset($this->cases[$name]);
return $this;
}
/**
* Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true.
*/
public function addMember(Method|Constant|EnumCase|TraitUse $member, bool $overwrite = false): static
{
$name = $member->getName();
[$type, $n] = match (true) {
$member instanceof Constant => ['consts', $name],
$member instanceof Method => ['methods', strtolower($name)],
$member instanceof TraitUse => ['traits', $name],
$member instanceof EnumCase => ['cases', $name],
};
if (!$overwrite && isset($this->$type[$n])) {
throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists.");
}
$this->$type[$n] = $member;
return $this;
}
public function __clone(): void
{
parent::__clone();
$clone = fn($item) => clone $item;
$this->consts = array_map($clone, $this->consts);
$this->methods = array_map($clone, $this->methods);
$this->traits = array_map($clone, $this->traits);
$this->cases = array_map($clone, $this->cases);
}
}
@@ -0,0 +1,595 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use PhpParser;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\ParserFactory;
use function addcslashes, array_map, assert, class_exists, end, in_array, is_array, rtrim, str_contains, str_repeat, str_replace, str_starts_with, strlen, substr, substr_replace, usort;
/**
* Extracts information from PHP code.
* @internal
*/
final class Extractor
{
private string $code;
/** @var Node[] */
private array $statements;
private PhpParser\PrettyPrinterAbstract $printer;
public function __construct(string $code)
{
if (!class_exists(ParserFactory::class)) {
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser' 4.7 or newer.");
}
$this->printer = new PhpParser\PrettyPrinter\Standard;
$this->parseCode($code);
}
private function parseCode(string $code): void
{
if (!str_starts_with($code, '<?php')) {
throw new Nette\InvalidStateException('The input string is not a PHP code.');
}
$this->code = Nette\Utils\Strings::unixNewLines($code);
$parser = (new ParserFactory)->createForNewestSupportedVersion();
$stmts = $parser->parse($this->code);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new PhpParser\NodeVisitor\ParentConnectingVisitor);
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['preserveOriginalNames' => true]));
$this->statements = $traverser->traverse($stmts);
}
/** @return array<string, string> */
public function extractMethodBodies(string $className): array
{
$nodeFinder = new NodeFinder;
$classNode = $nodeFinder->findFirst(
$this->statements,
fn(Node $node) => $node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $className,
);
$res = [];
foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\ClassMethod::class) as $methodNode) {
if ($methodNode->stmts) {
$res[$methodNode->name->toString()] = $this->getReformattedContents($methodNode->stmts, 2);
}
}
return $res;
}
/** @return array<string, array<string, array{string, bool}>> */
public function extractPropertyHookBodies(string $className): array
{
if (!class_exists(Node\PropertyHook::class)) {
return [];
}
$nodeFinder = new NodeFinder;
$classNode = $nodeFinder->findFirst(
$this->statements,
fn(Node $node) => $node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $className,
);
$res = [];
foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\Property::class) as $propertyNode) {
foreach ($propertyNode->props as $propNode) {
$propName = $propNode->name->toString();
foreach ($propertyNode->hooks as $hookNode) {
$body = $hookNode->body;
if ($body !== null) {
$contents = $this->getReformattedContents(is_array($body) ? $body : [$body], 3);
$res[$propName][$hookNode->name->toString()] = [$contents, !is_array($body)];
}
}
}
}
return $res;
}
public function extractFunctionBody(string $name): string
{
$functionNode = (new NodeFinder)->findFirst(
$this->statements,
fn(Node $node) => $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $name,
);
assert($functionNode instanceof Node\Stmt\Function_);
return $this->getReformattedContents($functionNode->stmts, 1);
}
/** @param Node[] $nodes */
private function getReformattedContents(array $nodes, int $level): string
{
if (!$nodes) {
return '';
}
$body = $this->getNodeContents(...$nodes);
$body = $this->performReplacements($body, $this->prepareReplacements($nodes, $level));
return Helpers::unindent($body, $level);
}
/**
* @param Node[] $nodes
* @return array<array{int, int, string}>
*/
private function prepareReplacements(array $nodes, int $level): array
{
$start = $this->getNodeStartPos($nodes[0]);
$replacements = [];
$indent = "\n" . str_repeat("\t", $level);
(new NodeFinder)->find($nodes, function (Node $node) use (&$replacements, $start, $level, $indent) {
if ($node instanceof Node\Name\FullyQualified) {
if ($node->getAttribute('originalName') instanceof Node\Name) {
$of = match (true) {
$node->getAttribute('parent') instanceof Node\Expr\ConstFetch => PhpNamespace::NameConstant,
$node->getAttribute('parent') instanceof Node\Expr\FuncCall => PhpNamespace::NameFunction,
default => PhpNamespace::NameNormal,
};
$replacements[] = [
$node->getStartFilePos() - $start,
$node->getEndFilePos() - $start,
Helpers::tagName($node->toCodeString(), $of),
];
}
} elseif (
$node instanceof Node\Scalar\String_
&& in_array($node->getAttribute('kind'), [Node\Scalar\String_::KIND_SINGLE_QUOTED, Node\Scalar\String_::KIND_DOUBLE_QUOTED], true)
&& str_contains($node->getAttribute('rawValue'), "\n")
) { // multi-line strings -> single line
$replacements[] = [
$node->getStartFilePos() - $start,
$node->getEndFilePos() - $start,
'"' . addcslashes($node->value, "\x00..\x1F\"") . '"',
];
} elseif (
$node instanceof Node\Scalar\String_
&& in_array($node->getAttribute('kind'), [Node\Scalar\String_::KIND_NOWDOC, Node\Scalar\String_::KIND_HEREDOC], true)
&& Helpers::unindent($node->getAttribute('docIndentation'), $level) === $node->getAttribute('docIndentation')
) { // fix indentation of NOWDOW/HEREDOC
$replacements[] = [
$node->getStartFilePos() - $start,
$node->getEndFilePos() - $start,
str_replace("\n", $indent, $this->getNodeContents($node)),
];
} elseif (
$node instanceof Node\Scalar\Encapsed
&& $node->getAttribute('kind') === Node\Scalar\String_::KIND_DOUBLE_QUOTED
) { // multi-line strings -> single line
foreach ($node->parts as $part) {
if ($part instanceof Node\Scalar\EncapsedStringPart) {
$replacements[] = [
$part->getStartFilePos() - $start,
$part->getEndFilePos() - $start,
addcslashes($part->value, "\x00..\x1F\""),
];
}
}
} elseif (
$node instanceof Node\Scalar\Encapsed && $node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC
&& Helpers::unindent($node->getAttribute('docIndentation'), $level) === $node->getAttribute('docIndentation')
) { // fix indentation of HEREDOC
$replacements[] = [
$tmp = $node->getStartFilePos() - $start + strlen($node->getAttribute('docLabel')) + 3, // <<<
$tmp,
$indent,
];
$replacements[] = [
$tmp = $node->getEndFilePos() - $start - strlen($node->getAttribute('docLabel')),
$tmp,
$indent,
];
foreach ($node->parts as $part) {
if ($part instanceof Node\Scalar\EncapsedStringPart) {
$replacements[] = [
$part->getStartFilePos() - $start,
$part->getEndFilePos() - $start,
str_replace("\n", $indent, $this->getNodeContents($part)),
];
}
}
}
});
return $replacements;
}
/** @param array<array{int, int, string}> $replacements */
private function performReplacements(string $s, array $replacements): string
{
usort($replacements, fn($a, $b) => $b[0] <=> $a[0]);
foreach ($replacements as [$start, $end, $replacement]) {
$s = substr_replace($s, $replacement, $start, $end - $start + 1);
}
return $s;
}
public function extractAll(): PhpFile
{
$phpFile = new PhpFile;
if (
$this->statements
&& !$this->statements[0] instanceof Node\Stmt\ClassLike
&& !$this->statements[0] instanceof Node\Stmt\Function_
) {
$this->addCommentAndAttributes($phpFile, $this->statements[0]);
}
$namespaces = ['' => $this->statements];
foreach ($this->statements as $node) {
if ($node instanceof Node\Stmt\Declare_
&& $node->declares[0]->key->name === 'strict_types'
&& $node->declares[0]->value instanceof Node\Scalar\LNumber
) {
$phpFile->setStrictTypes((bool) $node->declares[0]->value->value);
} elseif ($node instanceof Node\Stmt\Namespace_) {
$namespaces[$node->name->toString()] = $node->stmts;
}
}
foreach ($namespaces as $name => $nodes) {
foreach ($nodes as $node) {
match (true) {
$node instanceof Node\Stmt\Use_ => $this->addUseToNamespace($phpFile->addNamespace($name), $node),
$node instanceof Node\Stmt\ClassLike => $this->addClassLikeToFile($phpFile, $node),
$node instanceof Node\Stmt\Function_ => $this->addFunctionToFile($phpFile, $node),
default => null,
};
}
}
return $phpFile;
}
private function addUseToNamespace(PhpNamespace $namespace, Node\Stmt\Use_ $node): void
{
$of = [
$node::TYPE_NORMAL => PhpNamespace::NameNormal,
$node::TYPE_FUNCTION => PhpNamespace::NameFunction,
$node::TYPE_CONSTANT => PhpNamespace::NameConstant,
][$node->type];
foreach ($node->uses as $use) {
$namespace->addUse($use->name->toString(), $use->alias?->toString(), $of);
}
}
private function addClassLikeToFile(PhpFile $phpFile, Node\Stmt\ClassLike $node): ClassLike
{
if ($node instanceof Node\Stmt\Class_) {
$class = $phpFile->addClass($node->namespacedName->toString());
$class->setFinal($node->isFinal());
$class->setAbstract($node->isAbstract());
$class->setReadOnly($node->isReadonly());
if ($node->extends) {
$class->setExtends($node->extends->toString());
}
foreach ($node->implements as $item) {
$class->addImplement($item->toString());
}
} elseif ($node instanceof Node\Stmt\Interface_) {
$class = $phpFile->addInterface($node->namespacedName->toString());
foreach ($node->extends as $item) {
$class->addExtend($item->toString());
}
} elseif ($node instanceof Node\Stmt\Trait_) {
$class = $phpFile->addTrait($node->namespacedName->toString());
} elseif ($node instanceof Node\Stmt\Enum_) {
$class = $phpFile->addEnum($node->namespacedName->toString());
$class->setType($node->scalarType?->toString());
foreach ($node->implements as $item) {
$class->addImplement($item->toString());
}
} else {
throw new Nette\ShouldNotHappenException;
}
$this->addCommentAndAttributes($class, $node);
$this->addClassMembers($class, $node);
return $class;
}
private function addClassMembers(ClassLike $class, Node\Stmt\ClassLike $node): void
{
foreach ($node->stmts as $stmt) {
match (true) {
$stmt instanceof Node\Stmt\TraitUse => $this->addTraitToClass($class, $stmt),
$stmt instanceof Node\Stmt\Property => $this->addPropertyToClass($class, $stmt),
$stmt instanceof Node\Stmt\ClassMethod => $this->addMethodToClass($class, $stmt),
$stmt instanceof Node\Stmt\ClassConst => $this->addConstantToClass($class, $stmt),
$stmt instanceof Node\Stmt\EnumCase => $this->addEnumCaseToClass($class, $stmt),
default => null,
};
}
}
private function addTraitToClass(ClassLike $class, Node\Stmt\TraitUse $node): void
{
foreach ($node->traits as $item) {
$trait = $class->addTrait($item->toString());
}
assert($trait instanceof TraitUse);
foreach ($node->adaptations as $item) {
$trait->addResolution(rtrim($this->getReformattedContents([$item], 0), ';'));
}
$this->addCommentAndAttributes($trait, $node);
}
private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node): void
{
foreach ($node->props as $item) {
$prop = $class->addProperty($item->name->toString());
$prop->setStatic($node->isStatic());
$prop->setVisibility($this->toVisibility($node->flags), $this->toSetterVisibility($node->flags));
$prop->setType($node->type ? $this->toPhp($node->type) : null);
if ($item->default) {
$prop->setValue($this->toValue($item->default));
}
$prop->setReadOnly($node->isReadonly() || ($class instanceof ClassType && $class->isReadOnly()));
$this->addCommentAndAttributes($prop, $node);
$prop->setAbstract((bool) ($node->flags & Modifiers::ABSTRACT));
$prop->setFinal((bool) ($node->flags & Modifiers::FINAL));
$this->addHooksToProperty($prop, $node);
}
}
private function addHooksToProperty(Property|PromotedParameter $prop, Node\Stmt\Property|Node\Param $node): void
{
if (!class_exists(Node\PropertyHook::class)) {
return;
}
foreach ($node->hooks as $hookNode) {
$hook = $prop->addHook($hookNode->name->toString());
$hook->setFinal((bool) ($hookNode->flags & Modifiers::FINAL));
$this->setupFunction($hook, $hookNode);
if ($hookNode->body === null) {
$hook->setAbstract();
} elseif (!is_array($hookNode->body)) {
$hook->setBody($this->getReformattedContents([$hookNode->body], 1), short: true);
}
}
}
private function addMethodToClass(ClassLike $class, Node\Stmt\ClassMethod $node): void
{
$method = $class->addMethod($node->name->toString());
$method->setAbstract($node->isAbstract());
$method->setFinal($node->isFinal());
$method->setStatic($node->isStatic());
$method->setVisibility($this->toVisibility($node->flags));
$this->setupFunction($method, $node);
if ($method->getName() === Method::Constructor && $class instanceof ClassType && $class->isReadOnly()) {
array_map(fn($param) => $param instanceof PromotedParameter ? $param->setReadOnly() : $param, $method->getParameters());
}
}
private function addConstantToClass(ClassLike $class, Node\Stmt\ClassConst $node): void
{
foreach ($node->consts as $item) {
$const = $class->addConstant($item->name->toString(), $this->toValue($item->value));
$const->setVisibility($this->toVisibility($node->flags));
$const->setFinal($node->isFinal());
$this->addCommentAndAttributes($const, $node);
}
}
private function addEnumCaseToClass(EnumType $class, Node\Stmt\EnumCase $node): void
{
$value = match (true) {
$node->expr === null => null,
$node->expr instanceof Node\Scalar\LNumber, $node->expr instanceof Node\Scalar\String_ => $node->expr->value,
default => $this->toValue($node->expr),
};
$case = $class->addCase($node->name->toString(), $value);
$this->addCommentAndAttributes($case, $node);
}
private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void
{
$function = $phpFile->addFunction($node->namespacedName->toString());
$this->setupFunction($function, $node);
}
private function addCommentAndAttributes(
PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse|PropertyHook $element,
Node $node,
): void
{
if ($node->getDocComment()) {
$comment = $node->getDocComment()->getReformattedText();
$comment = Helpers::unformatDocComment($comment);
$element->setComment($comment);
$node->setDocComment(new PhpParser\Comment\Doc(''));
}
foreach ($node->attrGroups ?? [] as $group) {
foreach ($group->attrs as $attribute) {
$args = [];
foreach ($attribute->args as $arg) {
if ($arg->name) {
$args[$arg->name->toString()] = $this->toValue($arg->value);
} else {
$args[] = $this->toValue($arg->value);
}
}
$element->addAttribute($attribute->name->toString(), $args);
}
}
}
private function setupFunction(GlobalFunction|Method|PropertyHook $function, Node\FunctionLike $node): void
{
$function->setReturnReference($node->returnsByRef());
if (!$function instanceof PropertyHook) {
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
}
foreach ($node->getParams() as $item) {
$getVisibility = $this->toVisibility($item->flags);
$setVisibility = $this->toSetterVisibility($item->flags);
$final = (bool) ($item->flags & Modifiers::FINAL);
if ($getVisibility || $setVisibility || $final) {
$param = $function->addPromotedParameter($item->var->name)
->setVisibility($getVisibility, $setVisibility)
->setReadonly($item->isReadonly())
->setFinal($final);
$this->addHooksToProperty($param, $item);
} else {
$param = $function->addParameter($item->var->name);
}
$param->setType($item->type ? $this->toPhp($item->type) : null);
$param->setReference($item->byRef);
if (!$function instanceof PropertyHook) {
$function->setVariadic($item->variadic);
}
if ($item->default) {
$param->setDefaultValue($this->toValue($item->default));
}
$this->addCommentAndAttributes($param, $item);
}
$this->addCommentAndAttributes($function, $node);
if ($node->getStmts()) {
$indent = $function instanceof GlobalFunction ? 1 : 2;
$function->setBody($this->getReformattedContents($node->getStmts(), $indent));
}
}
private function toValue(Node\Expr $node): mixed
{
if ($node instanceof Node\Expr\ConstFetch) {
return match ($node->name->toLowerString()) {
'null' => null,
'true' => true,
'false' => false,
default => new Literal($this->getReformattedContents([$node], 0)),
};
} elseif ($node instanceof Node\Scalar\LNumber
|| $node instanceof Node\Scalar\DNumber
|| $node instanceof Node\Scalar\String_
) {
return $node->value;
} elseif ($node instanceof Node\Expr\Array_) {
$res = [];
foreach ($node->items as $item) {
if ($item->unpack) {
return new Literal($this->getReformattedContents([$node], 0));
} elseif ($item->key) {
$key = $this->toValue($item->key);
if ($key instanceof Literal) {
return new Literal($this->getReformattedContents([$node], 0));
}
$res[$key] = $this->toValue($item->value);
} else {
$res[] = $this->toValue($item->value);
}
}
return $res;
} else {
return new Literal($this->getReformattedContents([$node], 0));
}
}
private function toVisibility(int $flags): ?Visibility
{
return match (true) {
(bool) ($flags & Modifiers::PUBLIC) => Visibility::Public,
(bool) ($flags & Modifiers::PROTECTED) => Visibility::Protected,
(bool) ($flags & Modifiers::PRIVATE) => Visibility::Private,
default => null,
};
}
private function toSetterVisibility(int $flags): ?Visibility
{
return match (true) {
!class_exists(Node\PropertyHook::class) => null,
(bool) ($flags & Modifiers::PUBLIC_SET) => Visibility::Public,
(bool) ($flags & Modifiers::PROTECTED_SET) => Visibility::Protected,
(bool) ($flags & Modifiers::PRIVATE_SET) => Visibility::Private,
default => null,
};
}
private function toPhp(Node $value): string
{
$dolly = clone $value;
$dolly->setAttribute('comments', []);
return $this->printer->prettyPrint([$dolly]);
}
private function getNodeContents(Node ...$nodes): string
{
$start = $this->getNodeStartPos($nodes[0]);
return substr($this->code, $start, end($nodes)->getEndFilePos() - $start + 1);
}
private function getNodeStartPos(Node $node): int
{
return ($comments = $node->getComments())
? $comments[0]->getStartFilePos()
: $node->getStartFilePos();
}
}
@@ -0,0 +1,378 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use Nette\Utils\Reflection;
use function array_diff, array_filter, array_key_exists, array_map, count, explode, file_get_contents, implode, is_object, is_subclass_of, method_exists, reset;
use const PHP_VERSION_ID;
/**
* Creates a representations based on reflection or source code.
*/
final class Factory
{
/** @var string[][] */
private array $bodyCache = [];
/** @var Extractor[] */
private array $extractorCache = [];
/** @param \ReflectionClass<object> $from */
public function fromClassReflection(
\ReflectionClass $from,
bool $withBodies = false,
): ClassLike
{
if ($withBodies && ($from->isAnonymous() || $from->isInternal() || $from->isInterface())) {
throw new Nette\NotSupportedException('The $withBodies parameter cannot be used for anonymous or internal classes or interfaces.');
}
$enumIface = null;
if ($from->isEnum()) {
$class = new EnumType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
$from = new \ReflectionEnum($from->getName());
$enumIface = $from->isBacked() ? \BackedEnum::class : \UnitEnum::class;
} elseif ($from->isAnonymous()) {
$class = new ClassType;
} elseif ($from->isInterface()) {
$class = new InterfaceType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
} elseif ($from->isTrait()) {
$class = new TraitType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
} else {
$class = new ClassType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
$class->setFinal($from->isFinal() && $class->isClass());
$class->setAbstract($from->isAbstract() && $class->isClass());
$class->setReadOnly(PHP_VERSION_ID >= 80200 && $from->isReadOnly());
}
$ifaces = $from->getInterfaceNames();
foreach ($ifaces as $iface) {
$ifaces = array_filter($ifaces, fn(string $item): bool => !is_subclass_of($iface, $item));
}
if ($from->isInterface()) {
$class->setExtends($ifaces);
} elseif ($ifaces) {
$ifaces = array_diff($ifaces, [$enumIface]);
$class->setImplements($ifaces);
}
$class->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
$class->setAttributes($this->getAttributes($from));
if ($from->getParentClass()) {
$class->setExtends($from->getParentClass()->name);
$class->setImplements(array_diff($class->getImplements(), $from->getParentClass()->getInterfaceNames()));
}
$props = [];
foreach ($from->getProperties() as $prop) {
$declaringClass = Reflection::getPropertyDeclaringClass($prop);
if ($prop->isDefault()
&& $declaringClass->name === $from->name
&& !$prop->isPromoted()
&& !$class->isEnum()
) {
$props[] = $p = $this->fromPropertyReflection($prop);
if ($withBodies) {
$hookBodies ??= $this->getExtractor($declaringClass->getFileName())->extractPropertyHookBodies($declaringClass->name);
foreach ($hookBodies[$prop->getName()] ?? [] as $hookType => [$body, $short]) {
$p->getHook($hookType)->setBody($body, short: $short);
}
}
}
}
if ($props) {
$class->setProperties($props);
}
$methods = $resolutions = [];
foreach ($from->getMethods() as $method) {
$declaringMethod = Reflection::getMethodDeclaringMethod($method);
$declaringClass = $declaringMethod->getDeclaringClass();
if (
$declaringClass->name === $from->name
&& (!$enumIface || !method_exists($enumIface, $method->name))
) {
$methods[] = $m = $this->fromMethodReflection($method);
if ($withBodies) {
$bodies = &$this->bodyCache[$declaringClass->name];
$bodies ??= $this->getExtractor($declaringClass->getFileName())->extractMethodBodies($declaringClass->name);
if (isset($bodies[$declaringMethod->name])) {
$m->setBody($bodies[$declaringMethod->name]);
}
}
}
$modifier = $declaringMethod->getModifiers() !== $method->getModifiers()
? ' ' . $this->getVisibility($method)->value
: null;
$alias = $declaringMethod->name !== $method->name ? ' ' . $method->name : '';
if ($modifier || $alias) {
$resolutions[] = $declaringMethod->name . ' as' . $modifier . $alias;
}
}
$class->setMethods($methods);
foreach ($from->getTraitNames() as $trait) {
$trait = $class->addTrait($trait);
foreach ($resolutions as $resolution) {
$trait->addResolution($resolution);
}
$resolutions = [];
}
$consts = $cases = [];
foreach ($from->getReflectionConstants() as $const) {
if ($class->isEnum() && $from->hasCase($const->name)) {
$cases[] = $this->fromCaseReflection($const);
} elseif ($const->getDeclaringClass()->name === $from->name) {
$consts[] = $this->fromConstantReflection($const);
}
}
if ($consts) {
$class->setConstants($consts);
}
if ($cases) {
$class->setCases($cases);
}
return $class;
}
public function fromMethodReflection(\ReflectionMethod $from): Method
{
$method = new Method($from->name);
$method->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters()));
$method->setStatic($from->isStatic());
$isInterface = $from->getDeclaringClass()->isInterface();
$method->setVisibility($isInterface ? null : $this->getVisibility($from));
$method->setFinal($from->isFinal());
$method->setAbstract($from->isAbstract() && !$isInterface);
$method->setReturnReference($from->returnsReference());
$method->setVariadic($from->isVariadic());
$method->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
$method->setAttributes($this->getAttributes($from));
$method->setReturnType((string) $from->getReturnType());
return $method;
}
public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false): GlobalFunction|Closure
{
$function = $from->isClosure() ? new Closure : new GlobalFunction($from->name);
$function->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters()));
$function->setReturnReference($from->returnsReference());
$function->setVariadic($from->isVariadic());
if (!$from->isClosure()) {
$function->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
}
$function->setAttributes($this->getAttributes($from));
$function->setReturnType((string) $from->getReturnType());
if ($withBody) {
if ($from->isClosure() || $from->isInternal()) {
throw new Nette\NotSupportedException('The $withBody parameter cannot be used for closures or internal functions.');
}
$function->setBody($this->getExtractor($from->getFileName())->extractFunctionBody($from->name));
}
return $function;
}
public function fromCallable(callable $from): Method|GlobalFunction|Closure
{
$ref = Nette\Utils\Callback::toReflection($from);
return $ref instanceof \ReflectionMethod
? $this->fromMethodReflection($ref)
: $this->fromFunctionReflection($ref);
}
public function fromParameterReflection(\ReflectionParameter $from): Parameter
{
if ($from->isPromoted()) {
$property = $from->getDeclaringClass()->getProperty($from->name);
$param = (new PromotedParameter($from->name))
->setVisibility($this->getVisibility($property))
->setReadOnly($property->isReadonly())
->setFinal(PHP_VERSION_ID >= 80500 && $property->isFinal() && !$property->isPrivateSet());
$this->addHooks($property, $param);
} else {
$param = new Parameter($from->name);
}
$param->setReference($from->isPassedByReference());
$param->setType((string) $from->getType());
if ($from->isDefaultValueAvailable()) {
if ($from->isDefaultValueConstant()) {
$parts = explode('::', $from->getDefaultValueConstantName());
if (count($parts) > 1) {
$parts[0] = Helpers::tagName($parts[0]);
}
$param->setDefaultValue(new Literal(implode('::', $parts)));
} elseif (is_object($from->getDefaultValue())) {
$param->setDefaultValue($this->fromObject($from->getDefaultValue()));
} else {
$param->setDefaultValue($from->getDefaultValue());
}
}
$param->setAttributes($this->getAttributes($from));
return $param;
}
public function fromConstantReflection(\ReflectionClassConstant $from): Constant
{
$const = new Constant($from->name);
$const->setValue($from->getValue());
$const->setVisibility($this->getVisibility($from));
$const->setFinal($from->isFinal());
$const->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
$const->setAttributes($this->getAttributes($from));
return $const;
}
public function fromCaseReflection(\ReflectionClassConstant $from): EnumCase
{
$const = new EnumCase($from->name);
$const->setValue($from->getValue()->value ?? null);
$const->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
$const->setAttributes($this->getAttributes($from));
return $const;
}
public function fromPropertyReflection(\ReflectionProperty $from): Property
{
$defaults = $from->getDeclaringClass()->getDefaultProperties();
$prop = new Property($from->name);
$prop->setValue($defaults[$prop->getName()] ?? null);
$prop->setStatic($from->isStatic());
$prop->setVisibility($this->getVisibility($from));
$prop->setType((string) $from->getType());
$prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults));
$prop->setReadOnly($from->isReadOnly());
$prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
$prop->setAttributes($this->getAttributes($from));
if (PHP_VERSION_ID >= 80400) {
$this->addHooks($from, $prop);
$isInterface = $from->getDeclaringClass()->isInterface();
$prop->setFinal($from->isFinal() && !$prop->isPrivate(PropertyAccessMode::Set));
$prop->setAbstract($from->isAbstract() && !$isInterface);
}
return $prop;
}
private function addHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void
{
if (PHP_VERSION_ID < 80400) {
return;
}
$getV = $this->getVisibility($from);
$setV = $from->isPrivateSet()
? Visibility::Private
: ($from->isProtectedSet() ? Visibility::Protected : $getV);
$defaultSetV = $from->isReadOnly() && $getV !== Visibility::Private
? Visibility::Protected
: $getV;
if ($setV !== $defaultSetV) {
$prop->setVisibility($getV === Visibility::Public ? null : $getV, $setV);
}
foreach ($from->getHooks() as $type => $hook) {
$params = $hook->getParameters();
if (
count($params) === 1
&& $params[0]->getName() === 'value'
&& $params[0]->getType() == $from->getType() // intentionally ==
) {
$params = [];
}
$prop->addHook($type)
->setParameters(array_map([$this, 'fromParameterReflection'], $params))
->setAbstract($hook->isAbstract())
->setFinal($hook->isFinal())
->setReturnReference($hook->returnsReference())
->setComment(Helpers::unformatDocComment((string) $hook->getDocComment()))
->setAttributes($this->getAttributes($hook));
}
}
public function fromObject(object $obj): Literal
{
return new Literal('new \\' . $obj::class . '(/* unknown */)');
}
public function fromClassCode(string $code): ClassLike
{
$classes = $this->fromCode($code)->getClasses();
return reset($classes) ?: throw new Nette\InvalidStateException('The code does not contain any class.');
}
public function fromCode(string $code): PhpFile
{
$reader = new Extractor($code);
return $reader->extractAll();
}
/** @return Attribute[] */
private function getAttributes($from): array
{
return array_map(function ($attr) {
$args = $attr->getArguments();
foreach ($args as &$arg) {
if (is_object($arg)) {
$arg = $this->fromObject($arg);
}
}
return new Attribute($attr->getName(), $args);
}, $from->getAttributes());
}
private function getVisibility(\ReflectionProperty|\ReflectionMethod|\ReflectionClassConstant $from): Visibility
{
return $from->isPrivate()
? Visibility::Private
: ($from->isProtected() ? Visibility::Protected : Visibility::Public);
}
private function getExtractor(string $file): Extractor
{
$cache = &$this->extractorCache[$file];
$cache ??= new Extractor(file_get_contents($file));
return $cache;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of a global function.
*/
final class GlobalFunction
{
use Traits\FunctionLike;
use Traits\NameAware;
use Traits\CommentAware;
use Traits\AttributeAware;
public static function from(string|\Closure $function, bool $withBody = false): self
{
return (new Factory)->fromFunctionReflection(Nette\Utils\Callback::toReflection($function), $withBody);
}
public function __toString(): string
{
return (new Printer)->printFunction($this);
}
public function __clone(): void
{
$this->parameters = array_map(fn($param) => clone $param, $this->parameters);
}
}
@@ -0,0 +1,156 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use function is_string, preg_match, preg_replace, preg_replace_callback, str_contains, str_repeat, str_replace, strrpos, strtolower, substr, trim;
/**
* @internal
*/
final class Helpers
{
use Nette\StaticClass;
public const ReIdentifier = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*';
public const Keywords = [
// class keywords
'bool' => 1, 'false' => 1, 'float' => 1, 'int' => 1, 'iterable' => 1, 'mixed' => 1, 'never' => 1, 'null' => 1,
'object' => 1, 'parent' => 1, 'self' => 1, 'string' => 1, 'true' => 1, 'void' => 1,
// PHP keywords
'__halt_compiler' => 1, 'abstract' => 1, 'and' => 1, 'array' => 1, 'as' => 1, 'break' => 1, 'callable' => 1,
'case' => 1, 'catch' => 1, 'class' => 1, 'clone' => 1, 'const' => 1, 'continue' => 1, 'declare' => 1, 'default' => 1,
'die' => 1, 'do' => 1, 'echo' => 1, 'else' => 1, 'elseif' => 1, 'empty' => 1, 'enddeclare' => 1, 'endfor' => 1,
'endforeach' => 1, 'endif' => 1, 'endswitch' => 1, 'endwhile' => 1, 'eval' => 1, 'exit' => 1, 'extends' => 1,
'final' => 1, 'finally' => 1, 'fn' => 1, 'for' => 1, 'foreach' => 1, 'function' => 1, 'global' => 1, 'goto' => 1,
'if' => 1, 'implements' => 1, 'include' => 1, 'include_once' => 1, 'instanceof' => 1, 'insteadof' => 1,
'interface' => 1, 'isset' => 1, 'list' => 1, 'match' => 1, 'namespace' => 1, 'new' => 1, 'or' => 1, 'print' => 1,
'private' => 1, 'protected' => 1, 'public' => 1, 'readonly' => 1, 'require' => 1, 'require_once' => 1, 'return' => 1,
'static' => 1, 'switch' => 1, 'throw' => 1, 'trait' => 1, 'try' => 1, 'unset' => 1, 'use' => 1, 'var' => 1,
'while' => 1, 'xor' => 1, 'yield' => 1, '__CLASS__' => 1, '__DIR__' => 1, '__FILE__' => 1, '__FUNCTION__' => 1,
'__LINE__' => 1, '__METHOD__' => 1, '__NAMESPACE__' => 1, '__PROPERTY__' => 1, '__TRAIT__' => 1,
];
#[\Deprecated]
public const
PHP_IDENT = self::ReIdentifier,
KEYWORDS = self::Keywords;
public static function formatDocComment(string $content, bool $forceMultiLine = false): string
{
$s = trim($content);
$s = str_replace('*/', '* /', $s);
if ($s === '') {
return '';
} elseif ($forceMultiLine || str_contains($content, "\n")) {
$s = str_replace("\n", "\n * ", "/**\n$s") . "\n */";
return Nette\Utils\Strings::normalize($s) . "\n";
} else {
return "/** $s */\n";
}
}
public static function tagName(string $name, string $of = PhpNamespace::NameNormal): string
{
return isset(self::Keywords[strtolower($name)])
? $name
: "/*($of*/$name";
}
public static function simplifyTaggedNames(string $code, ?PhpNamespace $namespace): string
{
return preg_replace_callback('~/\*\(([ncf])\*/([\w\x7f-\xff\\\]++)~', function ($m) use ($namespace) {
[, $of, $name] = $m;
return $namespace
? $namespace->simplifyType($name, $of)
: $name;
}, $code);
}
public static function unformatDocComment(string $comment): string
{
return preg_replace('#^\s*\* ?#m', '', trim(trim(trim($comment), '/*')));
}
public static function unindent(string $s, int $level = 1): string
{
return $level
? preg_replace('#^(\t| {4}){1,' . $level . '}#m', '', $s)
: $s;
}
public static function isIdentifier(mixed $value): bool
{
return is_string($value) && preg_match('#^' . self::ReIdentifier . '$#D', $value);
}
public static function isNamespaceIdentifier(mixed $value, bool $allowLeadingSlash = false): bool
{
$re = '#^' . ($allowLeadingSlash ? '\\\?' : '') . self::ReIdentifier . '(\\\\' . self::ReIdentifier . ')*$#D';
return is_string($value) && preg_match($re, $value);
}
public static function extractNamespace(string $name): string
{
return ($pos = strrpos($name, '\\')) ? substr($name, 0, $pos) : '';
}
public static function extractShortName(string $name): string
{
return ($pos = strrpos($name, '\\')) === false
? $name
: substr($name, $pos + 1);
}
public static function tabsToSpaces(string $s, int $count = 4): string
{
return str_replace("\t", str_repeat(' ', $count), $s);
}
/**
* @param mixed[] $props
* @internal
*/
public static function createObject(string $class, array $props): object
{
return Dumper::createObject($class, $props);
}
public static function validateType(?string $type, bool &$nullable = false): ?string
{
if ($type === '' || $type === null) {
return null;
} elseif (!Nette\Utils\Validators::isTypeDeclaration($type)) {
throw new Nette\InvalidArgumentException("Value '$type' is not valid type.");
}
if ($type[0] === '?') {
$nullable = true;
return substr($type, 1);
}
return $type;
}
}
@@ -0,0 +1,95 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of an interface with properties, methods and constants.
*/
final class InterfaceType extends ClassLike
{
use Traits\ConstantsAware;
use Traits\MethodsAware;
use Traits\PropertiesAware;
/** @var string[] */
private array $extends = [];
/**
* @param string|string[] $names
*/
public function setExtends(string|array $names): static
{
$names = (array) $names;
$this->validateNames($names);
$this->extends = $names;
return $this;
}
/** @return string[] */
public function getExtends(): array
{
return $this->extends;
}
public function addExtend(string $name): static
{
$this->validateNames([$name]);
$this->extends[] = $name;
return $this;
}
/**
* Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true.
*/
public function addMember(Method|Constant|Property $member, bool $overwrite = false): static
{
$name = $member->getName();
[$type, $n] = match (true) {
$member instanceof Constant => ['consts', $name],
$member instanceof Method => ['methods', strtolower($name)],
$member instanceof Property => ['properties', $name],
};
if (!$overwrite && isset($this->$type[$n])) {
throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists.");
}
$this->$type[$n] = $member;
return $this;
}
/** @throws Nette\InvalidStateException */
public function validate(): void
{
foreach ($this->getProperties() as $property) {
if ($property->isInitialized()) {
throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have initialized properties.");
} elseif (!$property->getHooks()) {
throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have properties without hooks.");
}
}
}
public function __clone(): void
{
parent::__clone();
$clone = fn($item) => clone $item;
$this->consts = array_map($clone, $this->consts);
$this->methods = array_map($clone, $this->methods);
$this->properties = array_map($clone, $this->properties);
}
}
@@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* PHP literal value.
*/
class Literal
{
/**
* Creates a literal representing the creation of an object using the new operator.
* @param mixed[] $args
*/
public static function new(string $class, array $args = []): self
{
return new self('new ' . $class . '(...?:)', [$args]);
}
public function __construct(
private string $value,
/** @var ?mixed[] */
private ?array $args = null,
) {
}
public function __toString(): string
{
return $this->formatWith(new Dumper);
}
/** @internal */
public function formatWith(Dumper $dumper): string
{
return $this->args === null
? $this->value
: $dumper->format($this->value, ...$this->args);
}
}
@@ -0,0 +1,115 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use function func_num_args;
/**
* Definition of a class method.
*/
final class Method
{
use Traits\FunctionLike;
use Traits\NameAware;
use Traits\VisibilityAware;
use Traits\CommentAware;
use Traits\AttributeAware;
public const Constructor = '__construct';
private bool $static = false;
private bool $final = false;
private bool $abstract = false;
/**
* @param string|array{object|string, string}|\Closure $method
*/
public static function from(string|array|\Closure $method): static
{
return (new Factory)->fromMethodReflection(Nette\Utils\Callback::toReflection($method));
}
public function __toString(): string
{
return (new Printer)->printMethod($this);
}
public function setStatic(bool $state = true): static
{
$this->static = $state;
return $this;
}
public function isStatic(): bool
{
return $this->static;
}
public function setFinal(bool $state = true): static
{
$this->final = $state;
return $this;
}
public function isFinal(): bool
{
return $this->final;
}
public function setAbstract(bool $state = true): static
{
$this->abstract = $state;
return $this;
}
public function isAbstract(): bool
{
return $this->abstract;
}
/**
* @param string $name without $
*/
public function addPromotedParameter(string $name, mixed $defaultValue = null): PromotedParameter
{
$param = new PromotedParameter($name);
if (func_num_args() > 1) {
$param->setDefaultValue($defaultValue);
}
return $this->parameters[$name] = $param;
}
/** @throws Nette\InvalidStateException */
public function validate(): void
{
if ($this->abstract && ($this->final || $this->visibility === Visibility::Private)) {
throw new Nette\InvalidStateException("Method $this->name() cannot be abstract and final or private at the same time.");
}
}
public function __clone(): void
{
$this->parameters = array_map(fn($param) => clone $param, $this->parameters);
}
}
@@ -0,0 +1,96 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette\Utils\Type;
/**
* Definition of a function/method parameter.
*/
class Parameter
{
use Traits\NameAware;
use Traits\AttributeAware;
use Traits\CommentAware;
private bool $reference = false;
private ?string $type = null;
private bool $nullable = false;
private bool $hasDefaultValue = false;
private mixed $defaultValue = null;
public function setReference(bool $state = true): static
{
$this->reference = $state;
return $this;
}
public function isReference(): bool
{
return $this->reference;
}
public function setType(?string $type): static
{
$this->type = Helpers::validateType($type, $this->nullable);
return $this;
}
/** @return ($asObject is true ? ?Type : ?string) */
public function getType(bool $asObject = false): Type|string|null
{
return $asObject && $this->type
? Type::fromString($this->type)
: $this->type;
}
public function setNullable(bool $state = true): static
{
$this->nullable = $state;
return $this;
}
public function isNullable(): bool
{
return $this->nullable || ($this->hasDefaultValue && $this->defaultValue === null);
}
public function setDefaultValue(mixed $val): static
{
$this->defaultValue = $val;
$this->hasDefaultValue = true;
return $this;
}
public function getDefaultValue(): mixed
{
return $this->defaultValue;
}
public function hasDefaultValue(): bool
{
return $this->hasDefaultValue;
}
public function validate(): void
{
}
}
@@ -0,0 +1,193 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use function count;
/**
* Definition of a PHP file.
*
* Generates:
* - opening tag (<?php)
* - doc comments
* - one or more namespaces
*/
final class PhpFile
{
use Traits\CommentAware;
/** @var PhpNamespace[] */
private array $namespaces = [];
private bool $strictTypes = false;
public static function fromCode(string $code): self
{
return (new Factory)->fromCode($code);
}
/**
* Adds a class to the file. If it already exists, throws an exception.
* As a parameter, pass the full name with namespace.
*/
public function addClass(string $name): ClassType
{
return $this
->addNamespace(Helpers::extractNamespace($name))
->addClass(Helpers::extractShortName($name));
}
/**
* Adds an interface to the file. If it already exists, throws an exception.
* As a parameter, pass the full name with namespace.
*/
public function addInterface(string $name): InterfaceType
{
return $this
->addNamespace(Helpers::extractNamespace($name))
->addInterface(Helpers::extractShortName($name));
}
/**
* Adds a trait to the file. If it already exists, throws an exception.
* As a parameter, pass the full name with namespace.
*/
public function addTrait(string $name): TraitType
{
return $this
->addNamespace(Helpers::extractNamespace($name))
->addTrait(Helpers::extractShortName($name));
}
/**
* Adds an enum to the file. If it already exists, throws an exception.
* As a parameter, pass the full name with namespace.
*/
public function addEnum(string $name): EnumType
{
return $this
->addNamespace(Helpers::extractNamespace($name))
->addEnum(Helpers::extractShortName($name));
}
/**
* Adds a function to the file. If it already exists, throws an exception.
* As a parameter, pass the full name with namespace.
*/
public function addFunction(string $name): GlobalFunction
{
return $this
->addNamespace(Helpers::extractNamespace($name))
->addFunction(Helpers::extractShortName($name));
}
/**
* Adds a namespace to the file. If it already exists, it returns the existing one.
*/
public function addNamespace(string|PhpNamespace $namespace): PhpNamespace
{
$res = $namespace instanceof PhpNamespace
? ($this->namespaces[$namespace->getName()] = $namespace)
: ($this->namespaces[$namespace] ??= new PhpNamespace($namespace));
foreach ($this->namespaces as $namespace) {
$namespace->setBracketedSyntax(count($this->namespaces) > 1 && isset($this->namespaces['']));
}
return $res;
}
/**
* Removes the namespace from the file.
*/
public function removeNamespace(string|PhpNamespace $namespace): static
{
$name = $namespace instanceof PhpNamespace ? $namespace->getName() : $namespace;
unset($this->namespaces[$name]);
return $this;
}
/** @return PhpNamespace[] */
public function getNamespaces(): array
{
return $this->namespaces;
}
/** @return (ClassType|InterfaceType|TraitType|EnumType)[] */
public function getClasses(): array
{
$classes = [];
foreach ($this->namespaces as $n => $namespace) {
$n .= $n ? '\\' : '';
foreach ($namespace->getClasses() as $c => $class) {
$classes[$n . $c] = $class;
}
}
return $classes;
}
/** @return GlobalFunction[] */
public function getFunctions(): array
{
$functions = [];
foreach ($this->namespaces as $n => $namespace) {
$n .= $n ? '\\' : '';
foreach ($namespace->getFunctions() as $f => $function) {
$functions[$n . $f] = $function;
}
}
return $functions;
}
/**
* Adds a use statement to the file, to the global namespace.
*/
public function addUse(string $name, ?string $alias = null, string $of = PhpNamespace::NameNormal): static
{
$this->addNamespace('')->addUse($name, $alias, $of);
return $this;
}
/**
* Adds declare(strict_types=1) to output.
*/
public function setStrictTypes(bool $state = true): static
{
$this->strictTypes = $state;
return $this;
}
public function hasStrictTypes(): bool
{
return $this->strictTypes;
}
public function __toString(): string
{
return (new Printer)->printFile($this);
}
}
@@ -0,0 +1,16 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/** @deprecated use Nette\PhpGenerator\Literal */
class PhpLiteral extends Literal
{
}
@@ -0,0 +1,412 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use Nette\InvalidStateException;
use function strlen;
use const ARRAY_FILTER_USE_BOTH;
/**
* Definition of a PHP namespace.
*
* Generates:
* - namespace statement
* - variable amount of use statements
* - one or more class declarations
*/
final class PhpNamespace
{
public const
NameNormal = 'n',
NameFunction = 'f',
NameConstant = 'c';
#[\Deprecated('use PhpNamespace::NameNormal')]
public const NAME_NORMAL = self::NameNormal;
#[\Deprecated('use PhpNamespace::NameFunction')]
public const NAME_FUNCTION = self::NameFunction;
#[\Deprecated('use PhpNamespace::NameConstant')]
public const NAME_CONSTANT = self::NameConstant;
private string $name;
private bool $bracketedSyntax = false;
/** @var string[][] */
private array $aliases = [
self::NameNormal => [],
self::NameFunction => [],
self::NameConstant => [],
];
/** @var (ClassType|InterfaceType|TraitType|EnumType)[] */
private array $classes = [];
/** @var GlobalFunction[] */
private array $functions = [];
public function __construct(string $name)
{
if ($name !== '' && !Helpers::isNamespaceIdentifier($name)) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid name.");
}
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
/**
* @internal
*/
public function setBracketedSyntax(bool $state = true): static
{
$this->bracketedSyntax = $state;
return $this;
}
public function hasBracketedSyntax(): bool
{
return $this->bracketedSyntax;
}
/**
* Adds a use statement to the namespace for class, function or constant.
* @throws InvalidStateException
*/
public function addUse(string $name, ?string $alias = null, string $of = self::NameNormal): static
{
if (
!Helpers::isNamespaceIdentifier($name, allowLeadingSlash: true)
|| (Helpers::isIdentifier($name) && isset(Helpers::Keywords[strtolower($name)]))
) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid class/function/constant name.");
} elseif ($alias && (!Helpers::isIdentifier($alias) || isset(Helpers::Keywords[strtolower($alias)]))) {
throw new Nette\InvalidArgumentException("Value '$alias' is not valid alias.");
}
$name = ltrim($name, '\\');
$aliases = array_change_key_case($this->aliases[$of]);
$used = [self::NameNormal => $this->classes, self::NameFunction => $this->functions, self::NameConstant => []][$of];
if ($alias === null) {
$base = Helpers::extractShortName($name);
$counter = null;
do {
$alias = $base . $counter;
$lower = strtolower($alias);
$counter++;
} while ((isset($aliases[$lower]) && strcasecmp($aliases[$lower], $name) !== 0) || isset($used[$lower]));
} else {
$lower = strtolower($alias);
if (isset($aliases[$lower]) && strcasecmp($aliases[$lower], $name) !== 0) {
throw new InvalidStateException(
"Alias '$alias' used already for '{$aliases[$lower]}', cannot use for '$name'.",
);
} elseif (isset($used[$lower])) {
throw new Nette\InvalidStateException("Name '$alias' used already for '$this->name\\{$used[$lower]->getName()}'.");
}
}
$this->aliases[$of][$alias] = $name;
return $this;
}
public function removeUse(string $name, string $of = self::NameNormal): void
{
foreach ($this->aliases[$of] as $alias => $item) {
if (strcasecmp($item, $name) === 0) {
unset($this->aliases[$of][$alias]);
}
}
}
/**
* Adds a use statement to the namespace for function.
*/
public function addUseFunction(string $name, ?string $alias = null): static
{
return $this->addUse($name, $alias, self::NameFunction);
}
/**
* Adds a use statement to the namespace for constant.
*/
public function addUseConstant(string $name, ?string $alias = null): static
{
return $this->addUse($name, $alias, self::NameConstant);
}
/** @return string[] */
public function getUses(string $of = self::NameNormal): array
{
uasort($this->aliases[$of], fn(string $a, string $b): int => strtr($a, '\\', ' ') <=> strtr($b, '\\', ' '));
return array_filter(
$this->aliases[$of],
fn($name, $alias) => (bool) strcasecmp(($this->name ? $this->name . '\\' : '') . $alias, $name),
ARRAY_FILTER_USE_BOTH,
);
}
/**
* Resolves relative name to full name.
*/
public function resolveName(string $name, string $of = self::NameNormal): string
{
if (isset(Helpers::Keywords[strtolower($name)]) || $name === '') {
return $name;
} elseif ($name[0] === '\\') {
return substr($name, 1);
}
$aliases = array_change_key_case($this->aliases[$of]);
if ($of !== self::NameNormal) {
return $aliases[strtolower($name)]
?? $this->resolveName(Helpers::extractNamespace($name) . '\\') . Helpers::extractShortName($name);
}
$parts = explode('\\', $name, 2);
return ($res = $aliases[strtolower($parts[0])] ?? null)
? $res . (isset($parts[1]) ? '\\' . $parts[1] : '')
: $this->name . ($this->name ? '\\' : '') . $name;
}
/**
* Simplifies type hint with relative names.
*/
public function simplifyType(string $type, string $of = self::NameNormal): string
{
return preg_replace_callback('~[\w\x7f-\xff\\\]+~', fn($m) => $this->simplifyName($m[0], $of), $type);
}
/**
* Simplifies the full name of a class, function, or constant to a relative name.
*/
public function simplifyName(string $name, string $of = self::NameNormal): string
{
if (isset(Helpers::Keywords[strtolower($name)]) || $name === '') {
return $name;
}
$name = ltrim($name, '\\');
if ($of !== self::NameNormal) {
foreach ($this->aliases[$of] as $alias => $original) {
if (strcasecmp($original, $name) === 0) {
return $alias;
}
}
return $this->simplifyName(Helpers::extractNamespace($name) . '\\') . Helpers::extractShortName($name);
}
$shortest = null;
$relative = self::startsWith($name, $this->name . '\\')
? substr($name, strlen($this->name) + 1)
: null;
foreach ($this->aliases[$of] as $alias => $original) {
if ($relative && self::startsWith($relative . '\\', $alias . '\\')) {
$relative = null;
}
if (self::startsWith($name . '\\', $original . '\\')) {
$short = $alias . substr($name, strlen($original));
if (!isset($shortest) || strlen($shortest) > strlen($short)) {
$shortest = $short;
}
}
}
if (isset($shortest, $relative) && strlen($shortest) < strlen($relative)) {
return $shortest;
}
return $relative ?? $shortest ?? ($this->name ? '\\' : '') . $name;
}
/**
* Adds a class-like type to the namespace. If it already exists, throws an exception.
*/
public function add(ClassType|InterfaceType|TraitType|EnumType $class): static
{
$name = $class->getName();
if ($name === null) {
throw new Nette\InvalidArgumentException('Class does not have a name.');
}
$lower = strtolower($name);
if (isset($this->classes[$lower]) && $this->classes[$lower] !== $class) {
throw new Nette\InvalidStateException("Cannot add '$name', because it already exists.");
} elseif ($orig = array_change_key_case($this->aliases[self::NameNormal])[$lower] ?? null) {
throw new Nette\InvalidStateException("Name '$name' used already as alias for $orig.");
}
$this->classes[$lower] = $class;
return $this;
}
/**
* Adds a class to the namespace. If it already exists, throws an exception.
*/
public function addClass(string $name): ClassType
{
$this->add($class = new ClassType($name, $this));
return $class;
}
/**
* Adds an interface to the namespace. If it already exists, throws an exception.
*/
public function addInterface(string $name): InterfaceType
{
$this->add($iface = new InterfaceType($name, $this));
return $iface;
}
/**
* Adds a trait to the namespace. If it already exists, throws an exception.
*/
public function addTrait(string $name): TraitType
{
$this->add($trait = new TraitType($name, $this));
return $trait;
}
/**
* Adds an enum to the namespace. If it already exists, throws an exception.
*/
public function addEnum(string $name): EnumType
{
$this->add($enum = new EnumType($name, $this));
return $enum;
}
/**
* Returns a class-like type from the namespace.
*/
public function getClass(string $name): ClassType|InterfaceType|TraitType|EnumType
{
return $this->classes[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Class '$name' not found.");
}
/**
* Returns all class-like types in the namespace.
* @return (ClassType|InterfaceType|TraitType|EnumType)[]
*/
public function getClasses(): array
{
$res = [];
foreach ($this->classes as $class) {
$res[$class->getName()] = $class;
}
return $res;
}
/**
* Removes a class-like type from namespace.
*/
public function removeClass(string $name): static
{
unset($this->classes[strtolower($name)]);
return $this;
}
/**
* Adds a function to the namespace. If it already exists, throws an exception.
*/
public function addFunction(string $name): GlobalFunction
{
$lower = strtolower($name);
if (isset($this->functions[$lower])) {
throw new Nette\InvalidStateException("Cannot add '$name', because it already exists.");
} elseif ($orig = array_change_key_case($this->aliases[self::NameFunction])[$lower] ?? null) {
throw new Nette\InvalidStateException("Name '$name' used already as alias for $orig.");
}
return $this->functions[$lower] = new GlobalFunction($name);
}
/**
* Returns a function from the namespace.
*/
public function getFunction(string $name): GlobalFunction
{
return $this->functions[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Function '$name' not found.");
}
/**
* Returns all functions in the namespace.
* @return GlobalFunction[]
*/
public function getFunctions(): array
{
$res = [];
foreach ($this->functions as $fn) {
$res[$fn->getName()] = $fn;
}
return $res;
}
/**
* Removes a function type from namespace.
*/
public function removeFunction(string $name): static
{
unset($this->functions[strtolower($name)]);
return $this;
}
private static function startsWith(string $a, string $b): bool
{
return strncasecmp($a, $b, strlen($b)) === 0;
}
public function __toString(): string
{
return (new Printer)->printNamespace($this);
}
}
@@ -0,0 +1,543 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use Nette\Utils\Strings;
use function array_filter, array_map, count, end, get_debug_type, implode, is_scalar, ltrim, preg_replace, rtrim, str_contains, str_repeat, str_replace, strlen, substr;
/**
* Generates PHP code.
*/
class Printer
{
public int $wrapLength = 120;
public string $indentation = "\t";
public int $linesBetweenProperties = 0;
public int $linesBetweenMethods = 2;
public int $linesBetweenUseTypes = 0;
public string $returnTypeColon = ': ';
public bool $bracesOnNextLine = true;
public bool $singleParameterOnOneLine = false;
public bool $omitEmptyNamespaces = true;
protected ?PhpNamespace $namespace = null;
protected ?Dumper $dumper;
private bool $resolveTypes = true;
public function __construct()
{
$this->dumper = new Dumper;
}
public function printFunction(GlobalFunction $function, ?PhpNamespace $namespace = null): string
{
$this->namespace = $this->resolveTypes ? $namespace : null;
$line = 'function '
. ($function->getReturnReference() ? '&' : '')
. $function->getName();
$returnType = $this->printReturnType($function);
$params = $this->printParameters($function, strlen($line) + strlen($returnType) + 2); // 2 = parentheses
$body = $this->printFunctionBody($function);
$braceOnNextLine = $this->isBraceOnNextLine(str_contains($params, "\n"), (bool) $returnType);
return $this->printDocComment($function)
. $this->printAttributes($function->getAttributes())
. $line
. $params
. $returnType
. ($braceOnNextLine ? "\n" : ' ')
. "{\n" . $this->indent($body) . "}\n";
}
public function printClosure(Closure $closure, ?PhpNamespace $namespace = null): string
{
$this->namespace = $this->resolveTypes ? $namespace : null;
$uses = [];
foreach ($closure->getUses() as $param) {
$uses[] = ($param->isReference() ? '&' : '') . '$' . $param->getName();
}
$useStr = strlen($tmp = implode(', ', $uses)) > $this->wrapLength && count($uses) > 1
? "\n" . $this->indentation . implode(",\n" . $this->indentation, $uses) . ",\n"
: $tmp;
$body = $this->printFunctionBody($closure);
return $this->printAttributes($closure->getAttributes(), inline: true)
. 'function '
. ($closure->getReturnReference() ? '&' : '')
. $this->printParameters($closure)
. ($uses ? " use ($useStr)" : '')
. $this->printReturnType($closure)
. " {\n" . $this->indent($body) . '}';
}
public function printArrowFunction(Closure $closure, ?PhpNamespace $namespace = null): string
{
$this->namespace = $this->resolveTypes ? $namespace : null;
foreach ($closure->getUses() as $use) {
if ($use->isReference()) {
throw new Nette\InvalidArgumentException('Arrow function cannot bind variables by-reference.');
}
}
$body = $this->printFunctionBody($closure);
return $this->printAttributes($closure->getAttributes())
. 'fn'
. ($closure->getReturnReference() ? '&' : '')
. $this->printParameters($closure)
. $this->printReturnType($closure)
. ' => ' . rtrim($body, "\n") . ';';
}
public function printMethod(Method $method, ?PhpNamespace $namespace = null, bool $isInterface = false): string
{
$this->namespace = $this->resolveTypes ? $namespace : null;
$method->validate();
$line = ($method->isAbstract() && !$isInterface ? 'abstract ' : '')
. ($method->isFinal() ? 'final ' : '')
. ($method->getVisibility() ? $method->getVisibility() . ' ' : '')
. ($method->isStatic() ? 'static ' : '')
. 'function '
. ($method->getReturnReference() ? '&' : '')
. $method->getName();
$returnType = $this->printReturnType($method);
$params = $this->printParameters($method, strlen($line) + strlen($returnType) + strlen($this->indentation) + 2);
$body = $this->printFunctionBody($method);
$braceOnNextLine = $this->isBraceOnNextLine(str_contains($params, "\n"), (bool) $returnType);
return $this->printDocComment($method)
. $this->printAttributes($method->getAttributes())
. $line
. $params
. $returnType
. ($method->isAbstract() || $isInterface
? ";\n"
: ($braceOnNextLine ? "\n" : ' ') . "{\n" . $this->indent($body) . "}\n");
}
private function printFunctionBody(Closure|GlobalFunction|Method|PropertyHook $function): string
{
$code = Helpers::simplifyTaggedNames($function->getBody(), $this->namespace);
$code = Strings::normalize($code);
return ltrim(rtrim($code) . "\n");
}
public function printClass(
ClassType|InterfaceType|TraitType|EnumType $class,
?PhpNamespace $namespace = null,
): string
{
$this->namespace = $this->resolveTypes ? $namespace : null;
$class->validate();
$resolver = $this->namespace
? [$namespace, 'simplifyType']
: fn($s) => $s;
$traits = [];
if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof EnumType) {
foreach ($class->getTraits() as $trait) {
$resolutions = implode(";\n", $trait->getResolutions());
$resolutions = Helpers::simplifyTaggedNames($resolutions, $this->namespace);
$traits[] = $this->printDocComment($trait)
. 'use ' . $resolver($trait->getName())
. ($resolutions
? " {\n" . $this->indent($resolutions) . ";\n}\n"
: ";\n");
}
}
$cases = [];
$enumType = null;
if ($class instanceof EnumType) {
$enumType = $class->getType();
foreach ($class->getCases() as $case) {
$enumType ??= is_scalar($case->getValue()) ? get_debug_type($case->getValue()) : null;
$cases[] = $this->printDocComment($case)
. $this->printAttributes($case->getAttributes())
. 'case ' . $case->getName()
. ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue()))
. ";\n";
}
}
$readOnlyClass = $class instanceof ClassType && $class->isReadOnly();
$consts = [];
$methods = [];
if (
$class instanceof ClassType
|| $class instanceof InterfaceType
|| $class instanceof TraitType
|| $class instanceof EnumType
) {
foreach ($class->getConstants() as $const) {
$consts[] = $this->printConstant($const);
}
foreach ($class->getMethods() as $method) {
if ($readOnlyClass && $method->getName() === Method::Constructor) {
$method = clone $method;
array_map(fn($param) => $param instanceof PromotedParameter ? $param->setReadOnly(false) : null, $method->getParameters());
}
$methods[] = $this->printMethod($method, $namespace, $class->isInterface());
}
}
$properties = [];
if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof InterfaceType) {
foreach ($class->getProperties() as $property) {
$properties[] = $this->printProperty($property, $readOnlyClass, $class instanceof InterfaceType);
}
}
$members = array_filter([
implode('', $traits),
$this->joinProperties($consts),
$this->joinProperties($cases),
$this->joinProperties($properties),
($methods && $properties ? str_repeat("\n", $this->linesBetweenMethods - 1) : '')
. implode(str_repeat("\n", $this->linesBetweenMethods), $methods),
]);
if ($class instanceof ClassType) {
$line[] = $class->isAbstract() ? 'abstract' : null;
$line[] = $class->isFinal() ? 'final' : null;
$line[] = $class->isReadOnly() ? 'readonly' : null;
}
$line[] = match (true) {
$class instanceof ClassType => $class->getName() ? 'class ' . $class->getName() : null,
$class instanceof InterfaceType => 'interface ' . $class->getName(),
$class instanceof TraitType => 'trait ' . $class->getName(),
$class instanceof EnumType => 'enum ' . $class->getName() . ($enumType ? $this->returnTypeColon . $enumType : ''),
};
$line[] = ($class instanceof ClassType || $class instanceof InterfaceType) && $class->getExtends()
? 'extends ' . implode(', ', array_map($resolver, (array) $class->getExtends()))
: null;
$line[] = ($class instanceof ClassType || $class instanceof EnumType) && $class->getImplements()
? 'implements ' . implode(', ', array_map($resolver, $class->getImplements()))
: null;
$line[] = $class->getName() ? null : '{';
return $this->printDocComment($class)
. $this->printAttributes($class->getAttributes())
. implode(' ', array_filter($line))
. ($class->getName() ? "\n{\n" : "\n")
. ($members ? $this->indent(implode("\n", $members)) : '')
. '}'
. ($class->getName() ? "\n" : '');
}
public function printNamespace(PhpNamespace $namespace): string
{
$this->namespace = $this->resolveTypes ? $namespace : null;
$name = $namespace->getName();
$uses = [
$this->printUses($namespace),
$this->printUses($namespace, PhpNamespace::NameFunction),
$this->printUses($namespace, PhpNamespace::NameConstant),
];
$uses = implode(str_repeat("\n", $this->linesBetweenUseTypes), array_filter($uses));
$items = [];
foreach ($namespace->getClasses() as $class) {
$items[] = $this->printClass($class, $namespace);
}
foreach ($namespace->getFunctions() as $function) {
$items[] = $this->printFunction($function, $namespace);
}
$body = ($uses ? $uses . "\n" : '')
. implode("\n", $items);
if ($namespace->hasBracketedSyntax()) {
return 'namespace' . ($name ? " $name" : '') . "\n{\n"
. $this->indent($body)
. "}\n";
} else {
return ($name ? "namespace $name;\n\n" : '')
. $body;
}
}
public function printFile(PhpFile $file): string
{
$namespaces = [];
foreach ($file->getNamespaces() as $namespace) {
if (!$this->omitEmptyNamespaces || $namespace->getClasses() || $namespace->getFunctions()) {
$namespaces[] = $this->printNamespace($namespace);
}
}
return "<?php\n"
. ($file->getComment() ? "\n" . $this->printDocComment($file) : '')
. "\n"
. ($file->hasStrictTypes() ? "declare(strict_types=1);\n\n" : '')
. implode("\n\n", $namespaces);
}
protected function printUses(PhpNamespace $namespace, string $of = PhpNamespace::NameNormal): string
{
$prefix = [
PhpNamespace::NameNormal => '',
PhpNamespace::NameFunction => 'function ',
PhpNamespace::NameConstant => 'const ',
][$of];
$uses = [];
foreach ($namespace->getUses($of) as $alias => $original) {
$uses[] = Helpers::extractShortName($original) === $alias
? "use $prefix$original;\n"
: "use $prefix$original as $alias;\n";
}
return implode('', $uses);
}
protected function printParameters(Closure|GlobalFunction|Method|PropertyHook $function, int $column = 0): string
{
$special = false;
foreach ($function->getParameters() as $param) {
$param->validate();
$special = $special || $param instanceof PromotedParameter || $param->getAttributes() || $param->getComment();
}
if (!$special || ($this->singleParameterOnOneLine && count($function->getParameters()) === 1)) {
$line = $this->formatParameters($function, multiline: false);
if (!str_contains($line, "\n") && strlen($line) + $column <= $this->wrapLength) {
return $line;
}
}
return $this->formatParameters($function, multiline: true);
}
private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $function, bool $multiline): string
{
$params = $function->getParameters();
$res = '';
foreach ($params as $param) {
$variadic = !$function instanceof PropertyHook && $function->isVariadic() && $param === end($params);
$attrs = $this->printAttributes($param->getAttributes(), inline: true);
$res .=
$this->printDocComment($param)
. ($attrs ? ($multiline ? substr($attrs, 0, -1) . "\n" : $attrs) : '')
. ($param instanceof PromotedParameter
? ($param->isFinal() ? 'final ' : '')
. $this->printPropertyVisibility($param)
. ($param->isReadOnly() && $param->getType() ? ' readonly' : '')
. ' '
: '')
. ltrim($this->printType($param->getType(), $param->isNullable()) . ' ')
. ($param->isReference() ? '&' : '')
. ($variadic ? '...' : '')
. '$' . $param->getName()
. ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '')
. ($param instanceof PromotedParameter ? $this->printHooks($param) : '')
. ($multiline ? ",\n" : ', ');
}
return $multiline
? "(\n" . $this->indent($res) . ')'
: '(' . substr($res, 0, -2) . ')';
}
private function printConstant(Constant $const): string
{
$def = ($const->isFinal() ? 'final ' : '')
. ($const->getVisibility() ? $const->getVisibility() . ' ' : '')
. 'const '
. ltrim($this->printType($const->getType(), nullable: false) . ' ')
. $const->getName() . ' = ';
return $this->printDocComment($const)
. $this->printAttributes($const->getAttributes())
. $def
. $this->dump($const->getValue(), strlen($def)) . ";\n";
}
private function printProperty(Property $property, bool $readOnlyClass = false, bool $isInterface = false): string
{
$property->validate();
$type = $property->getType();
$def = ($property->isAbstract() && !$isInterface ? 'abstract ' : '')
. ($property->isFinal() ? 'final ' : '')
. $this->printPropertyVisibility($property)
. ($property->isStatic() ? ' static' : '')
. (!$readOnlyClass && $property->isReadOnly() && $type ? ' readonly' : '')
. ' '
. ltrim($this->printType($type, $property->isNullable()) . ' ')
. '$' . $property->getName();
$defaultValue = $property->getValue() === null && !$property->isInitialized()
? ''
: ' = ' . $this->dump($property->getValue(), strlen($def) + 3); // 3 = ' = '
return $this->printDocComment($property)
. $this->printAttributes($property->getAttributes())
. $def
. $defaultValue
. ($this->printHooks($property, $isInterface) ?: ';')
. "\n";
}
private function printPropertyVisibility(Property|PromotedParameter $param): string
{
$get = $param->getVisibility(PropertyAccessMode::Get);
$set = $param->getVisibility(PropertyAccessMode::Set);
return $set
? ($get ? "$get $set(set)" : "$set(set)")
: $get ?? 'public';
}
protected function printType(?string $type, bool $nullable): string
{
if ($type === null) {
return '';
}
if ($this->namespace) {
$type = $this->namespace->simplifyType($type);
}
return $nullable
? Type::nullable($type)
: $type;
}
protected function printDocComment(/*Traits\CommentAware*/ $commentable): string
{
$multiLine = $commentable instanceof GlobalFunction
|| $commentable instanceof Method
|| $commentable instanceof ClassLike
|| $commentable instanceof PhpFile;
return Helpers::formatDocComment((string) $commentable->getComment(), $multiLine);
}
protected function printReturnType(Closure|GlobalFunction|Method $function): string
{
return ($tmp = $this->printType($function->getReturnType(), $function->isReturnNullable()))
? $this->returnTypeColon . $tmp
: '';
}
/** @param Attribute[] $attrs */
protected function printAttributes(array $attrs, bool $inline = false): string
{
if (!$attrs) {
return '';
}
$this->dumper->indentation = $this->indentation;
$items = [];
foreach ($attrs as $attr) {
$args = $this->dumper->format('...?:', $attr->getArguments());
$args = Helpers::simplifyTaggedNames($args, $this->namespace);
$items[] = $this->printType($attr->getName(), nullable: false) . ($args === '' ? '' : "($args)");
$inline = $inline && !str_contains($args, "\n");
}
return $inline
? '#[' . implode(', ', $items) . '] '
: '#[' . implode("]\n#[", $items) . "]\n";
}
private function printHooks(Property|PromotedParameter $property, bool $isInterface = false): string
{
$hooks = $property->getHooks();
if (!$hooks) {
return '';
}
$simple = true;
foreach ($hooks as $type => $hook) {
$simple = $simple && ($hook->isAbstract() || $isInterface);
$hooks[$type] = $this->printDocComment($hook)
. $this->printAttributes($hook->getAttributes())
. ($hook->isAbstract() || $isInterface
? ($hook->getReturnReference() ? '&' : '')
. $type . ';'
: ($hook->isFinal() ? 'final ' : '')
. ($hook->getReturnReference() ? '&' : '')
. $type
. ($hook->getParameters() ? $this->printParameters($hook) : '')
. ' '
. ($hook->isShort()
? '=> ' . $hook->getBody() . ';'
: "{\n" . $this->indent($this->printFunctionBody($hook)) . '}'));
}
return $simple
? ' { ' . implode(' ', $hooks) . ' }'
: " {\n" . $this->indent(implode("\n", $hooks)) . "\n}";
}
public function setTypeResolving(bool $state = true): static
{
$this->resolveTypes = $state;
return $this;
}
protected function indent(string $s): string
{
$s = str_replace("\t", $this->indentation, $s);
return Strings::indent($s, 1, $this->indentation);
}
protected function dump(mixed $var, int $column = 0): string
{
$this->dumper->indentation = $this->indentation;
$this->dumper->wrapLength = $this->wrapLength;
$s = $this->dumper->dump($var, $column);
$s = Helpers::simplifyTaggedNames($s, $this->namespace);
return $s;
}
/** @param string[] $props */
private function joinProperties(array $props): string
{
return $this->linesBetweenProperties
? implode(str_repeat("\n", $this->linesBetweenProperties), $props)
: preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $props));
}
protected function isBraceOnNextLine(bool $multiLine, bool $hasReturnType): bool
{
return $this->bracesOnNextLine && (!$multiLine || $hasReturnType);
}
}
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of a promoted constructor parameter.
*/
final class PromotedParameter extends Parameter
{
use Traits\PropertyLike;
/** @throws Nette\InvalidStateException */
public function validate(): void
{
if ($this->readOnly && !$this->getType()) {
throw new Nette\InvalidStateException("Property \${$this->getName()}: Read-only properties are only supported on typed property.");
}
}
public function __clone(): void
{
$this->hooks = array_map(fn($item) => $item ? clone $item : $item, $this->hooks);
}
}
@@ -0,0 +1,138 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use Nette\Utils\Type;
/**
* Definition of a class property.
*/
final class Property
{
use Traits\NameAware;
use Traits\PropertyLike;
use Traits\CommentAware;
use Traits\AttributeAware;
private mixed $value = null;
private bool $static = false;
private ?string $type = null;
private bool $nullable = false;
private bool $initialized = false;
private bool $abstract = false;
public function setValue(mixed $val): static
{
$this->value = $val;
$this->initialized = true;
return $this;
}
public function &getValue(): mixed
{
return $this->value;
}
public function setStatic(bool $state = true): static
{
$this->static = $state;
return $this;
}
public function isStatic(): bool
{
return $this->static;
}
public function setType(?string $type): static
{
$this->type = Helpers::validateType($type, $this->nullable);
return $this;
}
/** @return ($asObject is true ? ?Type : ?string) */
public function getType(bool $asObject = false): Type|string|null
{
return $asObject && $this->type
? Type::fromString($this->type)
: $this->type;
}
public function setNullable(bool $state = true): static
{
$this->nullable = $state;
return $this;
}
public function isNullable(): bool
{
return $this->nullable || ($this->initialized && $this->value === null);
}
public function setInitialized(bool $state = true): static
{
$this->initialized = $state;
return $this;
}
public function isInitialized(): bool
{
return $this->initialized || $this->value !== null;
}
public function setAbstract(bool $state = true): static
{
$this->abstract = $state;
return $this;
}
public function isAbstract(): bool
{
return $this->abstract;
}
/** @throws Nette\InvalidStateException */
public function validate(): void
{
if ($this->readOnly && !$this->type) {
throw new Nette\InvalidStateException("Property \$$this->name: Read-only properties are only supported on typed property.");
} elseif ($this->abstract && $this->final) {
throw new Nette\InvalidStateException("Property \$$this->name cannot be abstract and final at the same time.");
} elseif (
$this->abstract
&& !Nette\Utils\Arrays::some($this->getHooks(), fn($hook) => $hook->isAbstract())
) {
throw new Nette\InvalidStateException("Property \$$this->name: Abstract property must have at least one abstract hook.");
}
}
public function __clone(): void
{
$this->hooks = array_map(fn($item) => $item ? clone $item : $item, $this->hooks);
}
}
@@ -0,0 +1,20 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Property access mode.
*/
enum PropertyAccessMode: string
{
case Set = 'set';
case Get = 'get';
}
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Nette\PhpGenerator;
use JetBrains\PhpStorm\Language;
/**
* Definition of a property hook.
*/
final class PropertyHook
{
use Traits\AttributeAware;
use Traits\CommentAware;
private string $body = '';
private bool $short = false;
private bool $final = false;
private bool $abstract = false;
/** @var Parameter[] */
private array $parameters = [];
private bool $returnReference = false;
/** @param ?mixed[] $args */
public function setBody(
#[Language('PHP')]
string $code,
?array $args = null,
bool $short = false,
): static
{
$this->body = $args === null
? $code
: (new Dumper)->format($code, ...$args);
$this->short = $short;
return $this;
}
public function getBody(): string
{
return $this->body;
}
public function isShort(): bool
{
return $this->short && trim($this->body) !== '';
}
public function setFinal(bool $state = true): static
{
$this->final = $state;
return $this;
}
public function isFinal(): bool
{
return $this->final;
}
public function setAbstract(bool $state = true): static
{
$this->abstract = $state;
return $this;
}
public function isAbstract(): bool
{
return $this->abstract;
}
/**
* @param Parameter[] $val
* @internal
*/
public function setParameters(array $val): static
{
(function (Parameter ...$val) {})(...$val);
$this->parameters = [];
foreach ($val as $v) {
$this->parameters[$v->getName()] = $v;
}
return $this;
}
/**
* @return Parameter[]
* @internal
*/
public function getParameters(): array
{
return $this->parameters;
}
/**
* Adds a parameter. If it already exists, it overwrites it.
* @param string $name without $
*/
public function addParameter(string $name): Parameter
{
return $this->parameters[$name] = new Parameter($name);
}
public function setReturnReference(bool $state = true): static
{
$this->returnReference = $state;
return $this;
}
public function getReturnReference(): bool
{
return $this->returnReference;
}
}
@@ -0,0 +1,20 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Property hook type.
*/
enum PropertyHookType: string
{
case Set = 'set';
case Get = 'get';
}
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Generates PHP code compatible with PSR-2 and PSR-12.
*/
class PsrPrinter extends Printer
{
public string $indentation = ' ';
public int $linesBetweenMethods = 1;
public int $linesBetweenUseTypes = 1;
protected function isBraceOnNextLine(bool $multiLine, bool $hasReturnType): bool
{
return !$multiLine;
}
}
@@ -0,0 +1,54 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of a trait with properties, methods, constants and traits.
*/
final class TraitType extends ClassLike
{
use Traits\ConstantsAware;
use Traits\MethodsAware;
use Traits\PropertiesAware;
use Traits\TraitsAware;
/**
* Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true.
*/
public function addMember(Method|Property|Constant|TraitUse $member, bool $overwrite = false): static
{
$name = $member->getName();
[$type, $n] = match (true) {
$member instanceof Constant => ['consts', $name],
$member instanceof Method => ['methods', strtolower($name)],
$member instanceof Property => ['properties', $name],
$member instanceof TraitUse => ['traits', $name],
};
if (!$overwrite && isset($this->$type[$n])) {
throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists.");
}
$this->$type[$n] = $member;
return $this;
}
public function __clone(): void
{
parent::__clone();
$clone = fn($item) => clone $item;
$this->consts = array_map($clone, $this->consts);
$this->methods = array_map($clone, $this->methods);
$this->properties = array_map($clone, $this->properties);
$this->traits = array_map($clone, $this->traits);
}
}
@@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
/**
* Definition of a trait use statement.
*/
final class TraitUse
{
use Traits\NameAware;
use Traits\CommentAware;
/** @var string[] */
private array $resolutions = [];
public function __construct(string $name)
{
if (!Nette\PhpGenerator\Helpers::isNamespaceIdentifier($name, allowLeadingSlash: true)) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid trait name.");
}
$this->name = $name;
}
public function addResolution(string $resolution): static
{
$this->resolutions[] = $resolution;
return $this;
}
/** @return string[] */
public function getResolutions(): array
{
return $this->resolutions;
}
}
@@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette\PhpGenerator\Attribute;
/**
* @internal
*/
trait AttributeAware
{
/** @var Attribute[] */
private array $attributes = [];
/** @param mixed[] $args */
public function addAttribute(string $name, array $args = []): static
{
$this->attributes[] = new Attribute($name, $args);
return $this;
}
/**
* Replaces all attributes.
* @param Attribute[] $attrs
*/
public function setAttributes(array $attrs): static
{
(function (Attribute ...$attrs) {})(...$attrs);
$this->attributes = $attrs;
return $this;
}
/** @return Attribute[] */
public function getAttributes(): array
{
return $this->attributes;
}
}
@@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
/**
* @internal
*/
trait CommentAware
{
private ?string $comment = null;
public function setComment(?string $val): static
{
$this->comment = $val;
return $this;
}
public function getComment(): ?string
{
return $this->comment;
}
/**
* Adds a new line to the comment.
*/
public function addComment(string $val): static
{
$this->comment .= $this->comment ? "\n$val" : $val;
return $this;
}
public function removeComment(): static
{
$this->comment = null;
return $this;
}
}
@@ -0,0 +1,79 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette;
use Nette\PhpGenerator\Constant;
/**
* @internal
*/
trait ConstantsAware
{
/** @var array<string, Constant> */
private array $consts = [];
/**
* Replaces all constants.
* @param Constant[] $consts
*/
public function setConstants(array $consts): static
{
(function (Constant ...$consts) {})(...$consts);
$this->consts = [];
foreach ($consts as $const) {
$this->consts[$const->getName()] = $const;
}
return $this;
}
/** @return Constant[] */
public function getConstants(): array
{
return $this->consts;
}
public function getConstant(string $name): Constant
{
return $this->consts[$name] ?? throw new Nette\InvalidArgumentException("Constant '$name' not found.");
}
/**
* Adds a constant. If it already exists, throws an exception or overwrites it if $overwrite is true.
*/
public function addConstant(string $name, mixed $value, bool $overwrite = false): Constant
{
if (!$overwrite && isset($this->consts[$name])) {
throw new Nette\InvalidStateException("Cannot add constant '$name', because it already exists.");
}
return $this->consts[$name] = (new Constant($name))
->setValue($value)
->setPublic();
}
public function removeConstant(string $name): static
{
unset($this->consts[$name]);
return $this;
}
public function hasConstant(string $name): bool
{
return isset($this->consts[$name]);
}
}
@@ -0,0 +1,179 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use JetBrains\PhpStorm\Language;
use Nette;
use Nette\PhpGenerator\Dumper;
use Nette\PhpGenerator\Parameter;
use Nette\Utils\Type;
use function func_num_args;
/**
* @internal
*/
trait FunctionLike
{
private string $body = '';
/** @var Parameter[] */
private array $parameters = [];
private bool $variadic = false;
private ?string $returnType = null;
private bool $returnReference = false;
private bool $returnNullable = false;
/** @param ?mixed[] $args */
public function setBody(
#[Language('PHP')]
string $code,
?array $args = null,
): static
{
$this->body = $args === null
? $code
: (new Dumper)->format($code, ...$args);
return $this;
}
public function getBody(): string
{
return $this->body;
}
/** @param ?mixed[] $args */
public function addBody(
#[Language('PHP')]
string $code,
?array $args = null,
): static
{
$this->body .= ($args === null ? $code : (new Dumper)->format($code, ...$args)) . "\n";
return $this;
}
/**
* @param Parameter[] $val
*/
public function setParameters(array $val): static
{
(function (Parameter ...$val) {})(...$val);
$this->parameters = [];
foreach ($val as $v) {
$this->parameters[$v->getName()] = $v;
}
return $this;
}
/** @return Parameter[] */
public function getParameters(): array
{
return $this->parameters;
}
public function getParameter(string $name): Parameter
{
return $this->parameters[$name] ?? throw new Nette\InvalidArgumentException("Parameter '$name' not found.");
}
/**
* Adds a parameter. If it already exists, it overwrites it.
* @param string $name without $
*/
public function addParameter(string $name, mixed $defaultValue = null): Parameter
{
$param = new Parameter($name);
if (func_num_args() > 1) {
$param->setDefaultValue($defaultValue);
}
return $this->parameters[$name] = $param;
}
/**
* @param string $name without $
*/
public function removeParameter(string $name): static
{
unset($this->parameters[$name]);
return $this;
}
public function hasParameter(string $name): bool
{
return isset($this->parameters[$name]);
}
public function setVariadic(bool $state = true): static
{
$this->variadic = $state;
return $this;
}
public function isVariadic(): bool
{
return $this->variadic;
}
public function setReturnType(?string $type): static
{
$this->returnType = Nette\PhpGenerator\Helpers::validateType($type, $this->returnNullable);
return $this;
}
/** @return ($asObject is true ? ?Type : ?string) */
public function getReturnType(bool $asObject = false): Type|string|null
{
return $asObject && $this->returnType
? Type::fromString($this->returnType)
: $this->returnType;
}
public function setReturnReference(bool $state = true): static
{
$this->returnReference = $state;
return $this;
}
public function getReturnReference(): bool
{
return $this->returnReference;
}
public function setReturnNullable(bool $state = true): static
{
$this->returnNullable = $state;
return $this;
}
public function isReturnNullable(): bool
{
return $this->returnNullable;
}
}
@@ -0,0 +1,89 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette;
use Nette\PhpGenerator\Method;
use function strtolower;
/**
* @internal
*/
trait MethodsAware
{
/** @var array<string, Method> */
private array $methods = [];
/**
* Replaces all methods.
* @param Method[] $methods
*/
public function setMethods(array $methods): static
{
(function (Method ...$methods) {})(...$methods);
$this->methods = [];
foreach ($methods as $m) {
$this->methods[strtolower($m->getName())] = $m;
}
return $this;
}
/** @return Method[] */
public function getMethods(): array
{
$res = [];
foreach ($this->methods as $m) {
$res[$m->getName()] = $m;
}
return $res;
}
public function getMethod(string $name): Method
{
return $this->methods[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Method '$name' not found.");
}
/**
* Adds a method. If it already exists, throws an exception or overwrites it if $overwrite is true.
*/
public function addMethod(string $name, bool $overwrite = false): Method
{
$lower = strtolower($name);
if (!$overwrite && isset($this->methods[$lower])) {
throw new Nette\InvalidStateException("Cannot add method '$name', because it already exists.");
}
$method = new Method($name);
if (!$this->isInterface()) {
$method->setPublic();
}
return $this->methods[$lower] = $method;
}
public function removeMethod(string $name): static
{
unset($this->methods[strtolower($name)]);
return $this;
}
public function hasMethod(string $name): bool
{
return isset($this->methods[strtolower($name)]);
}
}
@@ -0,0 +1,48 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette;
/**
* @internal
*/
trait NameAware
{
private string $name;
public function __construct(string $name)
{
if (!Nette\PhpGenerator\Helpers::isIdentifier($name)) {
throw new Nette\InvalidArgumentException("Value '$name' is not valid name.");
}
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
/**
* Returns clone with a different name.
*/
public function cloneWithName(string $name): static
{
$dolly = clone $this;
$dolly->__construct($name);
return $dolly;
}
}
@@ -0,0 +1,82 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette;
use Nette\PhpGenerator\Property;
use function func_num_args;
/**
* @internal
*/
trait PropertiesAware
{
/** @var array<string, Property> */
private array $properties = [];
/**
* Replaces all properties.
* @param Property[] $props
*/
public function setProperties(array $props): static
{
(function (Property ...$props) {})(...$props);
$this->properties = [];
foreach ($props as $v) {
$this->properties[$v->getName()] = $v;
}
return $this;
}
/** @return Property[] */
public function getProperties(): array
{
return $this->properties;
}
public function getProperty(string $name): Property
{
return $this->properties[$name] ?? throw new Nette\InvalidArgumentException("Property '$name' not found.");
}
/**
* Adds a property. If it already exists, throws an exception or overwrites it if $overwrite is true.
* @param string $name without $
*/
public function addProperty(string $name, mixed $value = null, bool $overwrite = false): Property
{
if (!$overwrite && isset($this->properties[$name])) {
throw new Nette\InvalidStateException("Cannot add property '$name', because it already exists.");
}
return $this->properties[$name] = func_num_args() > 1
? (new Property($name))->setValue($value)
: new Property($name);
}
/** @param string $name without $ */
public function removeProperty(string $name): static
{
unset($this->properties[$name]);
return $this;
}
public function hasProperty(string $name): bool
{
return isset($this->properties[$name]);
}
}
@@ -0,0 +1,160 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette\PhpGenerator\PropertyAccessMode;
use Nette\PhpGenerator\PropertyHook;
use Nette\PhpGenerator\PropertyHookType;
use Nette\PhpGenerator\Visibility;
use function array_filter, in_array, is_string;
/**
* @internal
*/
trait PropertyLike
{
/** @var array{set: ?Visibility, get: ?Visibility} */
private array $visibility = ['set' => null, 'get' => null];
private bool $final = false;
private bool $readOnly = false;
/** @var array<string, ?PropertyHook> */
private array $hooks = ['set' => null, 'get' => null];
public function setVisibility(Visibility|string|null $get, Visibility|string|null $set = null): static
{
$this->visibility = [
'set' => $set instanceof Visibility || $set === null ? $set : Visibility::from($set),
'get' => $get instanceof Visibility || $get === null ? $get : Visibility::from($get),
];
return $this;
}
public function getVisibility(PropertyAccessMode|string $mode = PropertyAccessMode::Get): ?string
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
return $this->visibility[$mode->value]?->value;
}
public function setPublic(PropertyAccessMode|string $mode = PropertyAccessMode::Get): static
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
$this->visibility[$mode->value] = Visibility::Public;
return $this;
}
public function isPublic(PropertyAccessMode|string $mode = PropertyAccessMode::Get): bool
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
return in_array($this->visibility[$mode->value], [Visibility::Public, null], true);
}
public function setProtected(PropertyAccessMode|string $mode = PropertyAccessMode::Get): static
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
$this->visibility[$mode->value] = Visibility::Protected;
return $this;
}
public function isProtected(PropertyAccessMode|string $mode = PropertyAccessMode::Get): bool
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
return $this->visibility[$mode->value] === Visibility::Protected;
}
public function setPrivate(PropertyAccessMode|string $mode = PropertyAccessMode::Get): static
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
$this->visibility[$mode->value] = Visibility::Private;
return $this;
}
public function isPrivate(PropertyAccessMode|string $mode = PropertyAccessMode::Get): bool
{
$mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode;
return $this->visibility[$mode->value] === Visibility::Private;
}
public function setFinal(bool $state = true): static
{
$this->final = $state;
return $this;
}
public function isFinal(): bool
{
return $this->final;
}
public function setReadOnly(bool $state = true): static
{
$this->readOnly = $state;
return $this;
}
public function isReadOnly(): bool
{
return $this->readOnly;
}
/**
* Replaces all hooks.
* @param PropertyHook[] $hooks
*/
public function setHooks(array $hooks): static
{
(function (PropertyHook ...$hooks) {})(...$hooks);
$this->hooks = $hooks;
return $this;
}
/** @return array<string, PropertyHook> */
public function getHooks(): array
{
return array_filter($this->hooks);
}
public function addHook(PropertyHookType|string $type, string $shortBody = ''): PropertyHook
{
$type = is_string($type) ? PropertyHookType::from($type) : $type;
return $this->hooks[$type->value] = (new PropertyHook)
->setBody($shortBody, short: true);
}
public function getHook(PropertyHookType|string $type): ?PropertyHook
{
$type = is_string($type) ? PropertyHookType::from($type) : $type;
return $this->hooks[$type->value] ?? null;
}
public function hasHook(PropertyHookType|string $type): bool
{
$type = is_string($type) ? PropertyHookType::from($type) : $type;
return isset($this->hooks[$type->value]);
}
}
@@ -0,0 +1,78 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette;
use Nette\PhpGenerator\TraitUse;
use function array_map, func_get_arg, func_num_args, is_array;
/**
* @internal
*/
trait TraitsAware
{
/** @var array<string, TraitUse> */
private array $traits = [];
/**
* Replaces all traits.
* @param TraitUse[] $traits
*/
public function setTraits(array $traits): static
{
(function (TraitUse ...$traits) {})(...$traits);
$this->traits = [];
foreach ($traits as $trait) {
$this->traits[$trait->getName()] = $trait;
}
return $this;
}
/** @return TraitUse[] */
public function getTraits(): array
{
return $this->traits;
}
/**
* Adds a method. If it already exists, throws an exception.
*/
public function addTrait(string $name): TraitUse
{
if (isset($this->traits[$name])) {
throw new Nette\InvalidStateException("Cannot add trait '$name', because it already exists.");
}
$this->traits[$name] = $trait = new TraitUse($name);
if (func_num_args() > 1 && is_array(func_get_arg(1))) { // back compatibility
trigger_error('Passing second argument to ' . __METHOD__ . '() is deprecated, use addResolution() instead.');
array_map(fn($item) => $trait->addResolution($item), func_get_arg(1));
}
return $trait;
}
public function removeTrait(string $name): static
{
unset($this->traits[$name]);
return $this;
}
public function hasTrait(string $name): bool
{
return isset($this->traits[$name]);
}
}
@@ -0,0 +1,75 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator\Traits;
use Nette\PhpGenerator\Visibility;
/**
* @internal
*/
trait VisibilityAware
{
private ?Visibility $visibility = null;
public function setVisibility(Visibility|string|null $value): static
{
$this->visibility = $value instanceof Visibility || $value === null
? $value
: Visibility::from($value);
return $this;
}
public function getVisibility(): ?string
{
return $this->visibility?->value;
}
public function setPublic(): static
{
$this->visibility = Visibility::Public;
return $this;
}
public function isPublic(): bool
{
return $this->visibility === Visibility::Public || $this->visibility === null;
}
public function setProtected(): static
{
$this->visibility = Visibility::Protected;
return $this;
}
public function isProtected(): bool
{
return $this->visibility === Visibility::Protected;
}
public function setPrivate(): static
{
$this->visibility = Visibility::Private;
return $this;
}
public function isPrivate(): bool
{
return $this->visibility === Visibility::Private;
}
}
@@ -0,0 +1,123 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
use Nette;
use function implode, preg_match, preg_replace, str_contains;
/**
* PHP return, property and parameter types.
*/
class Type
{
public const
String = 'string',
Int = 'int',
Float = 'float',
Bool = 'bool',
Array = 'array',
Object = 'object',
Callable = 'callable',
Iterable = 'iterable',
Void = 'void',
Never = 'never',
Mixed = 'mixed',
True = 'true',
False = 'false',
Null = 'null',
Self = 'self',
Parent = 'parent',
Static = 'static';
#[\Deprecated('use Type::String')]
public const STRING = self::String;
#[\Deprecated('use Type::Int')]
public const INT = self::Int;
#[\Deprecated('use Type::Float')]
public const FLOAT = self::Float;
#[\Deprecated('use Type::Bool')]
public const BOOL = self::Bool;
#[\Deprecated('use Type::Array')]
public const ARRAY = self::Array;
#[\Deprecated('use Type::Object')]
public const OBJECT = self::Object;
#[\Deprecated('use Type::Callable')]
public const CALLABLE = self::Callable;
#[\Deprecated('use Type::Iterable')]
public const ITERABLE = self::Iterable;
#[\Deprecated('use Type::Void')]
public const VOID = self::Void;
#[\Deprecated('use Type::Never')]
public const NEVER = self::Never;
#[\Deprecated('use Type::Mixed')]
public const MIXED = self::Mixed;
#[\Deprecated('use Type::False')]
public const FALSE = self::False;
#[\Deprecated('use Type::Null')]
public const NULL = self::Null;
#[\Deprecated('use Type::Self')]
public const SELF = self::Self;
#[\Deprecated('use Type::Parent')]
public const PARENT = self::Parent;
#[\Deprecated('use Type::Static')]
public const STATIC = self::Static;
public static function nullable(string $type, bool $nullable = true): string
{
if (str_contains($type, '&')) {
return $nullable
? throw new Nette\InvalidArgumentException('Intersection types cannot be nullable.')
: $type;
}
$nnType = preg_replace('#^\?|^null\||\|null(?=\||$)#i', '', $type);
$always = (bool) preg_match('#^(null|mixed)$#i', $nnType);
if ($nullable) {
return match (true) {
$always, $type !== $nnType => $type,
str_contains($type, '|') => $type . '|null',
default => '?' . $type,
};
} else {
return $always
? throw new Nette\InvalidArgumentException("Type $type cannot be not nullable.")
: $nnType;
}
}
public static function union(string ...$types): string
{
return implode('|', $types);
}
public static function intersection(string ...$types): string
{
return implode('&', $types);
}
}
@@ -0,0 +1,21 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\PhpGenerator;
/**
* Member visibility.
*/
enum Visibility: string
{
case Public = 'public';
case Protected = 'protected';
case Private = 'private';
}