├── .gitignore ├── src ├── model │ ├── Relation.php │ ├── option │ │ ├── Pk.php │ │ ├── Append.php │ │ ├── Hidden.php │ │ ├── Suffix.php │ │ ├── Visible.php │ │ ├── CreateTime.php │ │ ├── UpdateTime.php │ │ ├── Type.php │ │ └── Mapping.php │ └── relation │ │ ├── MorphTo.php │ │ ├── HasMany.php │ │ ├── HasOne.php │ │ ├── BelongsTo.php │ │ ├── MorphOne.php │ │ ├── MorphMany.php │ │ ├── MorphToMany.php │ │ ├── BelongsToMany.php │ │ ├── MorphByMany.php │ │ ├── HasOneThrough.php │ │ └── HasManyThrough.php ├── Inject.php ├── route │ ├── Group.php │ ├── Resource.php │ ├── Pattern.php │ ├── Model.php │ ├── Get.php │ ├── Put.php │ ├── Patch.php │ ├── Post.php │ ├── Delete.php │ ├── Middleware.php │ ├── Options.php │ ├── Validate.php │ └── Route.php ├── config.php ├── Service.php ├── Reader.php ├── InteractsWithInject.php ├── InteractsWithRoute.php └── InteractsWithModel.php ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | composer.lock 4 | vendor 5 | -------------------------------------------------------------------------------- /src/model/Relation.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'enable' => true, 6 | 'namespaces' => [], 7 | ], 8 | 'route' => [ 9 | 'enable' => true, 10 | 'controllers' => [], 11 | ], 12 | 'model' => [ 13 | 'enable' => true, 14 | ], 15 | 'ignore' => [], 16 | ]; 17 | -------------------------------------------------------------------------------- /src/route/Model.php: -------------------------------------------------------------------------------- 1 | params = $params; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/route/Options.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 14 | 15 | //自动注入 16 | $this->autoInject(); 17 | 18 | //注解路由 19 | $this->registerAnnotationRoute(); 20 | 21 | //模型注解方法提示 22 | $this->detectModelAnnotations(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/model/relation/MorphTo.php: -------------------------------------------------------------------------------- 1 | morph = $morph ?? $name; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/model/relation/HasMany.php: -------------------------------------------------------------------------------- 1 | morph = $morph ?? $name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/model/relation/MorphMany.php: -------------------------------------------------------------------------------- 1 | morph = $morph ?? $name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/model/relation/MorphToMany.php: -------------------------------------------------------------------------------- 1 | $name 16 | * @return array 17 | */ 18 | public function getAnnotations($ref, $name) 19 | { 20 | return array_map(function (ReflectionAttribute $attribute) { 21 | return $attribute->newInstance(); 22 | }, $ref->getAttributes($name, ReflectionAttribute::IS_INSTANCEOF)); 23 | } 24 | 25 | /** 26 | * @template T of object 27 | * @param ReflectionClass|ReflectionMethod $ref 28 | * @param class-string $name 29 | * @return T|null 30 | */ 31 | public function getAnnotation($ref, $name) 32 | { 33 | $attributes = $this->getAnnotations($ref, $name); 34 | 35 | foreach ($attributes as $attribute) { 36 | return $attribute; 37 | } 38 | 39 | return null; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # think-annotation for ThinkPHP 2 | 3 | ## 安装 4 | 5 | > composer require topthink/think-annotation 6 | 7 | ## 配置 8 | 9 | > 配置文件位于 `config/annotation.php` 10 | 11 | ## 使用方法 12 | 13 | ### 路由注解 14 | 15 | ~~~php 16 | 默认会扫描controller目录下的所有类 59 | > 可对个别目录单独配置 60 | 61 | ```php 62 | //... 63 | 'route' => [ 64 | 'enable' => true, 65 | 'controllers' => [ 66 | app_path('controller/admin') => [ 67 | 'name' => 'admin/api', 68 | 'middleware' => [], 69 | ], 70 | root_path('other/controller') 71 | ], 72 | ], 73 | //... 74 | ``` 75 | 76 | ### 模型注解 77 | 78 | ~~~php 79 | app->config->get('annotation.inject.enable', true)) { 18 | $this->app->resolving(function ($object, $app) { 19 | if ($this->isInjectClass(get_class($object))) { 20 | $refObject = new ReflectionObject($object); 21 | foreach ($refObject->getProperties() as $refProperty) { 22 | if ($refProperty->isDefault() && !$refProperty->isStatic()) { 23 | $attrs = $refProperty->getAttributes(Inject::class); 24 | if (!empty($attrs)) { 25 | if (!empty($attrs[0]->getArguments()[0])) { 26 | $type = $attrs[0]->getArguments()[0]; 27 | } elseif ($refProperty->getType() && !$refProperty->getType()->isBuiltin()) { 28 | $type = $refProperty->getType()->getName(); 29 | } 30 | 31 | if (isset($type)) { 32 | $value = $app->make($type); 33 | if (!$refProperty->isPublic()) { 34 | $refProperty->setAccessible(true); 35 | } 36 | $refProperty->setValue($object, $value); 37 | } 38 | } 39 | } 40 | } 41 | if ($refObject->hasMethod('__injected')) { 42 | $app->invokeMethod([$object, '__injected']); 43 | } 44 | } 45 | }); 46 | } 47 | } 48 | 49 | protected function isInjectClass($name) 50 | { 51 | $namespaces = ['app\\'] + $this->app->config->get('annotation.inject.namespaces', []); 52 | 53 | foreach ($namespaces as $namespace) { 54 | $namespace = rtrim($namespace, '\\') . '\\'; 55 | 56 | if (0 === stripos(rtrim($name, '\\') . '\\', $namespace)) { 57 | return true; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/InteractsWithRoute.php: -------------------------------------------------------------------------------- 1 | app->config->get('annotation.route.enable', true)) { 40 | $this->app->event->listen(RouteLoaded::class, function () { 41 | 42 | $this->route = $this->app->route; 43 | $this->controllerDir = realpath($this->app->getAppPath() . $this->app->config->get('route.controller_layer')); 44 | $this->controllerSuffix = $this->app->config->get('route.controller_suffix') ? 'Controller' : ''; 45 | 46 | $dirs = array_merge( 47 | $this->app->config->get('annotation.route.controllers', []), 48 | [$this->controllerDir] 49 | ); 50 | 51 | foreach ($dirs as $dir => $options) { 52 | if (is_numeric($dir)) { 53 | $dir = $options; 54 | $options = []; 55 | } 56 | 57 | if (is_dir($dir)) { 58 | $this->scanDir(realpath($dir), $options); 59 | } 60 | } 61 | }); 62 | } 63 | } 64 | 65 | protected function scanDir($dir, $options = []) 66 | { 67 | $groups = []; 68 | 69 | $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator( 70 | $dir, 71 | \FilesystemIterator::FOLLOW_SYMLINKS, 72 | )); 73 | 74 | $namespace = Arr::pull($options, 'namespace') ?: $this->getNamespace($dir, $this->controllerDir, "{$this->app->getNamespace()}\\controller"); 75 | 76 | if (empty($namespace)) { 77 | return; 78 | } 79 | 80 | foreach ($iterator as $fileInfo) { 81 | /** @var \SplFileInfo $fileInfo */ 82 | if (!$fileInfo->isFile()) { 83 | continue; 84 | } 85 | 86 | if ($fileInfo->getBasename('.php') === $fileInfo->getBasename()) { 87 | continue; 88 | } 89 | 90 | $filename = $fileInfo->getRealPath(); 91 | 92 | $classNamespace = $this->getNamespace(dirname($filename), $dir, $namespace); 93 | if (empty($namespace)) { 94 | continue; 95 | } 96 | 97 | $class = "\\{$classNamespace}\\{$fileInfo->getBasename('.php')}"; 98 | 99 | if (in_array($class, $this->parsedClass)) { 100 | continue; 101 | } 102 | 103 | $this->parsedClass[] = $class; 104 | 105 | $refClass = new ReflectionClass($class); 106 | 107 | if ($refClass->isAbstract() || $refClass->isInterface() || $refClass->isTrait()) { 108 | continue; 109 | } 110 | 111 | $prefix = $class; 112 | 113 | if (Str::startsWith($filename, $this->controllerDir)) { 114 | //控制器 115 | $filename = Str::substr($filename, strlen($this->controllerDir) + 1); 116 | $prefix = str_replace($this->controllerSuffix . '.php', '', str_replace(DIRECTORY_SEPARATOR, '.', $filename)); 117 | } 118 | 119 | $routes = []; 120 | //方法 121 | foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) { 122 | if ($routeAnn = $this->reader->getAnnotation($refMethod, Route::class)) { 123 | 124 | $routes[] = function () use ($routeAnn, $prefix, $refMethod) { 125 | //注册路由 126 | $rule = $this->route->rule($routeAnn->rule, "{$prefix}/{$refMethod->getName()}", $routeAnn->method); 127 | 128 | $rule->option($routeAnn->options); 129 | 130 | //变量规则 131 | if (!empty($patternsAnn = $this->reader->getAnnotations($refMethod, Pattern::class))) { 132 | foreach ($patternsAnn as $patternAnn) { 133 | $rule->pattern([$patternAnn->name => $patternAnn->value]); 134 | } 135 | } 136 | 137 | //中间件 138 | if (!empty($middlewaresAnn = $this->reader->getAnnotations($refMethod, Middleware::class))) { 139 | foreach ($middlewaresAnn as $middlewareAnn) { 140 | $rule->middleware($middlewareAnn->value, ...$middlewareAnn->params); 141 | } 142 | } 143 | 144 | //绑定模型,支持多个 145 | if (!empty($modelsAnn = $this->reader->getAnnotations($refMethod, Model::class))) { 146 | foreach ($modelsAnn as $modelAnn) { 147 | $rule->model($modelAnn->var, $modelAnn->value, $modelAnn->exception); 148 | } 149 | } 150 | 151 | //验证 152 | if ($validateAnn = $this->reader->getAnnotation($refMethod, Validate::class)) { 153 | $rule->validate($validateAnn->value, $validateAnn->scene, $validateAnn->message, $validateAnn->batch); 154 | } 155 | }; 156 | } 157 | } 158 | 159 | $groups[] = function () use ($routes, $refClass, $prefix) { 160 | $groupName = ''; 161 | $groupOptions = []; 162 | if ($groupAnn = $this->reader->getAnnotation($refClass, Group::class)) { 163 | $groupName = $groupAnn->name; 164 | $groupOptions = $groupAnn->options; 165 | } 166 | 167 | $group = $this->route->group($groupName, function () use ($refClass, $prefix, $routes) { 168 | //注册路由 169 | foreach ($routes as $route) { 170 | $route(); 171 | } 172 | 173 | if ($resourceAnn = $this->reader->getAnnotation($refClass, Resource::class)) { 174 | //资源路由 175 | $this->route->resource($resourceAnn->rule, $prefix)->option($resourceAnn->options); 176 | } 177 | }); 178 | 179 | $group->option($groupOptions); 180 | 181 | //变量规则 182 | if (!empty($patternsAnn = $this->reader->getAnnotations($refClass, Pattern::class))) { 183 | foreach ($patternsAnn as $patternAnn) { 184 | $group->pattern([$patternAnn->name => $patternAnn->value]); 185 | } 186 | } 187 | 188 | //中间件 189 | if (!empty($middlewaresAnn = $this->reader->getAnnotations($refClass, Middleware::class))) { 190 | foreach ($middlewaresAnn as $middlewareAnn) { 191 | $group->middleware($middlewareAnn->value, ...$middlewareAnn->params); 192 | } 193 | } 194 | }; 195 | } 196 | 197 | if (!empty($groups)) { 198 | $name = Arr::pull($options, 'name', ''); 199 | $this->route->group($name, function () use ($groups) { 200 | //注册路由 201 | foreach ($groups as $group) { 202 | $group(); 203 | } 204 | })->option($options); 205 | } 206 | } 207 | 208 | /** 209 | * Calculate the namespace based on the relative path between directories 210 | * 211 | * @param string $dir The directory to get the namespace for 212 | * @param string $base The base directory 213 | * @param string $baseNamespace The base namespace 214 | * @return string|null The calculated namespace or null if $dir is not a subdirectory of $base 215 | */ 216 | protected function getNamespace($dir, $base, $baseNamespace) 217 | { 218 | // Normalize directory separators 219 | $dir = rtrim(str_replace('\\', '/', $dir), '/') . '/'; 220 | $base = rtrim(str_replace('\\', '/', $base), '/') . '/'; 221 | 222 | // Check if $dir is a subdirectory of $base 223 | if (!str_starts_with($dir, $base)) { 224 | return null; 225 | } 226 | 227 | // Get the relative path 228 | $relativePath = substr($dir, strlen($base)); 229 | 230 | // If the relative path is empty, return the base namespace 231 | if (empty($relativePath) || $relativePath === '/') { 232 | return $baseNamespace; 233 | } 234 | 235 | // Convert directory separators to namespace separators 236 | $namespace = str_replace('/', '\\', rtrim($relativePath, '/')); 237 | 238 | // Combine with the base namespace 239 | return $baseNamespace . '\\' . $namespace; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/InteractsWithModel.php: -------------------------------------------------------------------------------- 1 | app->config->get('annotation.model.enable', true)) { 48 | 49 | Model::maker(function (Model $model) { 50 | $className = get_class($model); 51 | if (!isset($this->detected[$className])) { 52 | $annotations = $this->reader->getAnnotations(new ReflectionClass($model), Relation::class); 53 | 54 | foreach ($annotations as $annotation) { 55 | 56 | $relation = function () use ($annotation) { 57 | 58 | $refMethod = new ReflectionMethod($this, Str::camel(class_basename($annotation))); 59 | 60 | $args = []; 61 | foreach ($refMethod->getParameters() as $param) { 62 | $args[] = $annotation->{$param->getName()}; 63 | } 64 | 65 | return $refMethod->invokeArgs($this, $args); 66 | }; 67 | 68 | call_user_func([$model, 'macro'], $annotation->name, $relation); 69 | } 70 | 71 | $options = []; 72 | $refClass = new ReflectionClass($model); 73 | 74 | // Handle annotations with a common method 75 | $this->processModelAnnotation($refClass, $options, Append::class, 'append', 'append'); 76 | $this->processModelAnnotation($refClass, $options, CreateTime::class, 'create_time', 'name'); 77 | $this->processModelAnnotation($refClass, $options, Hidden::class, 'hidden', 'hidden'); 78 | $this->processModelAnnotation($refClass, $options, Pk::class, 'pk', 'name'); 79 | $this->processModelAnnotation($refClass, $options, Suffix::class, 'suffix', 'suffix'); 80 | $this->processModelAnnotation($refClass, $options, UpdateTime::class, 'update_time', 'name'); 81 | $this->processModelAnnotation($refClass, $options, Visible::class, 'visible', 'visible'); 82 | 83 | // Handle repeatable annotations with a common method 84 | $this->processRepeatableAnnotation($refClass, $options, Type::class, 'type', 'name', 'type'); 85 | $this->processRepeatableAnnotation($refClass, $options, Mapping::class, 'mapping', 'field', 'name'); 86 | 87 | $this->detected[$className] = [ 88 | 'options' => $options, 89 | ]; 90 | } else { 91 | $options = $this->detected[$className]['options']; 92 | } 93 | 94 | //options 95 | $model->setOptions($options); 96 | }); 97 | 98 | $this->app->event->listen(ModelGenerator::class, function (ModelGenerator $generator) { 99 | 100 | $annotations = $this->reader->getAnnotations($generator->getReflection(), Relation::class); 101 | 102 | foreach ($annotations as $annotation) { 103 | $property = Str::snake($annotation->name); 104 | switch (true) { 105 | case $annotation instanceof HasOne: 106 | $generator->addMethod($annotation->name, \think\model\relation\HasOne::class, [], ''); 107 | $generator->addProperty($property, $annotation->model, true); 108 | break; 109 | case $annotation instanceof BelongsTo: 110 | $generator->addMethod($annotation->name, \think\model\relation\BelongsTo::class, [], ''); 111 | $generator->addProperty($property, $annotation->model, true); 112 | break; 113 | case $annotation instanceof HasMany: 114 | $generator->addMethod($annotation->name, \think\model\relation\HasMany::class, [], ''); 115 | $generator->addProperty($property, $annotation->model . '[]', true); 116 | break; 117 | case $annotation instanceof HasManyThrough: 118 | $generator->addMethod($annotation->name, \think\model\relation\HasManyThrough::class, [], ''); 119 | $generator->addProperty($property, $annotation->model . '[]', true); 120 | break; 121 | case $annotation instanceof HasOneThrough: 122 | $generator->addMethod($annotation->name, \think\model\relation\HasOneThrough::class, [], ''); 123 | $generator->addProperty($property, $annotation->model, true); 124 | break; 125 | case $annotation instanceof BelongsToMany: 126 | $generator->addMethod($annotation->name, \think\model\relation\BelongsToMany::class, [], ''); 127 | $generator->addProperty($property, $annotation->model . '[]', true); 128 | break; 129 | case $annotation instanceof MorphOne: 130 | $generator->addMethod($annotation->name, \think\model\relation\MorphOne::class, [], ''); 131 | $generator->addProperty($property, $annotation->model, true); 132 | break; 133 | case $annotation instanceof MorphMany: 134 | $generator->addMethod($annotation->name, \think\model\relation\MorphMany::class, [], ''); 135 | $generator->addProperty($property, 'mixed', true); 136 | break; 137 | case $annotation instanceof MorphTo: 138 | $generator->addMethod($annotation->name, \think\model\relation\MorphTo::class, [], ''); 139 | $generator->addProperty($property, 'mixed', true); 140 | break; 141 | case $annotation instanceof MorphToMany: 142 | case $annotation instanceof MorphByMany: 143 | $generator->addMethod($annotation->name, \think\model\relation\MorphToMany::class, [], ''); 144 | $generator->addProperty($property, Collection::class, true); 145 | break; 146 | } 147 | } 148 | }); 149 | } 150 | } 151 | 152 | /** 153 | * Process a model annotation and add it to the options array 154 | * 155 | * @param ReflectionClass $refClass The reflection class of the model 156 | * @param array &$options The options array to modify 157 | * @param string $annotationClass The annotation class to look for 158 | * @param string $optionKey The key to use in the options array 159 | * @param string $propertyName The property name to get from the annotation 160 | */ 161 | protected function processModelAnnotation(ReflectionClass $refClass, array &$options, string $annotationClass, string $optionKey, string $propertyName): void 162 | { 163 | if ($annotation = $this->reader->getAnnotation($refClass, $annotationClass)) { 164 | $options[$optionKey] = $annotation->{$propertyName}; 165 | } 166 | } 167 | 168 | /** 169 | * Process repeatable model annotations and add them to the options array 170 | * 171 | * @param ReflectionClass $refClass The reflection class of the model 172 | * @param array &$options The options array to modify 173 | * @param string $annotationClass The annotation class to look for 174 | * @param string $optionKey The key to use in the options array 175 | * @param string $keyProperty The property name to use as key in the result array 176 | * @param string $valueProperty The property name to use as value in the result array 177 | */ 178 | protected function processRepeatableAnnotation(ReflectionClass $refClass, array &$options, string $annotationClass, string $optionKey, string $keyProperty, string $valueProperty): void 179 | { 180 | $annotations = $this->reader->getAnnotations($refClass, $annotationClass); 181 | if (!empty($annotations)) { 182 | $options[$optionKey] = []; 183 | foreach ($annotations as $annotation) { 184 | $options[$optionKey][$annotation->{$keyProperty}] = $annotation->{$valueProperty}; 185 | } 186 | } 187 | } 188 | } 189 | --------------------------------------------------------------------------------