Generator kodu PHP

Szukasz narzędzia do generowania kodu PHP klas, funkcji lub kompletnych plików?
  • Obsługuje wszystkie najnowsze funkcje PHP (takie jak property hooks, enumy, atrybuty itp.)
  • Umożliwia łatwą modyfikację istniejących klas
  • Kod wyjściowy jest zgodny ze stylem kodowania PSR-12 / PER
  • Dojrzała, stabilna i szeroko stosowana biblioteka

Instalacja

Bibliotekę pobierzesz i zainstalujesz za pomocą narzędzia Composer:

composer require nette/php-generator

Kompatybilność z PHP znajdziesz w tabeli.

Klasy

Zacznijmy od razu od przykładu tworzenia klasy za pomocą ClassType:

$class = new Nette\PhpGenerator\ClassType('Demo');

$class
	->setFinal()
	->setExtends(ParentClass::class)
	->addImplement(Countable::class)
	->addComment("Opis klasy.\nDruga linia\n")
	->addComment('@property-read Nette\Forms\Form $form');

// kod można łatwo wygenerować przez rzutowanie na ciąg znaków lub użycie echo:
echo $class;

Zwraca następujący wynik:

/**
 * Opis klasy
 * Druga linia
 *
 * @property-read Nette\Forms\Form $form
 */
final class Demo extends ParentClass implements Countable
{
}

Do wygenerowania kodu możemy również użyć tzw. printera, który w przeciwieństwie do echo $class będziemy mogli dalej konfigurować:

$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);

Możemy dodać stałe (klasa Constant) i zmienne (klasa Property):

$class->addConstant('ID', 123)
	->setProtected() // widoczność stałych
	->setType('int')
	->setFinal();

$class->addProperty('items', [1, 2, 3])
	->setPrivate() // lub setVisibility('private')
	->setStatic()
	->addComment('@var int[]');

$class->addProperty('list')
	->setType('?array')
	->setInitialized(); // wypisze '= null'

Wygeneruje:

final protected const int ID = 123;

/** @var int[] */
private static $items = [1, 2, 3];

public ?array $list = null;

I możemy dodać metody:

$method = $class->addMethod('count')
	->addComment('Policz to.')
	->setFinal()
	->setProtected()
	->setReturnType('?int') // typy zwracane w metodach
	->setBody('return count($items ?: $this->items);');

$method->addParameter('items', []) // $items = []
	->setReference()           // &$items = []
	->setType('array');        // array &$items = []

Wynikiem jest:

/**
 * Policz to.
 */
final protected function count(array &$items = []): ?int
{
	return count($items ?: $this->items);
}

Promowane parametry wprowadzone w PHP 8.0 można przekazać do konstruktora:

$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
	->setPrivate();

Wynikiem jest:

public function __construct(
	public $name,
	private $args = [],
) {
}

Właściwości i klasy przeznaczone tylko do odczytu można oznaczyć za pomocą funkcji setReadOnly().


Jeśli dodawana właściwość, stała, metoda lub parametr już istnieją, zostanie rzucony wyjątek.

Członków klasy można usunąć za pomocą removeProperty(), removeConstant(), removeMethod() lub removeParameter().

Do klasy można również dodać istniejące obiekty Method, Property lub Constant:

$method = new Nette\PhpGenerator\Method('getHandle');
$property = new Nette\PhpGenerator\Property('handle');
$const = new Nette\PhpGenerator\Constant('ROLE');

$class = (new Nette\PhpGenerator\ClassType('Demo'))
	->addMember($method)
	->addMember($property)
	->addMember($const);

Można również klonować istniejące metody, właściwości i stałe pod inną nazwą za pomocą cloneWithName():

$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);

Interfejs lub Trait

Można tworzyć interfejsy i traity (klasy InterfaceTypeTraitType):

$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');

Używanie traitów:

$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
	->addResolution('sayHello as protected')
	->addComment('@use MyTrait<Foo>');
echo $class;

Wynik:

class Demo
{
	use SmartObject;
	/** @use MyTrait<Foo> */
	use MyTrait {
		sayHello as protected;
	}
}

Enumy

Wyliczenia, które wprowadza PHP 8.1, można łatwo utworzyć w ten sposób: (klasa EnumType):

$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');

echo $enum;

Wynik:

enum Suit
{
	case Clubs;
	case Diamonds;
	case Hearts;
	case Spades;
}

Można również zdefiniować ekwiwalenty skalarne i utworzyć w ten sposób “backed” wyliczenie:

$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');

Do każdego case można dodać komentarz lub atrybuty za pomocą addComment() lub addAttribute().

Klasy anonimowe

Jako nazwę przekażemy null i mamy klasę anonimową:

$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
	->addParameter('foo');

echo '$obj = new class ($val) ' . $class . ';';

Wynik:

$obj = new class ($val) {

	public function __construct($foo)
	{
	}
};

Funkcje globalne

Kod funkcji generuje klasa GlobalFunction:

$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;

// lub użyj PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);

Wynik:

function foo($a, $b)
{
	return $a + $b;
}

Funkcje anonimowe

Kod funkcji anonimowych generuje klasa Closure:

$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
	->setReference();
echo $closure;

// lub użyj PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);

Wynik:

function ($a, $b) use (&$c) {
	return $a + $b;
}

Skrócone funkcje strzałkowe

Można również wypisać skróconą funkcję anonimową za pomocą printera:

$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');

echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);

Wynik:

fn($a, $b) => $a + $b

Sygnatury metod i funkcji

Metody reprezentuje klasa Method. Można ustawić widoczność, wartość zwracaną, dodać komentarze, atrybuty itp.:

$method = $class->addMethod('count')
	->addComment('Policz to.')
	->setFinal()
	->setProtected()
	->setReturnType('?int');

Poszczególne parametry reprezentuje klasa Parameter. Ponownie można ustawić wszystkie możliwe właściwości:

$method->addParameter('items', []) // $items = []
	->setReference()           // &$items = []
	->setType('array');        // array &$items = []

// function count(array &$items = [])

Do definicji tzw. parametrów variadics (lub też operatora splat) służy setVariadic():

$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');

Wygeneruje:

function count(...$items)
{
}

Ciała metod i funkcji

Ciało można przekazać naraz metodzie setBody() lub stopniowo (linia po linii) przez wielokrotne wywołanie addBody():

$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;

Wynik

function foo()
{
	$a = rand(10, 20);
	return $a;
}

Można użyć specjalnych symboli zastępczych do łatwego wstawiania zmiennych.

Proste symbole zastępcze ?

$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;

Wynik

function foo()
{
	return substr('any string', 3);
}

Symbol zastępczy dla variadic ...?

$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;

Wynik:

function foo()
{
	myfunc(1, 2, 3);
}

Można również użyć nazwanych parametrów dla PHP 8 za pomocą ...?:

$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);

// myfunc(foo: 1, bar: true);

Symbol zastępczy escapuje się za pomocą ukośnika \?

$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;

Wynik:

function foo($a)
{
	return $a ? 10 : 3;
}

Printer i zgodność z PSR

Do generowania kodu PHP służy klasa Printer:

$class = new Nette\PhpGenerator\ClassType('Demo');
// ...

$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // to samo, co: echo $class

Potrafi wygenerować kod wszystkich innych elementów, oferuje metody takie jak printFunction(), printNamespace(), itd.

Dostępna jest również klasa PsrPrinter, której wyjście jest zgodne ze stylem kodowania PSR-2 / PSR-12 / PER:

$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);

Potrzebujesz dostosować zachowanie do własnych potrzeb? Utwórz własną wersję, dziedzicząc po klasie Printer. Można przekonfigurować następujące zmienne:

class MyPrinter extends Nette\PhpGenerator\Printer
{
	// długość linii, po której następuje zawijanie wiersza
	public int $wrapLength = 120;
	// znak wcięcia, może być zastąpiony sekwencją spacji
	public string $indentation = "\t";
	// liczba pustych linii między właściwościami
	public int $linesBetweenProperties = 0;
	// liczba pustych linii między metodami
	public int $linesBetweenMethods = 2;
	// liczba pustych linii między grupami 'use statements' dla klas, funkcji i stałych
	public int $linesBetweenUseTypes = 0;
	// pozycja otwierającego nawiasu klamrowego dla funkcji i metod
	public bool $bracesOnNextLine = true;
	// umieść jeden parametr w jednej linii, nawet jeśli ma atrybut lub jest promowany
	public bool $singleParameterOnOneLine = false;
	// pomija przestrzenie nazw, które nie zawierają żadnej klasy ani funkcji
	public bool $omitEmptyNamespaces = true;
	// separator między prawym nawiasem a typem zwracanym funkcji i metod
	public string $returnTypeColon = ': ';
}

Jak i dlaczego właściwie różnią się standardowy Printer i PsrPrinter? Dlaczego w pakiecie nie ma tylko jednego printera, a mianowicie PsrPrinter?

Standardowy Printer formatuje kod tak, jak to robimy w całym Nette. Ponieważ Nette powstało znacznie wcześniej niż PSR, a także dlatego, że PSR przez długie lata nie dostarczało standardów na czas, ale na przykład z kilkuletnim opóźnieniem od wprowadzenia nowej funkcji w PHP, doszło do tego, że standard kodowania różni się w kilku drobnych szczegółach. Większą różnicą jest tylko używanie tabulatorów zamiast spacji. Wiemy, że używanie tabulatorów w naszych projektach umożliwia dostosowanie szerokości, co jest niezbędne dla osób z wadami wzroku. Przykładem drobnej różnicy jest umieszczenie nawiasu klamrowego na osobnej linii w przypadku funkcji i metod, i to zawsze. Zalecenie PSR wydaje nam się nielogiczne i prowadzi do zmniejszenia czytelności kodu.

Typy

Każdy typ lub typ union/intersection można przekazać jako ciąg znaków, można również użyć predefiniowanych stałych dla typów natywnych:

use Nette\PhpGenerator\Type;

$member->setType('array'); // lub Type::Array;
$member->setType('?array'); // or Type::nullable(Type::Array);
$member->setType('array|string'); // or Type::union(Type::Array, Type::String)
$member->setType('Foo&Bar'); // lub Type::intersection(Foo::class, Bar::class)
$member->setType(null); // usuwa typ

To samo dotyczy metody setReturnType().

Literały

Za pomocą Literal można przekazywać dowolny kod PHP, na przykład dla domyślnych wartości właściwości lub parametrów itp.:

use Nette\PhpGenerator\Literal;

$class = new Nette\PhpGenerator\ClassType('Demo');

$class->addProperty('foo', new Literal('Iterator::SELF_FIRST'));

$class->addMethod('bar')
	->addParameter('id', new Literal('1 + 2'));

echo $class;

Wynik:

class Demo
{
	public $foo = Iterator::SELF_FIRST;

	public function bar($id = 1 + 2)
	{
	}
}

Można również przekazać parametry do Literal i pozwolić je sformatować do poprawnego kodu PHP za pomocą symboli zastępczych:

new Literal('substr(?, ?)', [$a, $b]);
// generuje na przykład: substr('hello', 5);

Literał reprezentujący utworzenie nowego obiektu można łatwo wygenerować za pomocą metody new:

Literal::new(Demo::class, [$a, 'foo' => $b]);
// generuje na przykład: new Demo(10, foo: 20)

Atrybuty

Atrybuty PHP 8 można dodać do wszystkich klas, metod, właściwości, stałych, enumów, funkcji, closures i parametrów. Jako wartości parametrów można używać również literaly.

$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addAttribute('Table', [
	'name' => 'user',
	'constraints' => [
		Literal::new('UniqueConstraint', ['name' => 'ean', 'columns' => ['ean']]),
	],
]);

$class->addProperty('list')
	->addAttribute('Deprecated');

$method = $class->addMethod('count')
	->addAttribute('Foo\Cached', ['mode' => true]);

$method->addParameter('items')
	->addAttribute('Bar');

echo $class;

Wynik:

#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])]
class Demo
{
	#[Deprecated]
	public $list;


	#[Foo\Cached(mode: true)]
	public function count(
		#[Bar]
		$items,
	) {
	}
}

