├── .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 |