rikudou / source-generators
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 0
Open Issues: 0
Type:composer-plugin
Requires
- php: ^8.3
- composer-plugin-api: ^2.0
- nikic/php-parser: ^5.1
- rikudou/iterables: ^1.0
Requires (Dev)
- composer/composer: ^2.7
- friendsofphp/php-cs-fixer: ^3.63
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^11.4
- rector/rector: ^1.2
This package is auto-updated.
Last update: 2024-11-22 13:05:26 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 attributefindClassesByParent(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 classesimplementPartialClassMethod(MethodImplementation $implementation)
- implements a method in a partial classcreatePartialClassProperty(PropertyImplementation $implementation): void
- creates a property in a partial classmarkClassAsImplemented(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.