Property Hooks

Za pomocą property hooks (reprezentowanych przez klasę PropertyHook) można zdefiniować operacje get i set dla właściwości, co jest funkcją wprowadzoną w PHP 8.4:

$class = new Nette\PhpGenerator\ClassType('Demo');
$prop = $class->addProperty('firstName')
    ->setType('string');

$prop->addHook('set', 'strtolower($value)')
    ->addParameter('value')
	    ->setType('string');

$prop->addHook('get')
	->setBody('return ucfirst($this->firstName);');

echo $class;

Wygeneruje:

class Demo
{
    public string $firstName {
        set(string $value) => strtolower($value);
        get {
            return ucfirst($this->firstName);
        }
    }
}

Właściwości i property hooks mogą być abstrakcyjne lub finalne:

$class->addProperty('id')
    ->setType('int')
    ->addHook('get')
        ->setAbstract();

$class->addProperty('role')
    ->setType('string')
    ->addHook('set', 'strtolower($value)')
        ->setFinal();

Widoczność asymetryczna

PHP 8.4 wprowadza widoczność asymetryczną dla właściwości. Można ustawić różne poziomy dostępu dla odczytu i zapisu.

Widoczność można ustawić albo za pomocą metody setVisibility() z dwoma parametrami, albo za pomocą setPublic(), setProtected() lub setPrivate() z parametrem mode, który określa, czy widoczność dotyczy odczytu czy zapisu właściwości. Domyślny tryb to 'get'.

$class = new Nette\PhpGenerator\ClassType('Demo');

$class->addProperty('name')
    ->setType('string')
    ->setVisibility('public', 'private'); // public dla odczytu, private dla zapisu

$class->addProperty('id')
    ->setType('int')
    ->setProtected('set'); // protected dla zapisu

echo $class;

Wygeneruje:

class Demo
{
    public private(set) string $name;

    protected(set) int $id;
}

Przestrzeń nazw

Klasy, właściwości, interfejsy i wyliczenia (dalej zwane klasami) można grupować w przestrzenie nazw reprezentowane przez klasę PhpNamespace:

$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');

// tworzymy nowe klasy w przestrzeni nazw
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');

// lub wstawiamy istniejącą klasę do przestrzeni nazw
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);

Jeśli klasa już istnieje, zostanie rzucony wyjątek.

Można zdefiniować klauzule use:

// use Http\Request;
$namespace->addUse(Http\Request::class);
// use Http\Request as HttpReq;
$namespace->addUse(Http\Request::class, 'HttpReq');
// use function iter\range;
$namespace->addUseFunction('iter\range');

Aby uprościć w pełni kwalifikowaną nazwę klasy, funkcji lub stałej zgodnie z zdefiniowanymi aliasami, użyj metody simplifyName:

echo $namespace->simplifyName('Foo\Bar'); // 'Bar', ponieważ 'Foo' to bieżąca przestrzeń nazw
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', z powodu zdefiniowanego use-statement

Uproszczoną nazwę klasy, funkcji lub stałej można na odwrót przekształcić na w pełni kwalifikowaną nazwę za pomocą metody resolveName:

echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'

Tłumaczenie nazw klas

Gdy klasa jest częścią przestrzeni nazw, jest renderowana nieco inaczej: wszystkie typy (na przykład typehinty, typy zwracane, nazwa klasy nadrzędnej, implementowane interfejsy, używane właściwości i atrybuty) są automatycznie tłumaczone (jeśli tego nie wyłączysz, patrz poniżej). Oznacza to, że w definicjach musisz używać pełnych nazw klas, a te zostaną zastąpione aliasami (zgodnie z klauzulami use) lub w pełni kwalifikowanymi nazwami w wynikowym kodzie:

$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');

$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // zostanie uproszczone do A
	->addTrait('Bar\AliasedClass'); // zostanie uproszczone do AliasedClass

$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // w komentarzach upraszczamy ręcznie
$method->addParameter('arg')
	->setType('Bar\OtherClass'); // zostanie przetłumaczone na \Bar\OtherClass

echo $namespace;

// lub użyj PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);

