rikudou / source-generators
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
- rector/rector: ^1.2
This package is auto-updated.
Last update: 2024-08-27 07:30:30 UTC
README
此软件包为PHP提供源生成器的功能,这使得在构建时生成类成为可能,从而在运行时节省处理能力。
最重要的是,它使第三方软件包能够这样做,这意味着第三方软件包可以为接口的所有实例或所有带有属性的类创建源生成器。
阅读以下指南后,您可能对高级使用感兴趣。
安装
composer require rikudou/source-generators
示例
<?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 )); } }
注意:%className% 将自动替换为实际的类名
下次,当您转储 composer 自动加载器(通过使用 composer dump-autoload
或 composer install
)时,所有实现 SourceGenerator
(包括上面定义的 HelloWorldSourceGenerator
)的类都将运行,并将生成类。
这是由上述代码生成的类
<?php declare (strict_types=1); namespace App; final class HelloWorld { public function sayHello(): void { echo "Hello world!"; } }
用法
不错,不是吗?好吧,如果那只是可能性的极限,那就太无聊了。在 execute()
方法中作为参数接收的 Context
接口包含一些有用的方法来查找东西,特别是
getPartialClasses()
- 返回标记为部分的所有类的反射(有关部分类的更多信息见下文)findClassesByAttribute(string $attribute)
- 返回带有特定属性的所有类的反射findClassesByParent(string $parent)
- 返回扩展给定父类(无论是类还是接口)的所有类的反射
为了配合查找东西的方法,还有一些实现东西的方法
addClassSource(ClassSource $source)
- 你已经在示例中看到了这个方法,用于添加自定义生成的类implementPartialClassMethod(MethodImplementation $implementation)
- 在部分类中实现方法createPartialClassProperty(PropertyImplementation $implementation): void
- 在部分类中创建属性markClassAsImplemented(string $className): void
- 将部分类标记为已实现,即使没有任何变化
部分类?
部分类是只部分实现的类。例如,一个方法可能缺失。这些类用 #[PartialClass]
属性标记,并且标记为这样的每个类必须由源生成器实现。
每个部分类都可以有方法或属性用 #[PartialMethod]
或 #[PartialProperty]
标记,并且也必须实现。请注意,您可以实现其他方法/属性,包括现有的,但是用其中一个 Partial*
属性标记基本上创建了一个合同,表示它将由源生成器实现。
让我们以这个部分类为例
<?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}!"; } }
如果您现在运行 composer install
或 composer dump-autoload
,您将收到 UnimplementedPartialClassException
异常,表示:类 'App\HelloWorld' 是部分的,必须由源生成器实现。
所以让我们创建一个!
<?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', )); } }
现在生成了一个新类,看起来像这样
<?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}!"; } }
忽略难看的缩进,如果您现在运行此代码,您应该在终端看到 Hello, John!
!
<?php $hello = new \App\HelloWorld(); $hello->sayHello();
那么PHP是如何知道应使用两个同名和命名空间相同的类中的哪一个呢?这很简单,源生成器类始终优先。内部创建了一个新的源生成器类类映射,并且在这些类之前的自动加载器被注入。
类映射仅包含必要的部分,在我们的简单示例中,它看起来像这样
<?php $rikudouSourceGeneratorsClassMap = array ( 'App\\HelloWorld' => __DIR__ . '/HelloWorld.php', );
源代码编写
你可能已经注意到,在第一个例子中,我直接使用了类的源代码。虽然这对于一些简单的方法来说可能效果很好,但对于更复杂的方法,你最好使用抽象语法树(AST)。这是完全支持的,下面是使用AST重写的第一个示例
<?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()], )); } }
使用AST可以使得复杂逻辑的实现变得容易得多,因为你可以根据某些参数修改实现,而不是试图将多个字符串混合在一起,并在过程中迷失方向。
注意:%className% 将自动替换为实际的类名
那么它能做什么呢?
正如人们所说,天高任鸟飞。在.NET世界中,你可以使用源生成器在不使用任何运行时反射的情况下序列化和反序列化类和JSON。你可以用它来创建实现特定接口的所有类的集合。你可以创建一个Memoizable
属性,它会自动为所有标记为这样的类创建代理并缓存方法调用的结果。你可以创建一个非常高效的依赖注入。你可以创建一个AprilFoolsSourceGenerator
,它随机地将所有返回bool类型的方法的返回值取反。