rikudou/source-generators

dev-master 2024-08-27 07:30 UTC

This package is auto-updated.

Last update: 2024-08-27 07:30:30 UTC


README

This package provides the functionality of source generators to php, this makes it possible to generate classes at build-time and thus save on processing power in the runtime.

And most importantly, it makes it possible for 3rd party packages to do so, meaning a 3rd party package may create a source generator for all instances of an interface or for all classes annotated by an attribute.

After reading the guide below, you might be interested in the advanced usage.

Installing

composer require rikudou/source-generators

An example

<?php

namespace App;

use Rikudou\SourceGenerators\Context\Context;
use Rikudou\SourceGenerators\Contract\SourceGenerator;
use Rikudou\SourceGenerators\Dto\ClassSource;

final readonly class HelloWorldSourceGenerator implements SourceGenerator
{
    public function execute(Context $context): void
    {
        $context->addClassSource(new ClassSource(
            class: 'HelloWorld',
            namespace: 'App',
            content: <<<EOF
                final class %className%
                {
                    public function sayHello(): void
                    {
                        echo "Hello world!";
                    }
                }
                EOF
        ));
    }
}

Note: %className% gets replaced with the actual class name automatically

Next time, when you dump the composer autoloader (by using composer dump-autoload or composer install), every class implementing SourceGenerator (including the HelloWorldSourceGenerator defined above) will run and the class will get generated.

This is the class, as generated by the above code:

<?php

declare (strict_types=1);
namespace App;

final class HelloWorld
{
    public function sayHello(): void
    {
        echo "Hello world!";
    }
}

Usage

Neat, isn't it? Well, if that was the extent of what's possible, it would be boring. The Context interface that you receive as a parameter in the execute() method contains some useful methods for finding stuff, namely:

  • getPartialClasses() - returns reflections of all classes marked as partial (more on partial classes below)
  • findClassesByAttribute(string $attribute) - returns reflections of all classes marked with a specific attribute
  • findClassesByParent(string $parent) - returns reflections of all classes extending a given parent, be it class or an interface

To accompany the methods for finding stuff, there are methods for implementing stuff:

  • addClassSource(ClassSource $source) - the one you've seen already in the example, method for adding custom generated classes
  • implementPartialClassMethod(MethodImplementation $implementation) - implements a method in a partial class
  • createPartialClassProperty(PropertyImplementation $implementation): void - creates a property in a partial class
  • markClassAsImplemented(string $className): void - marks a partial class as implemented, even if nothing changed

Partial classes?

Partial classes are classes that are only partially implemented. For example a method may be missing. Those classes are marked with the #[PartialClass] attribute and every class marked as such must be implemented by a source generator.

Each partial class can have methods or properties marked with #[PartialMethod] or #[PartialProperty] and those must be implemented as well. Note that you may implement other methods/properties as well, including existing ones, but marking something with one of the Partial* attributes basically creates a contract that it will be implemented by a source generator.

As an example, let's define this partial class:

<?php

namespace App;

use Rikudou\SourceGenerators\Attribute\PartialClass;
use Rikudou\SourceGenerators\Attribute\PartialProperty;

#[PartialClass]
final class HelloWorld
{
    #[PartialProperty]
    private string $name;

    public function sayHello(): void
    {
        echo "Hello, {$this->name}!";
    }
}

If you'd run composer install or composer dump-autoload right now, you would get UnimplementedPartialClassException saying: The class 'App\HelloWorld' is partial and must be implemented by a source generator.. So let's create one!

<?php

namespace App;

use Rikudou\SourceGenerators\Context\Context;
use Rikudou\SourceGenerators\Contract\SourceGenerator;
use Rikudou\SourceGenerators\Dto\PropertyImplementation;

final readonly class HelloWorldSourceGenerator implements SourceGenerator
{
    public function execute(Context $context): void
    {
        $context->createPartialClassProperty(new PropertyImplementation(
            class: HelloWorld::class,
            name: 'name',
            defaultValue: 'John',
        ));
    }
}

Now a new class gets generated and it looks like this:

<?php

namespace App;

use Rikudou\SourceGenerators\Attribute\PartialClass;
use Rikudou\SourceGenerators\Attribute\PartialProperty;
final class HelloWorld
{
    private string $name = 'John';
    public function sayHello(): void
    {
        echo "Hello, {$this->name}!";
    }
}

The ugly indentation aside, if you now run this code, you should see Hello, John! in your terminal!

<?php

$hello = new \App\HelloWorld();
$hello->sayHello();

So how did php know which of the two classes with the same name and namespace should be used? That's easy, source generated classes always win. Internally a new classmap for source generated classes is created and the autoloader for those classes gets injected before the composer autoloader.

The classmap only includes what's necessary and for our simple example it looks like this:

<?php

$rikudouSourceGeneratorsClassMap = array (
  'App\\HelloWorld' => __DIR__ . '/HelloWorld.php',
);

Source code writing

You may have noticed that in the first example I've used a source code of the class directly. While that may work well for some simple methods, for more complex ones you'd rather use AST. This is fully supported, here's the 1st example rewritten to use it:

<?php

namespace App;

use PhpParser\Builder\Class_;
use PhpParser\Builder\Method;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Echo_;
use Rikudou\SourceGenerators\Context\Context;
use Rikudou\SourceGenerators\Contract\SourceGenerator;
use Rikudou\SourceGenerators\Dto\ClassSource;

final readonly class HelloWorldSourceGenerator implements SourceGenerator
{
    public function execute(Context $context): void
    {
        $class = (new Class_('%className%'))
            ->makeFinal()
            ->addStmt(
                (new Method('sayHello'))
                    ->makePublic()
                    ->setReturnType(new Identifier('void'))
                    ->addStmt(new Echo_([new String_('Hello world!')]))
            );
        $context->addClassSource(new ClassSource(
            class: 'HelloWorld',
            namespace: 'App',
            content: [$class->getNode()],
        ));
    }
}

Using AST like that makes complex logic much easier because you can modify the implementation based on some parameters instead of trying to mash multiple strings together and get lost in the process.

Note: %className% gets replaced with the actual class name automatically

So what all can this do?

The sky is the limit, as they say. In .NET world you can use source generators to serialize and deserialize classes and jsons without using any runtime reflection. You can use it to create a collection of all classes implementing a certain interface. You can create a Memoizable attribute that automatically creates proxies for all classes marked as such and memoizes the result of the method calls. You can create a very efficient dependency injection. You can create an AprilFoolsSourceGenerator that randomly makes all methods that return bool return the opposite value.