Wynik:

namespace Foo;

use Bar\AliasedClass;

class Demo implements A
{
	use AliasedClass;

	/**
	 * @return D
	 */
	public function method(\Bar\OtherClass $arg)
	{
	}
}

Automatyczne tłumaczenie można wyłączyć w ten sposób:

$printer = new Nette\PhpGenerator\Printer; // lub PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);

Pliki PHP

Klasy, funkcje i przestrzenie nazw można grupować w pliki PHP reprezentowane przez klasę PhpFile:

$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('Ten plik jest generowany automatycznie.');
$file->setStrictTypes(); // dodaje declare(strict_types=1)

$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');

// lub
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');

echo $file;

// lub użyj PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);

Wynik:

<?php

/**
 * Ten plik jest generowany automatycznie.
 */

declare(strict_types=1);

namespace Foo;

class A
{
}

function foo()
{
}

Uwaga: Do plików nie można dodawać żadnego innego kodu poza funkcjami i klasami.

Generowanie na podstawie istniejących

Oprócz tego, że klasy i funkcje można modelować za pomocą opisanego powyżej API, można je również wygenerować automatycznie na podstawie istniejących wzorców:

// tworzy klasę taką samą jak klasa PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);

// tworzy funkcję identyczną z funkcją trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');

// tworzy closure na podstawie podanej
$closure = Nette\PhpGenerator\Closure::from(
	function (stdClass $a, $b = null) {},
);

Ciała funkcji i metod są domyślnie puste. Jeśli chcesz je również załadować, użyj tego sposobu (wymaga instalacji pakietu nikic/php-parser):

$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);

$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);

Wczytywanie z plików PHP

Funkcje, klasy, interfejsy i enumy można wczytywać również bezpośrednio z ciągu znaków zawierającego kod PHP. Na przykład w ten sposób utworzymy obiekt ClassType:

$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
	<?php

	class Demo
	{
		public $foo;
	}
	XX);

Podczas wczytywania klas z kodu PHP, jednoliniowe komentarze poza ciałami metod są ignorowane (np. przy właściwościach itp.), ponieważ ta biblioteka nie ma API do pracy z nimi.

Można również wczytać bezpośrednio cały plik PHP, który może zawierać dowolną liczbę klas, funkcji lub nawet przestrzeni nazw:

$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));

Wczytany zostanie również komentarz początkowy pliku i deklaracja strict_types. Natomiast cały pozostały kod globalny jest ignorowany.

Wymagane jest zainstalowanie nikic/php-parser.

Jeśli potrzebujesz manipulować globalnym kodem w plikach lub poszczególnymi instrukcjami w ciałach metod, lepiej użyć bezpośrednio biblioteki nikic/php-parser.

Class Manipulator

Klasa ClassManipulator dostarcza narzędzi do manipulacji klasami.

$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);

Metoda inheritMethod() kopiuje metodę z klasy nadrzędnej lub implementowanego interfejsu do Twojej klasy. Pozwala to nadpisać metodę lub rozszerzyć jej sygnaturę:

$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
  ```

Metoda `inheritProperty()` kopiuje właściwość z klasy nadrzędnej do Twojej klasy. Jest to przydatne, gdy chcesz mieć w swojej klasie tę samą właściwość, ale na przykład z inną wartością domyślną:

```php
$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');

Metoda implement() automatycznie implementuje wszystkie metody i właściwości z danego interfejsu lub klasy abstrakcyjnej w Twojej klasie:

$manipulator->implement(SomeInterface::class);
// Teraz Twoja klasa implementuje SomeInterface i zawiera wszystkie jego metody

Zrzut zmiennych

Klasa Dumper konwertuje zmienną na parsowalny kod PHP. Dostarcza lepsze i bardziej przejrzyste wyjście niż standardowa funkcja var_export().

$dumper = new Nette\PhpGenerator\Dumper;

$var = ['a', 'b', 123];

echo $dumper->dump($var); // wypisze ['a', 'b', 123]

Tabela kompatybilności

PhpGenerator 4.1 jest kompatybilny z PHP 8.0 do 8.4.

wersja: 4.0