├── .gitignore ├── src ├── Annotation │ ├── Autowire.php │ ├── Validator.php │ ├── Transaction.php │ ├── RequestParam.php │ └── DataCache.php ├── AnnotationInterceptor.php ├── Blocker.php ├── ModelAnnotationScaner.php ├── ControllerAnnotationScaner.php ├── Proxy.php └── AnnotationScaner.php ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | /vendor 4 | composer.lock 5 | -------------------------------------------------------------------------------- /src/Annotation/Autowire.php: -------------------------------------------------------------------------------- 1 | block = $block; 24 | $this->data = $data; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ModelAnnotationScaner.php: -------------------------------------------------------------------------------- 1 | readPropertiesAnnotation($modelObj); 17 | 18 | return $modelObj; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ControllerAnnotationScaner.php: -------------------------------------------------------------------------------- 1 | readMethodAnnotation($instance, $action); 17 | $annotationScaner->readPropertiesAnnotation($instance); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Annotation/Validator.php: -------------------------------------------------------------------------------- 1 | =5.6", 16 | "ext-json": "*", 17 | "topthink/framework": "5.1.*", 18 | "doctrine/annotations": "1.4.0", 19 | "nikic/php-parser": "v3.1.5" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Fairy\\": "src" 24 | } 25 | }, 26 | "repositories": [ 27 | { 28 | "type": "composer", 29 | "url": "https://mirrors.aliyun.com/composer/" 30 | }, 31 | { 32 | "type": "composer", 33 | "url": "https://packagist.phpcomposer.com" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/Annotation/RequestParam.php: -------------------------------------------------------------------------------- 1 | getKeyName($proxy->getClass(), $methodName, $arguments)); 36 | 37 | return new Blocker($result ? true : false, $result); 38 | } 39 | 40 | public function afterAction($result, $methodName, $arguments, Proxy $proxy) 41 | { 42 | Cache::set($this->getKeyName($proxy->getClass(), $methodName, $arguments), $result, $this->transExpire()); 43 | } 44 | 45 | protected function getKeyName($className, $methodName, $arguments) 46 | { 47 | $key = $this->name ? preg_replace_callback('/\${(\d)}/', function ($matches) use ($arguments) { 48 | return isset($arguments[$matches[1]]) ? $arguments[$matches[1]] : $matches[0]; 49 | }, $this->name) : $methodName; 50 | 51 | return $className . ':' . $methodName . ':' . $key; 52 | } 53 | 54 | protected function transExpire() 55 | { 56 | $pattern = '/^(\d+)([a-zA-Z]*)$/'; 57 | if (!preg_match($pattern, $this->expire, $matches)) { 58 | return 0; 59 | } 60 | 61 | $result = $matches[1]; 62 | if (!$matches[2]) { 63 | return $result; 64 | } 65 | 66 | switch ($matches[2]) { 67 | case 'm': 68 | $result *= 60; 69 | break; 70 | case 'h': 71 | $result *= 60 * 60; 72 | break; 73 | case 'd': 74 | $result *= 24 * 60 * 60; 75 | break; 76 | } 77 | 78 | return $result; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Proxy.php: -------------------------------------------------------------------------------- 1 | bean = $bean; 14 | } 15 | 16 | public function __call($methodName, $arguments) 17 | { 18 | /**@var $annotationScaner AnnotationScaner */ 19 | $annotationScaner = app(AnnotationScaner::class); 20 | $interceptorAnnotationObjs = $annotationScaner->readMethodAnnotation($this->bean, $methodName); 21 | 22 | return $this->callMethod($methodName, $arguments, $interceptorAnnotationObjs); 23 | } 24 | 25 | /** 26 | * 执行拦截器并返回方法执行的结果 27 | * @param $methodName 28 | * @param $arguments 29 | * @param array $annotations 30 | * @return bool|mixed 31 | */ 32 | private function callMethod($methodName, $arguments, $annotations = []) 33 | { 34 | $isBlocked = false; 35 | $data = null; 36 | 37 | foreach ($annotations as $annotation) { 38 | $beforeActionData = call_user_func_array([$annotation, 'beforeAction'], [$methodName, $arguments, $this]); 39 | if ($beforeActionData !== null && $beforeActionData instanceof Blocker) { 40 | if (isset($beforeActionData->data)) { 41 | $data = $beforeActionData->data; 42 | } 43 | if ($beforeActionData->block) { 44 | $isBlocked = true; 45 | break; 46 | } 47 | } 48 | } 49 | 50 | if ($isBlocked) { 51 | $result = $data; 52 | } else { 53 | try { 54 | $result = call_user_func_array([$this->bean, $methodName], $arguments); 55 | } catch (\Exception $e) { 56 | $result = false; 57 | } 58 | foreach (array_reverse($annotations) as $annotation) { 59 | $afterActionData = call_user_func_array([$annotation, 'afterAction'], [$result, $methodName, $arguments, $this]); 60 | if ($afterActionData) { 61 | $result = $afterActionData; 62 | } 63 | } 64 | } 65 | 66 | return $result; 67 | } 68 | 69 | public function getBean() 70 | { 71 | return $this->bean; 72 | } 73 | 74 | public function getClass() 75 | { 76 | return get_class($this->bean); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thinkphp-annotation 2 | 3 | 4 | 5 | 前言: 6 | ------- 7 | 8 | thinkphp5.1中用注解的方式实现: 9 | 10 | - 请求数据验证 11 | - 请求数据过滤、格式化 12 | - 属性对象自动注入 13 | - 自动事务 14 | 15 | > 最新文档地址:https://www.cnblogs.com/cshaptx4869/p/12178960.html 16 | 17 | 18 | 19 | 安装 20 | ------------ 21 | 22 | ```bash 23 | composer require cshaptx4869/thinkphp-annotation 24 | ``` 25 | 26 | 27 | 28 | ## 配置 29 | 30 | `tags.php `添加行为,用于控制器注解扫描 31 | 32 | ```php 33 | 'action_begin' => [ 34 | \Fairy\ControllerAnnotationScaner::class 35 | ] 36 | ``` 37 | 38 | 模型中使用属性注解的话,需要在模型中引入 \Fairy\ModelAnnotationScaner 的trait 39 | 40 | ```php 41 | use \Fairy\ModelAnnotationScaner; 42 | ``` 43 | 44 | 添加 `system.php` 配置文件(可选) 45 | 46 | ```php 47 | return [ 48 | 'annotation' => [ 49 | 'cache' => false,// 是否开启注解读取缓存,默认false 50 | 'writelist' => []// 注解读取白名单,默认[] 51 | 'interceptor' => [// 注解拦截器相关配置 52 | 'enable' => true,// 默认开启注解拦截器 53 | 'whitelist' => []// 注解拦截器白名单,默认[] 54 | ], 55 | 'validate' => [ 56 | 'callback' => function($msg) { 57 | // 自定义验证错误信息后续处理 58 | } 59 | ] 60 | ] 61 | ] 62 | ``` 63 | 64 | > PS:默认验证器注解验证不通过会终止程序运行并返回`json`格式的验证错误信息。可通过配置 callback函数自定义后续处理。请注意,不同版本使用上会有些许差别。 65 | > 66 | 67 | 68 | 69 | ## 支持的注解 70 | 71 | ###### v0.1.0版: 72 | 73 | | 注解名 | 申明范围 | 作用 | 74 | | ---------------- | -------- | ---------------------------- | 75 | | @Autowire | 属性 | 自动注入类对象 | 76 | | @DynamicAutowire | 方法 | 声明当前方法允许属性注入的类 | 77 | | @IgnoreAutowire | 方法 | 声明当前方法忽略属性注入的类 | 78 | | @RequestParam | 方法 | 过滤、格式化请求参数 | 79 | | @Validator | 方法 | 验证器验证 | 80 | 81 | ###### v0.1.1版: 82 | 83 | | 注解名 | 申明范围 | 作用 | 84 | | ------------- | -------- | -------------------- | 85 | | @RequestParam | 方法 | 过滤、格式化请求参数 | 86 | | @Validator | 方法 | 验证器验证 | 87 | | @Autowire | 属性 | 自动注入类对象 | 88 | | @Transaction | 方法 | 自动事务 | 89 | 90 | > #### 版本差异: 91 | > 92 | > v0.1.1新增: 93 | > 94 | > **Transaction 注解** 95 | > 96 | > Transaction 注解根据当前方法返回值自动判断事务后续处理,如返回值等价于true就会自动commit,否则rollback。 97 | > 98 | > Transaction 注解需要搭配 Autowire注解使用,且不支持在控制器中使用,推荐在模型中使用。 99 | > 100 | > **ModelAnnotationScaner** 101 | > 102 | > 支持模型中使用属性注解 103 | > 104 | > 105 | > 106 | > Autowire 注解改动: 107 | > 108 | > v0.1.0 版本中 Autowire 注解必须写class属性,如 Autowire(class=ArticleModel::class),而在v0.1.1版本中 Autowire 注解则没有class属性而是通过@var ArticleModel 注解来自动识别。 109 | 110 | 111 | 112 | ## v0.1.0版本使用示例 113 | 114 | `ArticleController` 控制器: 115 | 116 | ```php 117 | requestParam; 175 | 176 | return MyToolkit::success($this->articleModel->store($postData)); 177 | } 178 | ``` 179 | 180 | 181 | 182 | ## v0.1.1版本使用示例 183 | 184 | `ArticleController` 控制器: 185 | 186 | ```php 187 | requestParam; 246 | 247 | return MyToolkit::success($this->articleModel->store($postData)); 248 | } 249 | } 250 | ``` 251 | 252 | `ArticleModel` 模型: 253 | 254 | ```php 255 | insertGetId($params); 287 | $realtion = array_map(function ($categoryId) use ($articleId) { 288 | return [ 289 | 'article_id' => $articleId, 290 | 'category_id' => $categoryId 291 | ]; 292 | }, $categoryIds); 293 | 294 | return $this->articleCategoryRelationModel->store($realtion); 295 | } 296 | } 297 | ``` 298 | 299 | 300 | 301 | ## IDE 注解插件支持 302 | 303 | 一些ide已经提供了对注释的支持,推荐安装,以便提供注解语法提示 304 | 305 | - Eclipse via the [Symfony2 Plugin](http://symfony.dubture.com/) 306 | - PHPStorm via the [PHP Annotations Plugin](http://plugins.jetbrains.com/plugin/7320) or the [Symfony2 Plugin](http://plugins.jetbrains.com/plugin/7219) 307 | 308 | -------------------------------------------------------------------------------- /src/AnnotationScaner.php: -------------------------------------------------------------------------------- 1 | init(); 47 | } 48 | 49 | protected function init() 50 | { 51 | AnnotationRegistry::registerLoader('class_exists'); 52 | // AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Route.php'); //注册文件 53 | // AnnotationRegistry::registerAutoloadNamespace('Faily\\Annotation'); //注册命名空间 54 | // AnnotationRegistry::registerAutoloadNamespaces(['Faily\\Annotation' => null]); //注册多个命名空间 55 | // 注解读取白名单 56 | $this->setWhiteList(); 57 | // 注解读取器 58 | $this->annotationReader = config('system.annotation.cache') ? 59 | new FileCacheReader(new AnnotationReader(), env('runtime_path') . DIRECTORY_SEPARATOR . "annotation", true) : 60 | new AnnotationReader(); 61 | $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP5); 62 | } 63 | 64 | /** 65 | * 读取类的所有属性的注解 66 | * @param $instance 67 | * @throws \ReflectionException 68 | */ 69 | public function readPropertiesAnnotation($instance) 70 | { 71 | $reflectionClass = new \ReflectionClass($instance); 72 | $reflectionProperties = $reflectionClass->getProperties(); 73 | foreach ($reflectionProperties as $reflectionProperty) { 74 | $propertyAnnotations = $this->annotationReader->getPropertyAnnotations($reflectionProperty); 75 | foreach ($propertyAnnotations as $propertyAnnotation) { 76 | if ($propertyAnnotation instanceof Autowire) { 77 | if ($reflectionProperty->isPublic() && !$reflectionProperty->isStatic()) { 78 | if ($class = $this->getPropertyType($reflectionClass->getFileName(), $reflectionClass->getNamespaceName(), $reflectionProperty->getDocComment())) { 79 | if (isset($this->proxy[$class])) { 80 | $reflectionProperty->setValue($instance, $this->proxy[$class]); 81 | } else { 82 | $proxyObj = new Proxy(app($class)); 83 | $this->proxy[$class] = $proxyObj; 84 | $reflectionProperty->setValue($instance, $proxyObj); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * 读取当前方法的注解 如果有拦截器注解则返回拦截器注解对象数组 95 | * @param $instance 96 | * @param $method 97 | * @return array 98 | * @throws \ReflectionException 99 | */ 100 | public function readMethodAnnotation($instance, $method) 101 | { 102 | $reflectionMethod = new \ReflectionMethod($instance, $method); 103 | $methodAnnotations = $this->annotationReader->getMethodAnnotations($reflectionMethod); 104 | $interceptorEnable = config('?system.annotation.interceptor.enable') ? config('system.annotation.interceptor.enable') : true; 105 | $interceptorWhitelist = config('?system.annotation.interceptor.whitelist') ? config('system.annotation.interceptor.whitelist') : []; 106 | $validateErrorMsgCallback = config('?system.annotation.validate.callback') ? config('system.annotation.validate.callback') : []; 107 | $interceptorAnnotationObjs = []; 108 | foreach ($methodAnnotations as $methodAnnotation) { 109 | if (!$methodAnnotation instanceof AnnotationInterceptor) { 110 | if ($methodAnnotation instanceof Validator) {// 验证器 111 | /**@var $validate \think\validate */ 112 | $validate = app($methodAnnotation->class); 113 | if (!$validate instanceof Validate) { 114 | throw new \Exception('class ' . $methodAnnotation->class . ' is not a thinkphp validate class'); 115 | } 116 | if ($methodAnnotation->batch) { 117 | $validate->batch(); 118 | } 119 | if ($methodAnnotation->scene) { 120 | $validate->scene($methodAnnotation->scene); 121 | } 122 | if (!$validate->check(app('request')->param())) { 123 | if ($methodAnnotation->throw) { 124 | throw new ValidateException($validate->getError()); 125 | } else { 126 | if ($validateErrorMsgCallback && $validateErrorMsgCallback instanceof \Closure) { 127 | call_user_func($validateErrorMsgCallback, $validate->getError()); 128 | } else { 129 | exit($this->formatErrorMsg($validate->getError())); 130 | } 131 | } 132 | } 133 | } else if ($methodAnnotation instanceof RequestParam) {// 参数获取器 134 | $requestParams = app('request')->only($methodAnnotation->fields, $methodAnnotation->method ?: 'param'); 135 | if ($formatRules = $methodAnnotation->json) { 136 | $this->formatRequestParams($formatRules, $requestParams); 137 | } 138 | if ($methodAnnotation->mapping) { 139 | $mapping = []; 140 | foreach ($requestParams as $key => $value) { 141 | if (isset($methodAnnotation->mapping[$key])) { 142 | $mapping[$methodAnnotation->mapping[$key]] = $value; 143 | } else { 144 | $mapping[$key] = $value; 145 | } 146 | } 147 | $requestParams = $mapping; 148 | } 149 | app('request')->requestParam = $requestParams; 150 | } 151 | } else { 152 | if ($interceptorEnable && !in_array(get_class($methodAnnotation), $interceptorWhitelist)) { 153 | $interceptorAnnotationObjs[] = $methodAnnotation; 154 | } 155 | } 156 | } 157 | 158 | return $interceptorAnnotationObjs; 159 | } 160 | 161 | /** 162 | * 设置注解读取白名单 163 | * @return array 164 | */ 165 | protected function setWhiteList() 166 | { 167 | if ($whitelist = config('system.annotation.whitelist')) { 168 | $this->whitelist = array_merge($this->whitelist, $whitelist); 169 | } 170 | foreach ($this->whitelist as $v) { 171 | AnnotationReader::addGlobalIgnoredName($v); 172 | } 173 | } 174 | 175 | /** 176 | * 格式化错误信息 177 | * @param string $msg 178 | * @return false|string 179 | */ 180 | protected function formatErrorMsg($msg = '') 181 | { 182 | return json_encode([ 183 | 'code' => 422, 'data' => '', 'msg' => $msg, 'time' => request()->time() 184 | ]); 185 | } 186 | 187 | /** 188 | * 格式化请求参数 189 | * @param array $rules 190 | * @param $requestParams 191 | * @throws \Exception 192 | */ 193 | protected function formatRequestParams(array $rules, &$requestParams) 194 | { 195 | foreach ($rules as $field => $rule) { 196 | if (is_int($field)) { 197 | $field = (string)$rule; 198 | } 199 | if (isset($requestParams[$field])) { 200 | $data = json_decode($requestParams[$field], true); 201 | if (is_null($data)) { 202 | throw new \Exception($field . '字段的值无法json反序列化'); 203 | } 204 | // 过滤json数据的字段 205 | if (is_array($rule) && $rule) { 206 | foreach ($data as $k => $v) { 207 | if (is_string($k)) {//一维数组 208 | if (!in_array($k, $rule)) { 209 | unset($data[$k]); 210 | } 211 | } else if (is_int($k) && is_array($v)) {//二维数组 212 | foreach ($v as $kk => $vv) { 213 | if (!in_array($kk, $rule)) { 214 | unset($data[$k][$kk]); 215 | } 216 | } 217 | } 218 | } 219 | } 220 | $requestParams[$field] = $data; 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * 获取属性声明的var类型 227 | * @param $filepath 228 | * @param $namespace 229 | * @param $doc 230 | * @return bool|mixed|string 231 | */ 232 | protected function getPropertyType($filepath, $namespace, $doc) 233 | { 234 | if (!preg_match('/@var\s+(\S+)\s+/', $doc, $matches)) { 235 | return false; 236 | } 237 | $type = $matches[1]; 238 | if (!(strpos($type, '\\') === 0)) { 239 | $stmts = $this->parser->parse(file_get_contents($filepath)); 240 | $uses = $this->getUses($stmts); 241 | if (array_key_exists($type, $uses)) { 242 | $type = $uses[$type]; 243 | } else { 244 | $type = $namespace . '\\' . $type; 245 | } 246 | } 247 | 248 | return $type; 249 | } 250 | 251 | /** 252 | * 获取类中的use信息 253 | * @param $stmts 254 | * @return array 255 | */ 256 | protected function getUses($stmts) 257 | { 258 | $uses = []; 259 | foreach ($stmts as $v) { 260 | if ($v instanceof Namespace_) { 261 | foreach ($v->stmts as $vv) { 262 | if ($vv instanceof Use_) { 263 | foreach ($vv->uses as $vvv) { 264 | $name = implode('\\', $vvv->name->parts); 265 | if ($vvv->alias) { 266 | $alias = $vvv->alias; 267 | } else { 268 | $pos = strrpos($name, '\\'); 269 | $alias = substr($name, $pos ? $pos + 1 : 0); 270 | } 271 | $uses[$alias] = $name; 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | return $uses; 279 | } 280 | } 281 | --------------------------------------------------------------------------------