PhpGenerator 3.6: news from PHP 8.1 and more

about a month ago by David Grudl     edit

PhpGenerator version 3.6 brings support for all the new features of the upcoming PHP 8.1. But that's not all!

Enum

The main new feature of PHP 8.1 is enumeration types. The generator makes it easy to create them:

$enum = Nette\PhpGenerator\ClassType::enum('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');
echo $enum;

Result:

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

You can also define scalar equivalents to create a “backed” enumeration:

$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');
$enum->addCase('Hearts', '♥');
$enum->addCase('Spades', '♠');

Result:

enum Suit: string
{
        case Clubs = '♣';
        case Diamonds = '♦';
        case Hearts = '♥';
        case Spades = '♠';
}

You can add comments or attributes to each case using addComment() and addAttribute() respectively.

Other new features in PHP 8.1

You can now flag constants as final, create readonly properties, use intersection types and the new operator in the default values of function or method parameters:

use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Type;

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

$class->addConstant('NAME', 'foo')
	->setFinal();

$class->addProperty('foo')
	->setType(Type::intersection('Foo', 'Bar')) // or 'Foo&Bar'
	->setReadOnly();

$class->addMethod('__construct')
	->addParameter('now', new Literal('new DateTime'));

echo $class;

Prints:

class Php81Features
{
	final public const NAME = 'foo';

	public readonly Foo&Bar $foo;

	public function __construct($now = new DateTime)
	{
	}
}

Furthermore, Dumper can output (static) closures using the new three-dot syntax.

Functions in Files and Namespaces

You can now add functions to PHP files or namespaces:

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

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

And also define use-statements for functions and constants:

// use function iter\range;
$namespace->addUseFunction('iter\range');
// use constant PHP_EOL;
$namespace->addUseConstant('PHP_EOL');

Loading Classes from Files and Strings

And watch out, this is big news! You can now load representations of classes, interfaces, functions, enums, etc. directly from a string containing PHP code without running it:

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

To load a single class, you can directly use:

$class = Nette\PhpGenerator\ClassType::fromCode($code);

This gives you an exact representation of the class, including the bodies of all methods and functions.

(Requires installing composer require nikic/php-parser)

Loading Classes from Reflection and Traits

It was already possible to create class representations based on existing ones using the reflection via ClassType::from(). This still works, of course. The only difficulty with reflection is that it can't tell which methods or properties come from traits. Therefore, Nette simply materialized the traits, i.e., the representations did not contain use TraitFoo, but directly the methods and properties.

trait TraitFoo
{
	public function foo()
	{
	}
}

class Bar
{
	use TraitFoo;
}

$class = Nette\PhpGenerator\ClassType::from(Bar::class);
echo $class;

Thus, the created class was written out as:

class Bar
{
	public function foo()
	{
	}
}

Which is functionally quite identical to the original class, but it does not preserve the exact form.

Nette has gradually learned to guess the use of traits quite well, and can now generate code without materialization. As of version 4.0 it will do this automatically, for now we tell it via a parameter:

$class = Nette\PhpGenerator\ClassType::from(Bar::class, materializeTraits: false);
// for PHP < 8 use from(Bar::class, true, false);

Smarter Literals

You can use literals to pass arbitrary PHP code to representations, for example for default values of properties or parameters etc. You can now pass parameters to literals and have them formatted into valid PHP code using placeholders:

use Nette\PhpGenerator\Literal;

$a = 10;
$b = 20;
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addMethod('bar')
	->addParameter('id', new Literal('? + ?', [$a, $b]));

echo $class;

Result:

class Demo
{
	public function bar($id = 10 + 20)
	{
	}
}

Comments for Traits

If a class uses traits, these can now have comments. They are important for example for PHPStan.

$class = new Nette\PhpGenerator\ClassType('Bar');
$class->addTrait('GenericTrait', true)
	->addComment('@use GenericTrait<int>');

Result:

class Bar
{
	/** @use GenericTrait<int> */
	use GenericTrait;
}

The true parameter is important – originally the addTrait() method returned a ClassType object (i.e. $this), with a true value it returns a new TraitUse object that allows to add a comment or resolution information (addResolution('hello as protected')). In version 4.0, this will be the default behavior.

Lots of other little things

The new version has a lot of other tweaks like:

  • checking for forbidden class names
  • ability to set the maximum line width $printer->wrapLength
  • getType(true) and getReturnType(true) return Type objects

Further reading