├── .gitignore
├── .php-cs-fixer.php
├── .phpstorm.meta.php
├── LICENSE
├── README.md
├── composer.json
├── php-accessor
├── phpunit.xml
├── src
├── Attribute
│ ├── Data.php
│ ├── DefaultNull.php
│ ├── Map
│ │ ├── AccessorType.php
│ │ ├── NamingConvention.php
│ │ └── PrefixConvention.php
│ └── Overlook.php
├── Console
│ ├── Application.php
│ ├── Command
│ │ └── GenerateCommand.php
│ └── ConfigurationResolver.php
├── Exception
│ ├── InvalidConfigurationException.php
│ ├── PhpAccessorException.php
│ └── ProxyGenerationException.php
├── File
│ └── File.php
├── Meta
│ └── ClassMetadata.php
├── Processor
│ ├── Attribute
│ │ ├── AbstractAttributeHandler.php
│ │ ├── AttributeHandlerInterface.php
│ │ ├── DataHandler.php
│ │ ├── DefaultNullHandler.php
│ │ ├── OverlookHandler.php
│ │ └── Parameter
│ │ │ ├── AbstractParameterHandler.php
│ │ │ ├── AccessorTypeHandler.php
│ │ │ ├── NamingConventionHandler.php
│ │ │ ├── ParameterHandlerInterface.php
│ │ │ └── PrefixConventionHandler.php
│ ├── AttributeProcessor.php
│ ├── ClassProcessor.php
│ ├── CommentProcessor.php
│ ├── Method
│ │ ├── AbstractAccessorMethod.php
│ │ ├── AccessorMethodInterface.php
│ │ ├── AccessorMethodType.php
│ │ ├── FieldMetadata.php
│ │ ├── FieldMetadataBuilder.php
│ │ ├── Generator
│ │ │ ├── GeneratorInterface.php
│ │ │ ├── Getter
│ │ │ │ ├── GetterBodyGenerator.php
│ │ │ │ ├── MethodCommentGenerator.php
│ │ │ │ └── ReturnTypeGenerator.php
│ │ │ ├── MethodNameGenerator.php
│ │ │ └── Setter
│ │ │ │ ├── ParameterTypeGenerator.php
│ │ │ │ ├── SetterBodyGenerator.php
│ │ │ │ └── SetterReturnTypeGenerator.php
│ │ ├── GetterMethod.php
│ │ └── SetterMethod.php
│ ├── MethodFactory.php
│ └── TraitAccessor.php
└── Runner.php
└── tests
├── Cases
├── AccessorTypeTest.php
├── ApplicationTest.php
├── ConstructorPromotionTest.php
├── DefaultNullTest.php
├── GetterMethodCommentTest.php
├── NamingConventionTest.php
├── OverlookTest.php
├── PrefixConventionTest.php
└── RunnerTest.php
├── Mock
├── ConstructorPromotion.php
├── DefaultNullAll.php
├── DefaultNullPartial.php
├── Foo.php
├── FooInterface1.php
├── FooInterface2.php
├── FooSub.php
├── GenerateMethodComment.php
├── GetterMethodComment.php
├── NamingConventionLowerCamelCase.php
├── NamingConventionNone.php
├── NamingConventionUpperCamelCase.php
├── OnlyGetter.php
├── OnlySetter.php
├── Overlook.php
├── PrefixConventionBooleanIs.php
├── PrefixConventionGetSet.php
├── SuperFoo.php
└── TestAttribute.php
├── Tools
└── GeneratorHelper.php
└── bootstrap.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /.idea
3 | composer.lock
4 | /.php-accessor
5 | *.cache
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | setRules([
21 | '@PSR2' => true,
22 | '@Symfony' => true,
23 | '@DoctrineAnnotation' => true,
24 | '@PhpCsFixer' => true,
25 | 'header_comment' => [
26 | 'comment_type' => 'PHPDoc',
27 | 'header' => $fileHeaderComment,
28 | 'separate' => 'none',
29 | 'location' => 'after_declare_strict',
30 | ],
31 | 'array_syntax' => [
32 | 'syntax' => 'short',
33 | ],
34 | 'list_syntax' => [
35 | 'syntax' => 'short',
36 | ],
37 | 'concat_space' => [
38 | 'spacing' => 'one',
39 | ],
40 | 'blank_line_before_statement' => [
41 | 'statements' => [
42 | 'declare',
43 | ],
44 | ],
45 | 'general_phpdoc_annotation_remove' => [
46 | 'annotations' => [
47 | 'author',
48 | ],
49 | ],
50 | 'ordered_imports' => [
51 | 'imports_order' => [
52 | 'class', 'function', 'const',
53 | ],
54 | 'sort_algorithm' => 'alpha',
55 | ],
56 | 'single_line_comment_style' => [
57 | 'comment_types' => [
58 | ],
59 | ],
60 | 'yoda_style' => [
61 | 'always_move_variable' => false,
62 | 'equal' => false,
63 | 'identical' => false,
64 | ],
65 | 'phpdoc_align' => [
66 | 'align' => 'left',
67 | ],
68 | 'multiline_whitespace_before_semicolons' => [
69 | 'strategy' => 'no_multi_line',
70 | ],
71 | 'constant_case' => [
72 | 'case' => 'lower',
73 | ],
74 | 'global_namespace_import' => [
75 | 'import_classes' => true,
76 | 'import_constants' => true,
77 | 'import_functions' => true,
78 | ],
79 | 'class_attributes_separation' => true,
80 | 'combine_consecutive_unsets' => true,
81 | 'declare_strict_types' => true,
82 | 'linebreak_after_opening_tag' => true,
83 | 'lowercase_static_reference' => true,
84 | 'no_useless_else' => true,
85 | 'no_unused_imports' => true,
86 | 'not_operator_with_successor_space' => true,
87 | 'not_operator_with_space' => false,
88 | 'ordered_class_elements' => true,
89 | 'php_unit_strict' => false,
90 | 'phpdoc_separation' => false,
91 | 'single_quote' => true,
92 | 'standardize_not_equals' => true,
93 | 'multiline_comment_opening_closing' => true,
94 | ])
95 | ->setRiskyAllowed(true)
96 | ->setFinder(
97 | PhpCsFixer\Finder::create()
98 | ->exclude('vendor')
99 | ->in(__DIR__)
100 | )
101 | ->setUsingCache(false);
102 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 | '@']));
13 | override(\PhpAccessor\Processor\Attribute\DataHandler::getParameterHandler(0), map(['' => '@']));
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 PHP Accessor
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Accessor
2 |
3 | 生成类访问器(Getter & Setter)
4 |
5 | ## 快速入门
6 |
7 | ### 安装
8 |
9 | ```console
10 | composer require free2one/php-accessor
11 | ```
12 |
13 | 项目`composer.json` 文件中配置以下信息
14 | ```json
15 | {
16 | "scripts":{
17 | "php-accessor": "@php vendor/bin/php-accessor generate"
18 | }
19 | }
20 | ```
21 | 将相应的注释添加到需要生成访问器的类中:
22 | ```php
23 | getDefaultNull()); // output: NULL
137 | ```
138 |
139 |
140 |
141 |
142 | ## 要点说明
143 |
144 | ### 如何使用生成的代理类
145 |
146 | 如果你的项目使用的是Hyperf框架,则可直接引入Hyperf PHP Accessor包。其他情况下,请参考以下示例。
147 |
148 | 待生成访问器的类`Entity`
149 |
150 | ```php
151 | 'generate',
187 | 'path' => $scanDir,
188 | '--dir' => $proxyDir,
189 | '--gen-meta' => 'yes', //发布线上时,可设置为no
190 | '--gen-proxy' => 'yes',
191 | ]);
192 | $app = new Application();
193 | $app->setAutoExit(false);
194 | $app->run($input);
195 |
196 | //利用composer注册自动加载
197 | $finder = new Finder();
198 | $finder->files()->name('*.php')->in($proxyDir);
199 | $classLoader = new ClassLoader();
200 | $classMap = [];
201 | foreach ($finder->getIterator() as $value) {
202 | $classname = str_replace('@', '\\', $value->getBasename('.' . $value->getExtension()));
203 | $classname = substr($classname, 1);
204 | $classMap[$classname] = $value->getRealPath();
205 | }
206 | $classLoader->addClassMap($classMap);
207 | $classLoader->register(true);
208 |
209 | //Entity已被替换为代理类😸
210 | $entity = new Entity();
211 | $entity->setId(222);
212 | var_dump($entity);
213 | ```
214 |
215 | ## 相关资源
216 |
217 | #### PHP Accessor: 访问器生成器
218 |
219 | #### PHP Accessor IDEA Plugin: Phpstorm插件,文件保存时自动生成访问器.支持访问器的跳转,代码提示,查找及类字段重构等.
220 |
221 | #### Hyperf PHP Accessor: Hyperf框架SDK
222 |
223 | #### Laravel PHP Accessor: Laravel框架SDK
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "free2one/php-accessor",
3 | "description": "Generate getter and setter methods automatically",
4 | "type": "library",
5 | "license": "MIT",
6 | "autoload": {
7 | "psr-4": {
8 | "PhpAccessor\\": "src/"
9 | }
10 | },
11 | "autoload-dev" : {
12 | "psr-4" : {
13 | "PhpAccessor\\Test\\" : "tests"
14 | }
15 | },
16 | "require": {
17 | "php": ">=8.0",
18 | "nikic/php-parser": "^4.15",
19 | "symfony/console": "^5.4 || ^6.0",
20 | "symfony/finder": "^5.4 || ^6.0",
21 | "symfony/filesystem": "^5.4 || ^6.0",
22 | "phpstan/phpdoc-parser": "^1.20"
23 | },
24 | "require-dev": {
25 | "friendsofphp/php-cs-fixer": "^3.0",
26 | "phpunit/phpunit": "^9.0",
27 | "symfony/var-dumper": "^6.0"
28 | },
29 | "scripts": {
30 | "test": "@php vendor/bin/phpunit -c phpunit.xml --colors=always"
31 | },
32 | "bin": [
33 | "php-accessor"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/php-accessor:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
35 | __HALT_COMPILER();
36 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | src
14 |
15 |
16 |
17 |
18 | tests
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Attribute/Data.php:
--------------------------------------------------------------------------------
1 | namingConvention = $namingConvention;
40 | $this->accessorType = $accessorType;
41 | $this->prefixConvention = $prefixConvention;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Attribute/DefaultNull.php:
--------------------------------------------------------------------------------
1 | add(new GenerateCommand());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Console/Command/GenerateCommand.php:
--------------------------------------------------------------------------------
1 | setDefinition(
29 | [
30 | new InputArgument('path', InputArgument::IS_ARRAY, 'The path.'),
31 | new InputOption('dir', '', InputOption::VALUE_REQUIRED, 'The directory to the generated file.'),
32 | new InputOption('gen-meta', '', InputOption::VALUE_REQUIRED, 'The metadata should be generated (can be yes or no).'),
33 | new InputOption('gen-proxy', '', InputOption::VALUE_REQUIRED, 'The proxy class should be generated (can be yes or no).'),
34 | ]
35 | )
36 | ->setDescription('Fixes a directory or a file.');
37 | }
38 |
39 | protected function execute(InputInterface $input, OutputInterface $output): int
40 | {
41 | $io = new SymfonyStyle($input, $output);
42 | $path = $input->getArgument('path');
43 | $resolver = new ConfigurationResolver(
44 | [
45 | 'path' => $path,
46 | 'dir' => $input->getOption('dir'),
47 | 'gen-meta' => $input->getOption('gen-meta'),
48 | 'gen-proxy' => $input->getOption('gen-proxy'),
49 | ],
50 | getcwd()
51 | );
52 |
53 | if (! $resolver->getGenProxy() && ! $resolver->getGenMeta()) {
54 | $io->error('Both metadata and proxy are set to false');
55 |
56 | return Command::FAILURE;
57 | }
58 |
59 | $finder = $resolver->getFinder();
60 | $finder = new ArrayIterator(iterator_to_array($finder));
61 | $runner = new Runner(
62 | finder: $finder,
63 | dir: $resolver->getDir(),
64 | genMeta: $resolver->getGenMeta(),
65 | genProxy: $resolver->getGenProxy(),
66 | );
67 | $runner->generate();
68 | foreach ($runner->getGeneratedFiles() as $proxyFile) {
69 | $io->writeln('[generated-file] ' . $proxyFile);
70 | }
71 |
72 | return Command::SUCCESS;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Console/ConfigurationResolver.php:
--------------------------------------------------------------------------------
1 | [],
26 | 'dir' => null,
27 | 'gen-meta' => null,
28 | 'gen-proxy' => null,
29 | ];
30 |
31 | private array $path = [];
32 |
33 | private $dir;
34 |
35 | private ?iterable $finder = null;
36 |
37 | private bool $genMeta;
38 |
39 | private bool $genProxy;
40 |
41 | public function __construct(
42 | array $options,
43 | string $cwd,
44 | ) {
45 | $this->cwd = $cwd;
46 |
47 | foreach ($options as $name => $value) {
48 | $this->setOption($name, $value);
49 | }
50 | }
51 |
52 | public function getFinder(): iterable
53 | {
54 | if ($this->finder === null) {
55 | $this->finder = $this->resolveFinder();
56 | }
57 |
58 | return $this->finder;
59 | }
60 |
61 | public function getPath(): array
62 | {
63 | if (empty($this->path)) {
64 | $this->path = $this->buildAbsolutePaths($this->options['path']);
65 | }
66 |
67 | return $this->path;
68 | }
69 |
70 | public function getDir(): string
71 | {
72 | if (empty($this->dir)) {
73 | $cwd = $this->cwd;
74 | $dir = $this->options['dir'] ?: null;
75 | if (empty($dir)) {
76 | $this->dir = $cwd;
77 | } else {
78 | $this->dir = $dir;
79 | }
80 | }
81 |
82 | return $this->dir;
83 | }
84 |
85 | public function getGenMeta(): bool
86 | {
87 | if (! isset($this->genMeta)) {
88 | if ($this->options['gen-meta'] === null) {
89 | $this->genMeta = false;
90 | } else {
91 | $this->genMeta = $this->resolveOptionBooleanValue('gen-meta');
92 | }
93 | }
94 |
95 | return $this->genMeta;
96 | }
97 |
98 | public function getGenProxy(): bool
99 | {
100 | if (! isset($this->genProxy)) {
101 | if ($this->options['gen-proxy'] === null) {
102 | $this->genProxy = true;
103 | } else {
104 | $this->genProxy = $this->resolveOptionBooleanValue('gen-proxy');
105 | }
106 | }
107 |
108 | return $this->genProxy;
109 | }
110 |
111 | private function resolveOptionBooleanValue(string $optionName): bool
112 | {
113 | $value = $this->options[$optionName];
114 |
115 | if (! is_string($value)) {
116 | throw new InvalidConfigurationException(sprintf('Expected boolean or string value for option "%s".', $optionName));
117 | }
118 |
119 | if ($value === 'yes') {
120 | return true;
121 | }
122 |
123 | if ($value === 'no') {
124 | return false;
125 | }
126 |
127 | throw new InvalidConfigurationException(sprintf('Expected "yes" or "no" for option "%s", got "%s".', $optionName, $value));
128 | }
129 |
130 | private function buildAbsolutePaths(array $paths): array
131 | {
132 | $filesystem = new Filesystem();
133 | $cwd = $this->cwd;
134 |
135 | return array_map(
136 | static function (string $rawPath) use ($cwd, $filesystem): string {
137 | $path = trim($rawPath);
138 |
139 | if ($path === '') {
140 | throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
141 | }
142 |
143 | $absolutePath = $filesystem->isAbsolutePath($path)
144 | ? $path
145 | : $cwd . DIRECTORY_SEPARATOR . $path;
146 |
147 | if (! file_exists($absolutePath)) {
148 | throw new InvalidConfigurationException(sprintf('The path "%s" is not readable.', $path));
149 | }
150 |
151 | return $absolutePath;
152 | },
153 | $paths
154 | );
155 | }
156 |
157 | private function setOption(string $name, $value): void
158 | {
159 | if (! array_key_exists($name, $this->options)) {
160 | throw new InvalidConfigurationException(sprintf('Unknown option name: "%s".', $name));
161 | }
162 |
163 | $this->options[$name] = $value;
164 | }
165 |
166 | private function resolveFinder(): iterable
167 | {
168 | $paths = array_filter(array_map(
169 | static function (string $path) {
170 | return realpath($path);
171 | },
172 | $this->getPath()
173 | ));
174 |
175 | $pathsByType = [
176 | 'file' => [],
177 | 'dir' => [],
178 | ];
179 |
180 | foreach ($paths as $path) {
181 | if (is_file($path)) {
182 | $pathsByType['file'][] = $path;
183 | } else {
184 | $pathsByType['dir'][] = $path . DIRECTORY_SEPARATOR;
185 | }
186 | }
187 |
188 | return (new Finder())
189 | ->files()
190 | ->name('*.php')
191 | ->exclude('vendor')
192 | ->in($pathsByType['dir'])
193 | ->append($pathsByType['file']);
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/Exception/InvalidConfigurationException.php:
--------------------------------------------------------------------------------
1 | filesystem = new Filesystem();
33 | $this->initDirectories();
34 | }
35 |
36 | public function dumpFile($dirType, string $filename, $content): string
37 | {
38 | $dir = $this->dirs->offsetGet($dirType);
39 | if (empty($dir)) {
40 | throw new ProxyGenerationException('Illegal directory type: ' . $dirType);
41 | }
42 |
43 | $filePath = $dir . $filename;
44 | $this->filesystem->dumpFile($filePath, $content);
45 |
46 | return $filePath;
47 | }
48 |
49 | private function initDirectories(): void
50 | {
51 | try {
52 | $this->dirs = new ArrayIterator([
53 | static::META => $this->workDir . DIRECTORY_SEPARATOR . static::META . DIRECTORY_SEPARATOR,
54 | static::PROXY => $this->workDir . DIRECTORY_SEPARATOR . static::PROXY . DIRECTORY_SEPARATOR,
55 | static::ACCESSOR => $this->workDir . DIRECTORY_SEPARATOR . static::PROXY . DIRECTORY_SEPARATOR . static::ACCESSOR . DIRECTORY_SEPARATOR,
56 | ]);
57 | $this->filesystem->mkdir($this->dirs);
58 | } catch (IOException $e) {
59 | if (str_ends_with($e->getMessage(), 'File exists')) {
60 | return;
61 | }
62 |
63 | throw $e;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Meta/ClassMetadata.php:
--------------------------------------------------------------------------------
1 | classname = $classname;
31 | $this->accessorClassname = $accessorClassname;
32 | $this->project = $project;
33 | $this->updateTime = new DateTime();
34 | }
35 |
36 | public function addMethod(AccessorMethodInterface $method): static
37 | {
38 | $this->methods[] = $method;
39 |
40 | return $this;
41 | }
42 |
43 | public function jsonSerialize(): array
44 | {
45 | $json = [];
46 | foreach ($this as $key => $value) {
47 | if ($value instanceof DateTime) {
48 | /* @var DateTime $value */
49 | $json[$key] = $value->format('Y-m-d H:i:s');
50 | } else {
51 | $json[$key] = $value;
52 | }
53 | }
54 |
55 | return $json;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/AbstractAttributeHandler.php:
--------------------------------------------------------------------------------
1 | processNode($node);
25 | }
26 | }
27 |
28 | protected function processNode(Node $node): void
29 | {
30 | if ($node instanceof Attribute) {
31 | $this->processAttribute($node);
32 | } elseif ($node instanceof Property) {
33 | $this->processProperty($node);
34 | }
35 | }
36 |
37 | protected function processProperty(Property $property): void
38 | {
39 | $nodeFinder = new NodeFinder();
40 | /** @var Attribute[] $attributes */
41 | $attributes = $nodeFinder->findInstanceOf($property->attrGroups, Attribute::class);
42 | foreach ($attributes as $attribute) {
43 | $this->processAttribute($attribute, $property);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/AttributeHandlerInterface.php:
--------------------------------------------------------------------------------
1 | parameterHandlers[$handler] = new $handler();
42 | }
43 | }
44 |
45 | public function processAttribute(Attribute $attribute, ?Property $property = null): void
46 | {
47 | if ($attribute->name->toString() != AttributeData::class || $property != null) {
48 | return;
49 | }
50 |
51 | $this->isPending = true;
52 | $this->processAttributeArgs($attribute->args);
53 | }
54 |
55 | public function isPending(): bool
56 | {
57 | return $this->isPending;
58 | }
59 |
60 | public function getParameterHandler(string $handlerClassname): ParameterHandlerInterface
61 | {
62 | return $this->parameterHandlers[$handlerClassname];
63 | }
64 |
65 | private function processAttributeArgs(array $args): void
66 | {
67 | foreach ($args as $arg) {
68 | $this->processArgWithHandlers($arg);
69 | }
70 | }
71 |
72 | private function processArgWithHandlers(Arg $arg): void
73 | {
74 | foreach ($this->parameterHandlers as $parameterHandler) {
75 | $parameterHandler->processParameter($arg);
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/DefaultNullHandler.php:
--------------------------------------------------------------------------------
1 | name->toString() != DefaultNull::class) {
27 | return;
28 | }
29 |
30 | if ($property === null) {
31 | $this->isDefaultNull = true;
32 | return;
33 | }
34 |
35 | foreach ($property->props as $prop) {
36 | $this->propertyIsDefaultNull[$prop->name->name] = true;
37 | }
38 | }
39 |
40 | public function isDefaultNull(string $propertyName): bool
41 | {
42 | if ($this->isDefaultNull) {
43 | return true;
44 | }
45 |
46 | return isset($this->propertyIsDefaultNull[$propertyName]) && $this->propertyIsDefaultNull[$propertyName];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/OverlookHandler.php:
--------------------------------------------------------------------------------
1 | name->toString() != OverlookAttribute::class || $property === null) {
25 | return;
26 | }
27 |
28 | foreach ($property->props as $prop) {
29 | $this->propertyIsOverlook[$prop->name->name] = true;
30 | }
31 | }
32 |
33 | public function isOverlook(Property $property): bool
34 | {
35 | foreach ($property->props as $prop) {
36 | if (isset($this->propertyIsOverlook[$prop->name->name])
37 | && $this->propertyIsOverlook[$prop->name->name]
38 | ) {
39 | return true;
40 | }
41 | }
42 |
43 | return false;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/Parameter/AbstractParameterHandler.php:
--------------------------------------------------------------------------------
1 | name->name;
23 | $parameterValue = $parameter->value;
24 |
25 | if (! $this->shouldProcess($parameterName, $parameterValue)
26 | || ! ($parameterValue instanceof ClassConstFetch)
27 | || ! ($parameterValue->name instanceof Identifier)) {
28 | return;
29 | }
30 |
31 | $this->config = $this->getConfigValueFromClassConstants($parameterValue->name->name) ?? $this->config;
32 | }
33 |
34 | /**
35 | * Determines whether the given parameter should be processed.
36 | *
37 | * This method should be implemented by subclasses to provide specific
38 | * logic for determining whether a parameter should be processed based
39 | * on its name and value.
40 | *
41 | * @param string $parameterName the name of the parameter
42 | * @param Expr $parameterValue the value of the parameter
43 | * @return bool returns true if the parameter should be processed, false otherwise
44 | */
45 | abstract protected function shouldProcess(string $parameterName, Expr $parameterValue): bool;
46 |
47 | /**
48 | * Returns the class name of the specific parameter handler.
49 | *
50 | * This method should be implemented by subclasses to provide the specific
51 | * class name for the parameter handler. This class name is used in the
52 | * `getConfigValueFromClassConstants` method to fetch the configuration value.
53 | *
54 | * @return string the class name of the specific parameter handler
55 | */
56 | abstract protected function getClassName(): string;
57 |
58 | private function getConfigValueFromClassConstants(string $name): mixed
59 | {
60 | return defined($this->getClassName() . '::' . $name)
61 | ? constant($this->getClassName() . '::' . $name)
62 | : null;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/Parameter/AccessorTypeHandler.php:
--------------------------------------------------------------------------------
1 | config == AccessorTypeMap::BOTH || $this->config == AccessorTypeMap::GETTER) {
24 | return true;
25 | }
26 |
27 | return false;
28 | }
29 |
30 | public function shouldGenerateSetter(): bool
31 | {
32 | if ($this->config == AccessorTypeMap::BOTH || $this->config == AccessorTypeMap::SETTER) {
33 | return true;
34 | }
35 |
36 | return false;
37 | }
38 |
39 | protected function shouldProcess(string $parameterName, Expr $parameterValue): bool
40 | {
41 | return $parameterName == 'accessorType';
42 | }
43 |
44 | protected function getClassName(): string
45 | {
46 | return AccessorTypeMap::class;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/Parameter/NamingConventionHandler.php:
--------------------------------------------------------------------------------
1 | config) {
24 | NamingConventionMap::LOWER_CAMEL_CASE => $this->camelize($fieldName, true),
25 | NamingConventionMap::UPPER_CAMEL_CASE => $this->camelize($fieldName),
26 | default => ucfirst($fieldName),
27 | };
28 | }
29 |
30 | protected function shouldProcess(string $parameterName, Expr $parameterValue): bool
31 | {
32 | return $parameterName == 'namingConvention';
33 | }
34 |
35 | protected function getClassName(): string
36 | {
37 | return NamingConventionMap::class;
38 | }
39 |
40 | private function camelize($str, $low = false): string
41 | {
42 | $str = str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($str))));
43 |
44 | return $low ? lcfirst($str) : $str;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Processor/Attribute/Parameter/ParameterHandlerInterface.php:
--------------------------------------------------------------------------------
1 | config) {
27 | PrefixConvention::GET_SET => $this->buildGetSetPrefix($accessorMethodType),
28 | PrefixConvention::BOOLEAN_IS => $this->buildBooleanIsPrefix($fieldType, $accessorMethodType),
29 | default => '',
30 | };
31 | }
32 |
33 | protected function shouldProcess(string $parameterName, Expr $parameterValue): bool
34 | {
35 | return $parameterName == 'prefixConvention';
36 | }
37 |
38 | protected function getClassName(): string
39 | {
40 | return PrefixConvention::class;
41 | }
42 |
43 | private function buildGetSetPrefix(string $accessorMethodType): string
44 | {
45 | return $accessorMethodType === AccessorMethodType::GETTER ? 'get' : 'set';
46 | }
47 |
48 | private function buildBooleanIsPrefix(array $fieldType, string $accessorMethodType): string
49 | {
50 | return in_array('bool', $fieldType) && $accessorMethodType == AccessorMethodType::GETTER
51 | ? 'is'
52 | : $this->buildGetSetPrefix($accessorMethodType);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Processor/AttributeProcessor.php:
--------------------------------------------------------------------------------
1 | nodeFinder = new NodeFinder();
45 | $this->initHandlers();
46 | $this->parse($node);
47 | }
48 |
49 | public function shouldProcess(): bool
50 | {
51 | return $this->getAttributeHandler(DataHandler::class)->isPending();
52 | }
53 |
54 | /**
55 | * Builds a method name from the given field name, field type, and accessor method type.
56 | *
57 | * @param string $fieldName the name of the field
58 | * @param array $fieldType the type of the field
59 | * @param string $accessorMethodType the type of the accessor method
60 | *
61 | * @return string the built method name
62 | */
63 | public function buildMethodNameFromField(string $fieldName, array $fieldType, string $accessorMethodType): string
64 | {
65 | $dataHandler = $this->getAttributeHandler(DataHandler::class);
66 | $prefix = $dataHandler->getParameterHandler(PrefixConventionHandler::class)->buildPrefix($fieldType, $accessorMethodType);
67 | $name = $dataHandler->getParameterHandler(NamingConventionHandler::class)->buildName($fieldName);
68 |
69 | return $prefix . $name;
70 | }
71 |
72 | /**
73 | * Whether to generate accessor methods for the property.
74 | */
75 | public function isIgnored(Property $property): bool
76 | {
77 | return $this->getAttributeHandler(OverlookHandler::class)->isOverlook($property);
78 | }
79 |
80 | public function isDefaultNull(string $fieldName): bool
81 | {
82 | return $this->getAttributeHandler(DefaultNullHandler::class)->isDefaultNull($fieldName);
83 | }
84 |
85 | public function shouldGenMethod(string $accessorMethodType): bool
86 | {
87 | return match ($accessorMethodType) {
88 | AccessorMethodType::GETTER => $this->getAttributeHandler(DataHandler::class)->getParameterHandler(AccessorTypeHandler::class)->shouldGenerateGetter(),
89 | AccessorMethodType::SETTER => $this->getAttributeHandler(DataHandler::class)->getParameterHandler(AccessorTypeHandler::class)->shouldGenerateSetter(),
90 | default => false,
91 | };
92 | }
93 |
94 | private function initHandlers(): void
95 | {
96 | foreach (self::$registeredHandlers as $handlerClassname) {
97 | $this->attributeHandlers[$handlerClassname] = new $handlerClassname();
98 | }
99 | }
100 |
101 | /**
102 | * Parses the given node.
103 | *
104 | * This method is responsible for parsing the given node. It does this by calling the parseAttributes method twice.
105 | * The first call processes the attributes of the node, and the second call processes the properties of the node.
106 | *
107 | * @param Node $node the node to parse
108 | */
109 | private function parse(Node $node): void
110 | {
111 | $this->parseAttributes($node->attrGroups, Attribute::class);
112 | $this->parseAttributes($node, Property::class);
113 | }
114 |
115 | private function parseAttributes(Node|array $nodes, string $class): void
116 | {
117 | $foundNodes = $this->nodeFinder->findInstanceOf($nodes, $class);
118 | foreach ($this->attributeHandlers as $attributeHandler) {
119 | $attributeHandler->processAttributes($foundNodes);
120 | }
121 | }
122 |
123 | private function getAttributeHandler(string $handlerClassname): ?AttributeHandlerInterface
124 | {
125 | return $this->attributeHandlers[$handlerClassname] ?? null;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Processor/ClassProcessor.php:
--------------------------------------------------------------------------------
1 | nodeFinder = new NodeFinder();
55 | }
56 |
57 | public function enterNode(Node $node)
58 | {
59 | if (! $node instanceof Class_ || empty($node->attrGroups)) {
60 | return null;
61 | }
62 |
63 | $this->classname = '\\' . $node->namespacedName->toString();
64 | $attributeProcessor = new AttributeProcessor($node);
65 | if (! $attributeProcessor->shouldProcess() || ! $this->parsePropertiesAndMethods($node)) {
66 | return null;
67 | }
68 |
69 | $this->genAccessors($node->namespacedName->toString(), $attributeProcessor);
70 |
71 | if (empty($this->accessorMethods) || ! $this->genMethod) {
72 | return null;
73 | }
74 |
75 | $this->genCompleted = true;
76 |
77 | return $this->rebuildClass($node);
78 | }
79 |
80 | public function afterTraverse(array $nodes)
81 | {
82 | if (! $this->genCompleted) {
83 | return null;
84 | }
85 |
86 | return $this->addIncludeForTraitAccessor($nodes);
87 | }
88 |
89 | public function isGenCompleted(): bool
90 | {
91 | return $this->genCompleted;
92 | }
93 |
94 | public function getClassname(): string
95 | {
96 | return $this->classname;
97 | }
98 |
99 | public function getAccessorMethods(): array
100 | {
101 | return $this->accessorMethods;
102 | }
103 |
104 | public function getTraitAccessor(): TraitAccessor
105 | {
106 | return $this->traitAccessor;
107 | }
108 |
109 | /**
110 | * Parse properties and methods from class node.
111 | */
112 | private function parsePropertiesAndMethods(Class_ $node): bool
113 | {
114 | $this->originalProperties = $this->nodeFinder->findInstanceOf($node, Property::class);
115 | $originalClassMethods = $this->nodeFinder->findInstanceOf($node, ClassMethod::class);
116 |
117 | /** @var ClassMethod[] $originalClassMethods */
118 | foreach ($originalClassMethods as $method) {
119 | $this->originalMethods[] = $method->name->name;
120 | if ($method->name->name == '__construct') {
121 | $this->addPromotedParamsToProperties($method);
122 | }
123 | }
124 |
125 | return ! empty($this->originalProperties);
126 | }
127 |
128 | private function addPromotedParamsToProperties(ClassMethod $method): void
129 | {
130 | foreach ($method->getParams() as $param) {
131 | if ($param->flags == 0) {
132 | continue;
133 | }
134 |
135 | $propertyBuilder = new \PhpParser\Builder\Property($param->var->name);
136 | $propertyBuilder->setDefault($param->default);
137 | $property = $propertyBuilder->getNode();
138 | $property->flags = $param->flags;
139 | $property->type = $param->type;
140 | $this->originalProperties[] = $property;
141 | }
142 | }
143 |
144 | /**
145 | * Generate accessor methods and trait accessor.
146 | */
147 | private function genAccessors(
148 | string $classname,
149 | AttributeProcessor $attributeProcessor,
150 | ): void {
151 | foreach ($this->originalProperties as $property) {
152 | if ($attributeProcessor->isIgnored($property)) {
153 | continue;
154 | }
155 |
156 | $this->accessorMethods = array_merge(
157 | $this->accessorMethods,
158 | $this->createMethodsFromProperties($classname, $property, $attributeProcessor)
159 | );
160 | }
161 |
162 | $this->genTraitAccessor();
163 | }
164 |
165 | private function createMethodsFromProperties(
166 | string $classname,
167 | Property $property,
168 | AttributeProcessor $attributeProcessor
169 | ): array {
170 | $methods = [];
171 | foreach ($property->props as $prop) {
172 | $methods = array_merge(
173 | $methods,
174 | MethodFactory::createFromProperty(
175 | classname: $classname,
176 | property: $prop,
177 | propertyType: $property->type,
178 | propertyDocComment: $this->commentProcessor->buildDocNode($property),
179 | attributeProcessor: $attributeProcessor
180 | )
181 | );
182 | }
183 |
184 | return $methods;
185 | }
186 |
187 | private function genTraitAccessor(): void
188 | {
189 | $this->traitAccessor = new TraitAccessor($this->classname);
190 | foreach ($this->accessorMethods as $accessorMethod) {
191 | if (in_array($accessorMethod->getMethodName(), $this->originalMethods)) {
192 | continue;
193 | }
194 |
195 | $this->traitAccessor->addAccessorMethod($accessorMethod);
196 | }
197 | }
198 |
199 | private function rebuildClass(Class_ $node): Class_
200 | {
201 | $builder = new BuilderFactory();
202 | $class = $builder
203 | ->class($node->name->toString())
204 | ->addStmt($builder->useTrait('\\' . $this->traitAccessor->getClassName()));
205 |
206 | $node->extends && $class->extend($node->extends);
207 | $node->isAbstract() && $class->makeAbstract();
208 |
209 | $this->addAttributes($class, $node);
210 | $this->addImplements($class, $node);
211 |
212 | $newNode = $class->getNode();
213 | $newNode->stmts = array_merge($newNode->stmts, $node->stmts);
214 |
215 | return $newNode;
216 | }
217 |
218 | private function addAttributes(\PhpParser\Builder\Class_ $class, Class_ $node): void
219 | {
220 | foreach ($node->attrGroups as $attrGroup) {
221 | if ($this->shouldIgnoreAttribute($attrGroup)) {
222 | continue;
223 | }
224 | $class->addAttribute($attrGroup);
225 | }
226 | }
227 |
228 | private function shouldIgnoreAttribute(AttributeGroup $attrGroup): bool
229 | {
230 | foreach ($attrGroup->attrs as $attr) {
231 | if ($attr->name->toString() == Data::class) {
232 | return true;
233 | }
234 | }
235 | return false;
236 | }
237 |
238 | private function addImplements(\PhpParser\Builder\Class_ $class, Class_ $node): void
239 | {
240 | foreach ($node->implements as $implement) {
241 | $class->implement($implement);
242 | }
243 | }
244 |
245 | /**
246 | * Add include statement for trait accessor.
247 | */
248 | private function addIncludeForTraitAccessor(array $nodes): array
249 | {
250 | $namespace = (new NodeFinder())->findFirstInstanceOf($nodes, Namespace_::class);
251 | $includePath = File::ACCESSOR . DIRECTORY_SEPARATOR . $this->traitAccessor->getClassName() . '.php';
252 | $include = new Expression(new Include_(new String_($includePath), Include_::TYPE_INCLUDE_ONCE));
253 | array_unshift($namespace->stmts, $include);
254 |
255 | return $nodes;
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/Processor/CommentProcessor.php:
--------------------------------------------------------------------------------
1 | 'null',
31 | 'bool' => 'bool',
32 | 'boolean' => 'bool',
33 | 'string' => 'string',
34 | 'int' => 'int',
35 | 'integer' => 'int',
36 | 'float' => 'float',
37 | 'double' => 'float',
38 | 'array' => 'array',
39 | 'object' => 'object',
40 | 'callable' => 'callable',
41 | 'resource' => 'resource',
42 | 'mixed' => 'mixed',
43 | 'iterable' => 'iterable',
44 | ];
45 |
46 | private Lexer $phpDocLexer;
47 |
48 | private PhpDocParser $phpDocParser;
49 |
50 | /** @var PhpDocNode[] */
51 | private array $docNodes = [];
52 |
53 | public function __construct(
54 | private NameContext $nameContext,
55 | ) {
56 | $this->phpDocLexer = new Lexer();
57 | $constantExpressionParser = new ConstExprParser();
58 | $this->phpDocParser = new PhpDocParser(new TypeParser($constantExpressionParser), $constantExpressionParser);
59 | }
60 |
61 | public function buildDocNode(Node $node): ?PhpDocNode
62 | {
63 | if (isset($this->docNodes[spl_object_id($node)])) {
64 | return $this->docNodes[spl_object_id($node)];
65 | }
66 |
67 | if (empty($docComment = $node->getDocComment())) {
68 | return null;
69 | }
70 |
71 | $tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment->getText()));
72 | $ast = $this->phpDocParser->parse($tokens);
73 | foreach ($ast->getVarTagValues() as $varTagValueNode) {
74 | $this->resolveTypeNode($varTagValueNode->type);
75 | }
76 |
77 | $node->setDocComment(new Doc((string) $ast));
78 | $this->docNodes[spl_object_id($node)] = $ast;
79 |
80 | return $ast;
81 | }
82 |
83 | protected function resolveTypeNode(TypeNode $typeNode): void
84 | {
85 | if ($typeNode instanceof IdentifierTypeNode) {
86 | $typeNode->name = $this->resolveTypeName($typeNode);
87 |
88 | return;
89 | }
90 |
91 | if ($typeNode instanceof ArrayTypeNode) {
92 | $this->resolveTypeNode($typeNode->type);
93 | } elseif ($typeNode instanceof GenericTypeNode) {
94 | foreach ($typeNode->genericTypes as $genericType) {
95 | $this->resolveTypeNode($genericType);
96 | }
97 | } elseif ($typeNode instanceof UnionTypeNode) {
98 | foreach ($typeNode->types as $type) {
99 | $this->resolveTypeNode($type);
100 | }
101 | } elseif ($typeNode instanceof ArrayShapeNode) {
102 | foreach ($typeNode->items as $item) {
103 | $this->resolveTypeNode($item->valueType);
104 | }
105 | }
106 | }
107 |
108 | protected function resolveTypeName(IdentifierTypeNode $node): string
109 | {
110 | if (isset(self::PRIMITIVE_TYPES[$node->name])) {
111 | return $node->name;
112 | }
113 |
114 | $resolvedName = $this->nameContext->getResolvedName(new Node\Name($node->name), Node\Stmt\Use_::TYPE_NORMAL);
115 | if (empty($resolvedName)) {
116 | return $node->name;
117 | }
118 |
119 | return $resolvedName->toCodeString();
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/Processor/Method/AbstractAccessorMethod.php:
--------------------------------------------------------------------------------
1 | $value) {
45 | $json[$key] = $value;
46 | }
47 |
48 | if (isset($json['fieldMetadata'])) {
49 | unset($json['fieldMetadata']);
50 | }
51 | if (isset($json['generators'])) {
52 | unset($json['generators']);
53 | }
54 | if (isset($json['body'])) {
55 | unset($json['body']);
56 | }
57 |
58 | return $json;
59 | }
60 |
61 | public function setReturnTypes(array $returnTypes): static
62 | {
63 | $this->returnTypes = $returnTypes;
64 | return $this;
65 | }
66 |
67 | public function setBody(array $body): static
68 | {
69 | $this->body = $body;
70 | return $this;
71 | }
72 |
73 | public function addGenerator(GeneratorInterface $generator): void
74 | {
75 | $this->generators[] = $generator;
76 | }
77 |
78 | public function generate(): void
79 | {
80 | foreach ($this->generators as $generator) {
81 | $generator->generate($this->fieldMetadata, $this);
82 | }
83 | }
84 |
85 | public function setFieldMetadata(FieldMetadata $fieldMetadata): static
86 | {
87 | $this->fieldMetadata = $fieldMetadata;
88 | $this->fieldName = $fieldMetadata->getFieldName();
89 | $this->className = $fieldMetadata->getClassname();
90 | $this->fieldTypes = $fieldMetadata->getFieldTypes();
91 |
92 | return $this;
93 | }
94 |
95 | public function setMethodComment(string $methodComment): static
96 | {
97 | $this->methodComment = $methodComment;
98 | return $this;
99 | }
100 |
101 | public function setFieldTypes(array $fieldTypes): static
102 | {
103 | $this->fieldTypes = $fieldTypes;
104 | return $this;
105 | }
106 |
107 | public function setMethodName(string $methodName): static
108 | {
109 | $this->methodName = $methodName;
110 | return $this;
111 | }
112 |
113 | public function getClassName(): string
114 | {
115 | return $this->className;
116 | }
117 |
118 | public function getFieldName(): string
119 | {
120 | return $this->fieldName;
121 | }
122 |
123 | public function getFieldTypes(): array
124 | {
125 | return $this->fieldTypes;
126 | }
127 |
128 | public function getMethodName(): string
129 | {
130 | return $this->methodName;
131 | }
132 |
133 | public function getName(): string
134 | {
135 | return $this->name;
136 | }
137 |
138 | public function getMethodComment(): string
139 | {
140 | return $this->methodComment;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Processor/Method/AccessorMethodInterface.php:
--------------------------------------------------------------------------------
1 | classname;
27 | }
28 |
29 | public function setClassname(string $classname): FieldMetadata
30 | {
31 | $this->classname = $classname;
32 | return $this;
33 | }
34 |
35 | public function getFieldName(): string
36 | {
37 | return $this->fieldName;
38 | }
39 |
40 | public function setFieldName(string $fieldName): FieldMetadata
41 | {
42 | $this->fieldName = $fieldName;
43 | return $this;
44 | }
45 |
46 | public function getFieldTypes(): array
47 | {
48 | return $this->fieldTypes;
49 | }
50 |
51 | public function addFieldType(string $fieldType): FieldMetadata
52 | {
53 | $this->fieldTypes[] = $fieldType;
54 | return $this;
55 | }
56 |
57 | public function setFieldTypes(array $fieldTypes): FieldMetadata
58 | {
59 | $this->fieldTypes = $fieldTypes;
60 | return $this;
61 | }
62 |
63 | public function getComment(): ?PhpDocNode
64 | {
65 | return $this->comment;
66 | }
67 |
68 | public function setComment(?PhpDocNode $comment): FieldMetadata
69 | {
70 | $this->comment = $comment;
71 | return $this;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Processor/Method/FieldMetadataBuilder.php:
--------------------------------------------------------------------------------
1 | fieldMetadata = new FieldMetadata();
31 | }
32 |
33 | public function build(): FieldMetadata
34 | {
35 | $this->fieldMetadata->setClassname($this->classname);
36 | $this->fieldMetadata->setFieldName($this->property->name->toString());
37 | $this->fieldMetadata->setComment($this->propertyDocComment);
38 | $this->buildFieldTypes($this->propertyType);
39 |
40 | return $this->fieldMetadata;
41 | }
42 |
43 | private function buildFieldTypes($propertyType): void
44 | {
45 | if ($propertyType == null) {
46 | return;
47 | }
48 |
49 | if ($propertyType instanceof Identifier) {
50 | $this->fieldMetadata->addFieldType($propertyType->name);
51 |
52 | return;
53 | }
54 |
55 | if ($propertyType instanceof NullableType) {
56 | $this->fieldMetadata->addFieldType('null');
57 | $this->buildFieldTypes($propertyType->type);
58 |
59 | return;
60 | }
61 |
62 | if ($propertyType instanceof Name) {
63 | $this->fieldMetadata->addFieldType('\\' . $propertyType->toString());
64 |
65 | return;
66 | }
67 |
68 | if ($propertyType instanceof IntersectionType) {
69 | foreach ($propertyType->types as $type) {
70 | $this->buildFieldTypes($type);
71 | }
72 |
73 | return;
74 | }
75 |
76 | if ($propertyType instanceof UnionType) {
77 | foreach ($propertyType->types as $type) {
78 | $this->buildFieldTypes($type);
79 | }
80 |
81 | return;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/GeneratorInterface.php:
--------------------------------------------------------------------------------
1 | propertyFetch($builder->var('this'), $fieldMetadata->getFieldName());
34 | $returnBody = $this->attributeProcessor->isDefaultNull($fieldMetadata->getFieldName())
35 | ? new Coalesce($propertyFetch, $builder->constFetch('null'))
36 | : $propertyFetch;
37 | $accessorMethod->setBody([new Return_($returnBody)]);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/Getter/MethodCommentGenerator.php:
--------------------------------------------------------------------------------
1 | getComment();
23 | if (empty($comment) || empty($varTagValues = $comment->getVarTagValues())) {
24 | return;
25 | }
26 |
27 | $accessorMethod->setMethodComment((string) new PhpDocNode([
28 | new PhpDocTagNode('@return', new ReturnTagValueNode($varTagValues[0]->type, '')),
29 | ]));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/Getter/ReturnTypeGenerator.php:
--------------------------------------------------------------------------------
1 | getFieldTypes())) {
30 | $types = ['mixed'];
31 | } else {
32 | $types = $fieldMetadata->getFieldTypes();
33 | if ($this->attributeProcessor->isDefaultNull($fieldMetadata->getFieldName())
34 | && ! in_array('null', $types)
35 | && ! in_array('mixed', $types)) {
36 | $types[] = 'null';
37 | }
38 | }
39 |
40 | $accessorMethod->setReturnTypes($types);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/MethodNameGenerator.php:
--------------------------------------------------------------------------------
1 | attributeProcessor->buildMethodNameFromField(
25 | fieldName: $fieldMetadata->getFieldName(),
26 | fieldType: $fieldMetadata->getFieldTypes(),
27 | accessorMethodType: $accessorMethod->getName()
28 | );
29 | $accessorMethod->setMethodName($methodName);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/Setter/ParameterTypeGenerator.php:
--------------------------------------------------------------------------------
1 | getFieldTypes() ?: ['mixed'];
24 | $accessorMethod->setParameterTypes($types);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/Setter/SetterBodyGenerator.php:
--------------------------------------------------------------------------------
1 | getFieldName();
24 | $builder = new BuilderFactory();
25 | $thisField = $builder->propertyFetch($builder->var('this'), $fieldName);
26 | $fieldVar = $builder->var($fieldName);
27 |
28 | $accessorMethod->setBody([
29 | new Expression(new Assign($thisField, $fieldVar)),
30 | new Return_($builder->var('this')),
31 | ]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Processor/Method/Generator/Setter/SetterReturnTypeGenerator.php:
--------------------------------------------------------------------------------
1 | setReturnTypes(['static']);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Processor/Method/GetterMethod.php:
--------------------------------------------------------------------------------
1 | method($this->methodName)
22 | ->makePublic()
23 | ->setReturnType(implode('|', $this->returnTypes))
24 | ->setDocComment($this->methodComment)
25 | ->addStmts($this->body);
26 |
27 | return $method->getNode();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Processor/Method/SetterMethod.php:
--------------------------------------------------------------------------------
1 | parameterTypes = $parameterTypes;
24 | return $this;
25 | }
26 |
27 | public function buildMethod(): ClassMethod
28 | {
29 | $builder = new BuilderFactory();
30 |
31 | $param = $builder->param($this->fieldMetadata->getFieldName())
32 | ->setType(implode('|', $this->parameterTypes));
33 |
34 | $method = $builder->method($this->methodName)
35 | ->makePublic()
36 | ->addParam($param)
37 | ->setReturnType(implode('|', $this->returnTypes))
38 | ->addStmts($this->body);
39 |
40 | return $method->getNode();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Processor/MethodFactory.php:
--------------------------------------------------------------------------------
1 | [
41 | 'processor' => GetterMethod::class,
42 | 'generators' => [
43 | MethodNameGenerator::class,
44 | MethodCommentGenerator::class,
45 | ReturnTypeGenerator::class,
46 | GetterBodyGenerator::class,
47 | ],
48 | ],
49 | AccessorMethodType::SETTER => [
50 | 'processor' => SetterMethod::class,
51 | 'generators' => [
52 | MethodNameGenerator::class,
53 | ParameterTypeGenerator::class,
54 | SetterReturnTypeGenerator::class,
55 | SetterBodyGenerator::class,
56 | ],
57 | ],
58 | ];
59 |
60 | /**
61 | * @return AccessorMethodInterface[]
62 | */
63 | public static function createFromProperty(
64 | string $classname,
65 | PropertyProperty $property,
66 | null|Identifier|Name|ComplexType $propertyType,
67 | null|PhpDocNode $propertyDocComment,
68 | AttributeProcessor $attributeProcessor
69 | ): array {
70 | $accessorMethods = [];
71 | $builder = new FieldMetadataBuilder($classname, $property, $propertyType, $propertyDocComment);
72 | $fieldMetadata = $builder->build();
73 | foreach (self::$generatorConfig as $accessorMethodType => $generators) {
74 | if (! $attributeProcessor->shouldGenMethod($accessorMethodType)) {
75 | continue;
76 | }
77 | /** @var AbstractAccessorMethod $processor */
78 | $processor = new $generators['processor']();
79 | $processor->setFieldMetadata($fieldMetadata);
80 | foreach ($generators['generators'] as $generator) {
81 | $processor->addGenerator(new $generator($attributeProcessor));
82 | }
83 | $processor->generate();
84 | $accessorMethods[] = $processor;
85 | }
86 |
87 | return $accessorMethods;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Processor/TraitAccessor.php:
--------------------------------------------------------------------------------
1 | className = '_Proxy' . str_replace('\\', '_', $classShortName) . 'Accessor';
25 | }
26 |
27 | public function addAccessorMethod(AccessorMethodInterface $abstractMethod): static
28 | {
29 | $this->accessorMethods[] = $abstractMethod;
30 |
31 | return $this;
32 | }
33 |
34 | public function getClassName(): string
35 | {
36 | return $this->className;
37 | }
38 |
39 | public function buildTrait(): Trait_
40 | {
41 | $builder = new BuilderFactory();
42 | $trait = $builder->trait($this->className);
43 | foreach ($this->accessorMethods as $accessorMethod) {
44 | $trait->addStmt($accessorMethod->buildMethod());
45 | }
46 |
47 | return $trait->getNode();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Runner.php:
--------------------------------------------------------------------------------
1 | file = new File($dir);
36 | }
37 |
38 | public function getGeneratedFiles(): array
39 | {
40 | return $this->generatedFiles;
41 | }
42 |
43 | public function generate(): void
44 | {
45 | if (! $this->genProxy && ! $this->genMeta) {
46 | return;
47 | }
48 | foreach ($this->finder as $value) {
49 | $this->generateFile($value);
50 | }
51 | }
52 |
53 | private function generateFile(SplFileInfo $fileInfo): void
54 | {
55 | $source = file_get_contents($fileInfo->getRealPath());
56 | $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
57 | $stmts = $parser->parse($source);
58 |
59 | $traverser = new NodeTraverser();
60 | $nameResolver = new NameResolver();
61 | $traverser->addVisitor($nameResolver);
62 | $stmts = $traverser->traverse($stmts);
63 |
64 | $traverser = new NodeTraverser();
65 | $classProcessor = new ClassProcessor($this->genProxy, new CommentProcessor($nameResolver->getNameContext()));
66 | $traverser->addVisitor($classProcessor);
67 | $ast = $traverser->traverse($stmts);
68 |
69 | $classProcessor->isGenCompleted() && $this->generateProxy($classProcessor, $ast);
70 | $this->generateMetadata($classProcessor);
71 | }
72 |
73 | /**
74 | * @param Node[] $stmts
75 | */
76 | private function generateProxy(ClassProcessor $classProcessor, array $stmts): void
77 | {
78 | if (! $this->genProxy) {
79 | return;
80 | }
81 |
82 | $this->generatedFiles[] = $this->file->dumpFile(
83 | File::PROXY,
84 | $this->getFileName($classProcessor->getClassname()) . '.php',
85 | $this->getPrintFileContent($stmts)
86 | );
87 | $this->generatedFiles[] = $this->file->dumpFile(
88 | File::ACCESSOR,
89 | $this->getFileName($classProcessor->getTraitAccessor()->getClassname()) . '.php',
90 | $this->getPrintFileContent([$classProcessor->getTraitAccessor()->buildTrait()])
91 | );
92 | }
93 |
94 | private function generateMetadata(ClassProcessor $classProcessor): void
95 | {
96 | if (! $this->genMeta || empty($accessorMethods = $classProcessor->getAccessorMethods())) {
97 | return;
98 | }
99 |
100 | $classMetadata = new ClassMetadata('', $classProcessor->getClassname(), $classProcessor->getTraitAccessor()->getClassName());
101 | array_walk($accessorMethods, fn ($method) => $classMetadata->addMethod($method));
102 | $this->generatedFiles[] = $this->file->dumpFile(
103 | File::META,
104 | $this->getFileName($classProcessor->getClassname()) . '.json',
105 | json_encode($classMetadata)
106 | );
107 | }
108 |
109 | private function getFileName($classname): string
110 | {
111 | return str_replace('\\', '@', $classname);
112 | }
113 |
114 | private function getPrintFileContent(array $stmts): string
115 | {
116 | return (new Standard())->prettyPrintFile($stmts);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/tests/Cases/AccessorTypeTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($generatedFiles);
45 | $proxyFile = $generatedFiles[1];
46 | $classMethods = GeneratorHelper::getMethods($proxyFile);
47 | $this->assertCount(1, $classMethods);
48 | $this->assertSame($method, $classMethods[$method]['name']);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/Cases/ApplicationTest.php:
--------------------------------------------------------------------------------
1 | 'generate',
25 | 'path' => [
26 | __ROOT__ . '/tests/Mock/Foo.php',
27 | ],
28 | '--gen-meta' => 'yes',
29 | '--dir' => __ROOT__ . DIRECTORY_SEPARATOR . '.php-accessor',
30 | ]);
31 | $app = new Application();
32 | $app->setAutoExit(false);
33 | $res = $app->run($input);
34 | $this->assertSame(0, $res);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Cases/ConstructorPromotionTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($generatedFiles);
38 | $proxyFile = $generatedFiles[1];
39 | $classMethods = GeneratorHelper::getMethods($proxyFile);
40 | $this->assertCount(count($methods), $classMethods);
41 | foreach ($methods as $method) {
42 | $this->assertArrayHasKey($method, $classMethods);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Cases/DefaultNullTest.php:
--------------------------------------------------------------------------------
1 | 'return $this->id;', 'getSex' => 'return $this->sex ?? null;'],
28 | ],
29 | [
30 | DefaultNullAll::class,
31 | ['getId' => 'return $this->id ?? null;', 'getSex' => 'return $this->sex ?? null;'],
32 | ],
33 | ];
34 | }
35 |
36 | /**
37 | * @dataProvider genProvider
38 | */
39 | public function testGen(string $classname, array $methods)
40 | {
41 | $generatedFiles = GeneratorHelper::genFromClass($classname);
42 | $this->assertNotEmpty($generatedFiles);
43 | $proxyFile = $generatedFiles[1];
44 | $methodInfo = GeneratorHelper::getMethods($proxyFile);
45 | foreach ($methods as $method => $body) {
46 | $this->assertArrayHasKey($method, $methodInfo);
47 | $this->assertSame($body, $methodInfo[$method]['body']);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/Cases/GetterMethodCommentTest.php:
--------------------------------------------------------------------------------
1 | null,
28 | 'getAge' => null,
29 | 'getId' => "/**\n * @return int\n */",
30 | 'getArray1' => "/**\n * @return \\PhpAccessor\\Test\\Mock\\FooSub[]\n */",
31 | 'getArray2' => "/**\n * @return \\PhpAccessor\\Test\\Mock\\FooSub[]\n */",
32 | 'getArray3' => "/**\n * @return string[]\n */",
33 | 'getArray4' => "/**\n * @return array\n */",
34 | 'getArray5' => "/**\n * @return array<\\PhpAccessor\\Test\\Mock\\FooSub>\n */",
35 | 'getArray6' => "/**\n * @return array\n */",
36 | 'getFoo' => "/**\n * @return int\n */",
37 | ],
38 | ],
39 | ];
40 | }
41 |
42 | /**
43 | * @dataProvider genProvider
44 | */
45 | public function testGen(string $classname, array $methods)
46 | {
47 | $generatedFiles = GeneratorHelper::genFromClass($classname);
48 | $this->assertNotEmpty($generatedFiles);
49 | $proxyFile = $generatedFiles[1];
50 | $methodInfo = GeneratorHelper::getMethods($proxyFile);
51 |
52 | foreach ($methods as $method => $comment) {
53 | $this->assertArrayHasKey($method, $methodInfo);
54 | $this->assertSame($comment, $methodInfo[$method]['comment']);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Cases/NamingConventionTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($generatedFiles);
48 | $proxyFile = $generatedFiles[1];
49 | $classMethods = GeneratorHelper::getMethods($proxyFile);
50 | $this->assertCount(count($methods), $classMethods);
51 | foreach ($methods as $method) {
52 | $this->assertArrayHasKey($method, $classMethods);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Cases/OverlookTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($generatedFiles);
38 | $proxyFile = $generatedFiles[1];
39 | $classMethods = GeneratorHelper::getMethods($proxyFile);
40 | $this->assertCount(count($methods), $classMethods);
41 | foreach ($methods as $method) {
42 | $this->assertArrayHasKey($method, $classMethods);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Cases/PrefixConventionTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty($generatedFiles);
43 | $proxyFile = $generatedFiles[1];
44 | $classMethods = GeneratorHelper::getMethods($proxyFile);
45 | $this->assertCount(count($methods), $classMethods);
46 | foreach ($methods as $method) {
47 | $this->assertArrayHasKey($method, $classMethods);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/Cases/RunnerTest.php:
--------------------------------------------------------------------------------
1 | [$classPath],
29 | 'dir' => __ROOT__ . DIRECTORY_SEPARATOR . '.php-accessor',
30 | 'gen-meta' => 'yes',
31 | 'gen-proxy' => 'yes',
32 | ],
33 | getcwd()
34 | );
35 | $finder = $resolver->getFinder();
36 | $finder = new ArrayIterator(iterator_to_array($finder));
37 | $runner = new Runner(
38 | finder: $finder,
39 | dir: $resolver->getDir(),
40 | genMeta: $resolver->getGenMeta(),
41 | genProxy: $resolver->getGenProxy(),
42 | );
43 | $runner->generate();
44 | $files = $runner->getGeneratedFiles();
45 |
46 | $this->assertCount(3, $files);
47 | $proxy = $files[0];
48 | include_once $proxy;
49 | $foo = new Foo();
50 | $this->assertIsObject($foo);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Mock/ConstructorPromotion.php:
--------------------------------------------------------------------------------
1 | setName([]);
42 | $this->setTestId2(213123);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Mock/FooInterface1.php:
--------------------------------------------------------------------------------
1 |
42 | */
43 | public array $array4;
44 |
45 | /**
46 | * @var array
47 | */
48 | public array $array5;
49 |
50 | /**
51 | * @var array
52 | */
53 | public array $array6;
54 |
55 | /**
56 | * @var null|array
57 | */
58 | public array $array7;
59 |
60 | /**
61 | * @var array{user: Foo, orders: array}
62 | */
63 | public array $array8;
64 |
65 | /**
66 | * @var int
67 | * @var string
68 | */
69 | public $foo;
70 | }
71 |
--------------------------------------------------------------------------------
/tests/Mock/GetterMethodComment.php:
--------------------------------------------------------------------------------
1 | */
33 | private array $array4;
34 |
35 | /** @var array */
36 | private array $array5;
37 |
38 | /** @var array */
39 | private array $array6;
40 |
41 | /**
42 | * @var int
43 | * @var string
44 | */
45 | private $foo;
46 |
47 | private FooSub $fooSub;
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Mock/NamingConventionLowerCamelCase.php:
--------------------------------------------------------------------------------
1 | getFileName();
27 | $resolver = new ConfigurationResolver(
28 | [
29 | 'path' => [$path],
30 | 'dir' => __ROOT__ . DIRECTORY_SEPARATOR . '.php-accessor',
31 | 'gen-meta' => 'yes',
32 | 'gen-proxy' => 'yes',
33 | ],
34 | getcwd()
35 | );
36 |
37 | $finder = $resolver->getFinder();
38 | $finder = new ArrayIterator(iterator_to_array($finder));
39 | $runner = new Runner(
40 | finder: $finder,
41 | dir: $resolver->getDir(),
42 | genMeta: $resolver->getGenMeta(),
43 | genProxy: $resolver->getGenProxy(),
44 | );
45 | $runner->generate();
46 |
47 | return $runner->getGeneratedFiles();
48 | }
49 |
50 | /**
51 | * @return array
52 | */
53 | public static function getMethods(string $classPath): array
54 | {
55 | $source = file_get_contents($classPath);
56 | $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
57 | $stmts = $parser->parse($source);
58 | $traverser = new NodeTraverser();
59 | $visitor = new class() extends NodeVisitorAbstract {
60 | public array $methods = [];
61 |
62 | private Standard $standard;
63 |
64 | public function __construct()
65 | {
66 | $this->standard = new Standard();
67 | }
68 |
69 | public function enterNode(Node $node): void
70 | {
71 | if (! $node instanceof Node\Stmt\ClassMethod) {
72 | return;
73 | }
74 |
75 | $this->methods[$node->name->name] = [
76 | 'name' => $node->name->name,
77 | 'body' => $this->standard->prettyPrint($node->getStmts()),
78 | 'comment' => $node->getDocComment()?->getText(),
79 | ];
80 | }
81 | };
82 | $traverser->addVisitor($visitor);
83 | $traverser->traverse($stmts);
84 |
85 | return $visitor->methods;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |