├── README.md ├── RedLock分布式锁 └── README.md ├── composer └── 执行流程源码分析.md ├── img └── connection1.png ├── phpfpm ├── reload操作发生502错误的问题分析.md └── 管理源码.md ├── predis ├── redisCluster模式源码.md └── redisSentinel模式源码.md ├── workerman └── TcpConnection类的生存周期.md └── yii2 ├── [Cache与过滤器一]数据缓存源码.md ├── [Cache与过滤器三]Page缓存源码.md ├── [Cache与过滤器二]过滤器Filters源码.md ├── [Cache与过滤器四]HTTP缓存源码.md ├── [redis]Connection源码.md ├── [关键概念一]Behavior行为源码.md ├── [关键概念三]别名与类自动加载源码.md ├── [关键概念二]Events事件源码.md ├── [安全一]用户认证源码.md ├── [数据库一]连接数据库源码.md ├── [数据库三]事务源码.md ├── [数据库二]执行sql源码.md ├── [数据库四]批处理查询源码.md ├── [模型层一]Model源码.md ├── [模型层二]ActiveRecord源码.md ├── [请求处理一]路由源码.md ├── [请求处理三]错误处理源码.md └── [请求处理二]Session和Cookie源码.md /README.md: -------------------------------------------------------------------------------- 1 | 2 | :smiling_imp::smiling_imp::smiling_imp: 3 | **PHP各种框架源码分析** 4 | :smiling_imp::smiling_imp::smiling_imp: 5 | 6 | |框架| 7 | |---| 8 | |Composer| 9 | |Workerman3.5.22| 10 | |Yii2| 11 | |predis| 12 | |RedLock| 13 | |phpfpm管理脚本| 14 | 15 | ## Composer 16 | * [执行流程源码分析](/composer/执行流程源码分析.md) 17 | 18 | ## Workerman 19 | * [TcpConnection类的生存周期](workerman/TcpConnection类的生存周期.md) 20 | 21 | ## phpfpm管理脚本 22 | * [reload操作发生502错误的问题分析](phpfpm/reload操作发生502错误的问题分析.md) 23 | * [管理源码](/phpfpm/管理源码.md) 24 | 25 | ## Yii2源码分析 26 | * 关键概念 27 | * [Behavior行为源码](yii2/%5B关键概念一%5DBehavior行为源码.md) 28 | * [Events事件源码](yii2/%5B关键概念二%5DEvents事件源码.md) 29 | * [别名与类自动加载源码](yii2/%5B关键概念三%5D别名与类自动加载源码.md) 30 | * [依赖注入DI源码](yii2/%5B关键概念三%5D别名与类自动加载源码.md) 31 | * Caching与过滤器 32 | * [数据缓存源码](yii2/%5BCache与过滤器一%5D数据缓存源码.md) 33 | * [过滤器源码](yii2/%5BCache与过滤器二%5D过滤器Filters源码.md) 34 | * [Page缓存源码](yii2/%5BCache与过滤器三%5DPage缓存源码.md) 35 | * [HTTP缓存源码](yii2/%5BCache与过滤器四%5DHTTP缓存源码.md) 36 | * 数据库系列 37 | * [连接数据库源码](yii2/%5B数据库一%5D连接数据库源码.md) 38 | * [执行sql源码](yii2/%5B数据库二%5D执行sql源码.md) 39 | * [事务源码](yii2/%5B数据库三%5D事务源码.md) 40 | * [批处理查询源码](yii2/%5B数据库四%5D批处理查询源码.md) 41 | * redis系列 42 | * [Connection源码](/yii2/%5Bredis%5DConnection源码.md) 43 | * 安全系列 44 | * [用户认证源码](yii2/%5B安全一%5D用户认证源码.md) 45 | * 模型层系列 46 | * [Model源码](yii2/%5B模型层一%5DModel源码.md) 47 | * [ActiveRecord源码](yii2/%5B模型层二%5DActiveRecord源码.md) 48 | * 请求处理 49 | * [路由源码](yii2/%5B请求处理一%5D路由源码.md) 50 | * [Session和Cookie源码](yii2/%5B请求处理二%5DSession和Cookie源码.md) 51 | * [错误处理源码](yii2/%5B请求处理三%5D错误处理源码.md) 52 | 53 | ## predis源码分析 54 | * [redisCluster源码](/predis/redisCluster模式源码.md) 55 | * [redisSentinel源码](predis/redisSentinel模式源码.md) 56 | 57 | ## RedLock源码分析 58 | * [RedLock源码分析](/RedLock分布式锁/README.md) 59 | -------------------------------------------------------------------------------- /RedLock分布式锁/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 目录 3 | * [单机模式下的redis锁](#单机模式下的redis锁) 4 | * [主从模式下的redis锁](#主从模式下的redis锁) 5 | * [集群模式下的redis锁](#集群模式下的redis锁) 6 | * [删除锁操作](#删除锁操作) 7 | * [删除锁操作的改进](#删除锁操作的改进) 8 | * [多节点轮流请求的超时时间](#多节点轮流请求的超时时间) 9 | * [php版本redlock源码](#php版本redlock源码) 10 | 11 | # 单机模式下的redis锁 12 | ||client A|client B| 13 | |:---|:---|:---| 14 | |1|set lock 1234 nx px 3000获得锁成功|| 15 | |2||set lock 1234 nx px 3000 3000毫秒内,获得锁失败| 16 | |3||set lock 1234 nx px 3000 3000毫秒外,获得锁成功| 17 | 18 | 可见单机模式下的redis锁是可以做到互斥性,也不会发生死锁 19 | 但是最大的问题是如果这个redis单机宕机,会造成整个服务不可用 20 | # 主从模式下的redis锁 21 | ||client A|client B| 22 | |:---|:---|:---| 23 | |1|set lock 1234 nx px 3000获得锁成功,master同步给slave|| 24 | |2||set lock 1234 nx px 3000 3000毫秒内,获得锁失败| 25 | |3||set lock 1234 nx px 3000 3000毫秒外,获得锁成功,master同步给slave| 26 | |4|set lock 1234 nx px 3000 3000毫秒外,获得锁成功,master宕机,lock未同步给slave,slave升级为master|| 27 | |5||set lock 1234 nx px 3000 3000毫秒内,获得锁成功,因为新的master没有lock锁| 28 | 29 | 由于redis主从是异步复制 30 | - 客户端发送写命令给master 31 | - master执行写命令,将结果返回给客户端 32 | - 执行成功则master将命令同步给slave 33 | 34 | 可见在主从模式下锁会造成互斥性失效 35 | 36 | # 集群模式下的redis锁 37 | 因为cluster模式,需要根据键做crc32,然后去判断键分配到那个槽里面,所以集群模式可以理解为单机模式 38 | 39 | # 删除锁操作 40 | 在客户端拿到锁并进行业务逻辑后,需要进行锁删除操作,以便尽快解锁 41 | 42 | ||client A|client B| 43 | |:---|:---|:---| 44 | |1|set lock 1234 nx px 3000获得锁成功|| 45 | |2||set lock 1234 nx px 3000 第1500毫秒,获得锁失败| 46 | |3|第2000毫秒,业务逻辑处理完毕,del lock|| 47 | |4||第2200毫秒,set lock 1234 nx px 3000 获得锁成功| 48 | 49 | 以上情况开起来很美好,但是考虑如下情况 50 | 51 | ||client A|client B| 52 | |:---|:---|:---| 53 | |1|set lock 1234 nx px 3000获得锁成功|| 54 | |2||set lock 1234 nx px 3000 第1500毫秒,获得锁失败| 55 | |3|第2000毫秒,业务逻辑处理完毕,del lock,命令在网络上发生阻塞,没有传递给redis客户端|| 56 | |4||第2200毫秒,set lock 1234 nx px 3000 获得锁失败,因为redis没有收到client A的del操作| 57 | |5||第4000毫秒,set lock 1234 nx px 3000获得锁成功| 58 | |6|del命令被redis接受|lock未超时,client B未处理完业务逻辑| 59 | |7|set lock 1234 nx px 3000获得锁成功|| 60 | 61 | 可见这种删除操作无法做到互斥性,client A将client B拿到的锁给删除了 62 | # 删除锁操作的改进 63 | 每次生成lock的值都是随机的,然后使用lua脚本来删除,这个删除操作是一个原子性操作 64 | ``` 65 | if redis.call("GET", KEYS[1]) == ARGV[1] then 66 | return redis.call("DEL", KEYS[1]) 67 | else 68 | return 0 69 | end 70 | ``` 71 | 使用redis监视器可以查看到这个lua脚本的执行过程 72 | ``` 73 | 127.0.0.1:6380> monitor 74 | ok 75 | 1565688900.609818 [0 127.0.0.1:34344] "EVAL" "\r\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\r\n return redis.call(\"DEL\", KEYS[1])\r\n else\r\n return 0\r\n end" "1" "aaa" "bbb" 76 | 1565688900.609912 [0 lua] "GET" "aaa" 77 | 1565688900.609918 [0 lua] "DEL" "aaa" 78 | ``` 79 | 流程改进如下 80 | 81 | ||client A|client B| 82 | |:---|:---|:---| 83 | |1|随机生成锁的值为rand1,set lock rand1 nx px 3000获得锁成功|| 84 | |2||随机生成锁的值为rand2,set lock rand2 nx px abcd 第1500毫秒,获得锁失败| 85 | |3|第2000毫秒,业务逻辑处理完毕,执行删除lua脚本,命令在网络上发生阻塞,没有传递给redis客户端|| 86 | |4||第2200毫秒,随机生成锁的值为rand3,set lock rand3 nx px 3000 获得锁失败,因为redis没有收到client A的del操作| 87 | |5||第4000毫秒,随机生成锁的值为rand4,set lock rand4 nx px 3000获得锁成功| 88 | |6|lua脚本被redis接受,因为值不相同所以删除失败|lock未超时,client B未处理完业务逻辑| 89 | |7|随机生成锁的值为rand5,set lock rand5 nx px 3000获得锁失败|| 90 | 91 | # 多节点轮流请求的超时时间 92 | 因为不能使用单机、主从和cluster,所以我们使用多节点,每个节点都不能有主从,当请求有count(servers_map) / 2 + 1个数量成功则认为是获取锁成功 93 | 94 | 如有5个redis节点,4个节点都成功set了,就认为获取锁成功 95 | 96 | ||redis A|redis B|redis C|redis D|redis E| 97 | |:---|:---|:---|:---|:---|:---| 98 | |1|set lock 1234 nx px 3000获得锁成功|set lock 1234 nx px 3000获得锁成功|set lock rand nx px 3000获得锁成功|set lock 1234 nx px 3000获得锁失败|set lock 1234 nx px 3000获得锁成功| 99 | 100 | 因为是轮流请求redis节点的,会造成请求拿锁的时机超过锁的超时时间情况 101 | - 锁超时时间为2秒 102 | - 轮流请求5个redis节点,前4个节点锁成功,共用时1秒,第5个节点用时3秒 103 | - 认为拿锁成功,其他客户端同样可以拿到锁,但是前4个节点的锁已经过期 104 | 105 | 解决方案是记录拿锁请求开始时间和拿锁结束时间,对比超时时间 106 | - 记录请求开始时间 107 | - 锁超时时间为2秒 108 | - 轮流请求5个redis节点,前4个节点锁成功,共用时1秒,第5个节点用时3秒,共用时4秒 109 | - 记录请求结束时间 110 | - 因为总请求5秒大于锁超时2秒时间,所以认为拿锁失败 111 | - 轮流放锁 112 | 113 | # php版本redlock源码 114 | 客户端demo如下 115 | ``` 116 | lock('test', 10000); 130 | if ($lock) { 131 | print_r($lock); 132 | } else { 133 | print "Lock not acquired\n"; 134 | } 135 | sleep(1); 136 | } 137 | 138 | ``` 139 | 构造函数如下 140 | ``` 141 | function __construct(array $servers, $retryDelay = 200, $retryCount = 3) 142 | { 143 | //拿锁的redis节点列表 144 | $this->servers = $servers; 145 | //拿锁失败重试的间隔时间 146 | $this->retryDelay = $retryDelay; 147 | //拿锁失败的重试次数 148 | $this->retryCount = $retryCount; 149 | //多少个服务器拿锁成功就认为拿锁成功 150 | $this->quorum = min(count($servers), (count($servers) / 2 + 1)); 151 | } 152 | ``` 153 | 拿锁操作 154 | ``` 155 | public function lock($resource, $ttl) 156 | { 157 | //实例化redis 158 | $this->initInstances(); 159 | //生成随机的值 160 | $token = uniqid(); 161 | 162 | $retry = $this->retryCount; 163 | 164 | do { 165 | $n = 0; 166 | 167 | $startTime = microtime(true) * 1000; 168 | //循环拿锁 169 | foreach ($this->instances as $instance) { 170 | if ($this->lockInstance($instance, $resource, $token, $ttl)) { 171 | $n++; 172 | } 173 | } 174 | 175 | //锁超时时间的一个偏移量 176 | $drift = ($ttl * $this->clockDriftFactor) + 2; 177 | //拿锁请求的时间间隔 178 | $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift; 179 | //判断是否拿锁成功 180 | if ($n >= $this->quorum && $validityTime > 0) { 181 | return [ 182 | 'validity' => $validityTime, 183 | 'resource' => $resource, 184 | 'token' => $token, 185 | ]; 186 | 187 | } else { 188 | //放锁操作 189 | foreach ($this->instances as $instance) { 190 | $this->unlockInstance($instance, $resource, $token); 191 | } 192 | } 193 | 194 | //重试时间间隔 195 | $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay); 196 | usleep($delay * 1000); 197 | 198 | $retry--; 199 | 200 | } while ($retry > 0); 201 | 202 | return false; 203 | } 204 | ``` 205 | 锁命令 206 | ``` 207 | private function lockInstance($instance, $resource, $token, $ttl) 208 | { 209 | return $instance->set($resource, $token, ['NX', 'PX' => $ttl]); 210 | } 211 | ``` 212 | 放锁操作 213 | ``` 214 | public function unlock(array $lock) 215 | { 216 | $this->initInstances(); 217 | $resource = $lock['resource']; 218 | $token = $lock['token']; 219 | 220 | foreach ($this->instances as $instance) { 221 | $this->unlockInstance($instance, $resource, $token); 222 | } 223 | } 224 | private function unlockInstance($instance, $resource, $token) 225 | { 226 | $script = ' 227 | if redis.call("GET", KEYS[1]) == ARGV[1] then 228 | return redis.call("DEL", KEYS[1]) 229 | else 230 | return 0 231 | end 232 | '; 233 | return $instance->eval($script, [$resource, $token], 1); 234 | } 235 | ``` 236 | -------------------------------------------------------------------------------- /composer/执行流程源码分析.md: -------------------------------------------------------------------------------- 1 | 2 | ## 目录 3 | * [基础流程](#基础流程) 4 | * [classMap](#classMap) 5 | * [PSR4](#PSR4) 6 | * [PSR0](#PSR0) 7 | * [使用apcu扩展优化](#使用apcu扩展优化) 8 | 9 | # 基础流程 10 | Composer源码一般都在框架的入口处引入,用来注册一个类自动加载,以Yii2的入口文件为例 11 | ``` 12 | run(); 21 | ``` 22 | Composer源码一般在框架的vendor文件夹下的composer中 23 | 从autoload.php开始,这个文件返回了一个ClassLoader类的实例,这是一个单例模式 24 | ``` 25 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); 48 | if ($useStaticLoader) { 49 | require_once __DIR__ . '/autoload_static.php'; 50 | 51 | call_user_func(\Composer\Autoload\ComposerStaticInit97e0a3a5b1c8c48ec684b54219c87b9c::getInitializer($loader)); 52 | } else { 53 | $map = require __DIR__ . '/autoload_namespaces.php'; 54 | foreach ($map as $namespace => $path) { 55 | $loader->set($namespace, $path); 56 | } 57 | 58 | $map = require __DIR__ . '/autoload_psr4.php'; 59 | foreach ($map as $namespace => $path) { 60 | $loader->setPsr4($namespace, $path); 61 | } 62 | 63 | $classMap = require __DIR__ . '/autoload_classmap.php'; 64 | if ($classMap) { 65 | $loader->addClassMap($classMap); 66 | } 67 | } 68 | //注册类自动加载 69 | $loader->register(true); 70 | 71 | if ($useStaticLoader) { 72 | $includeFiles = Composer\Autoload\ComposerStaticInit97e0a3a5b1c8c48ec684b54219c87b9c::$files; 73 | } else { 74 | $includeFiles = require __DIR__ . '/autoload_files.php'; 75 | } 76 | foreach ($includeFiles as $fileIdentifier => $file) { 77 | composerRequire97e0a3a5b1c8c48ec684b54219c87b9c($fileIdentifier, $file); 78 | } 79 | 80 | return $loader; 81 | } 82 | ``` 83 | 我对这里有一个疑问,如下 84 | ``` 85 | spl_autoload_register(array('ComposerAutoloaderInit97e0a3a5b1c8c48ec684b54219c87b9c', 'loadClassLoader'), true, true); 86 | self::$loader = $loader = new \Composer\Autoload\ClassLoader(); 87 | spl_autoload_unregister(array('ComposerAutoloaderInit97e0a3a5b1c8c48ec684b54219c87b9c', 'loadClassLoader')); 88 | ``` 89 | 这个类自动加载的作用主要是引入ClassLoader.php文件,为何不直接引入而要用自动加载引入???很费解 90 | ``` 91 | public static function loadClassLoader($class) 92 | { 93 | if ('Composer\Autoload\ClassLoader' === $class) { 94 | require __DIR__ . '/ClassLoader.php'; 95 | } 96 | } 97 | ``` 98 | 这个问题我提issue给官方,有兴趣的同学可以去看一下,我是没看懂啥意思,英文实在太差 99 | https://github.com/composer/composer/issues/7684 100 | 因为ClassLoader的prefixLengthsPsr4、prefixDirsPsr4、prefixesPsr0、classMap属性都是私有的,需要从autoload_static这个文件中去赋值,composer使用了闭包绑定机制(我感觉代码很秀) 101 | ``` 102 | //将autoload_static的几个属性赋值给ClassLoader的私有属性 103 | public static function getInitializer(ClassLoader $loader) 104 | { 105 | return \Closure::bind(function () use ($loader) { 106 | $loader->prefixLengthsPsr4 = ComposerStaticInit97e0a3a5b1c8c48ec684b54219c87b9c::$prefixLengthsPsr4; 107 | $loader->prefixDirsPsr4 = ComposerStaticInit97e0a3a5b1c8c48ec684b54219c87b9c::$prefixDirsPsr4; 108 | $loader->prefixesPsr0 = ComposerStaticInit97e0a3a5b1c8c48ec684b54219c87b9c::$prefixesPsr0; 109 | $loader->classMap = ComposerStaticInit97e0a3a5b1c8c48ec684b54219c87b9c::$classMap; 110 | 111 | }, null, ClassLoader::class); 112 | } 113 | ``` 114 | 然后就是注册一个类自动加载,并且将这个自动加载放到加载队列的最前面 115 | ``` 116 | public function register($prepend = false) 117 | { 118 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 119 | } 120 | public function loadClass($class) 121 | { 122 | if ($file = $this->findFile($class)) { 123 | includeFile($file); 124 | 125 | return true; 126 | } 127 | } 128 | ``` 129 | 在findFile方法中,涉及到classMap、PSR4、PSR0三种类查找模式 130 | # classMap 131 | classMap模式是最直观的,直接查找类与真实文件路径的映射关系 132 | ``` 133 | if (isset($this->classMap[$class])) { 134 | return $this->classMap[$class]; 135 | } 136 | //如果classMap查不到的话,或者以前的查找找不到的话,是否直接返回false 137 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 138 | return false; 139 | } 140 | ``` 141 | classMap的缺点就是需要维护的映射关系太大,Yii2的默认classMap有600多行之多 142 | 也可以手动添加classMap,使用addClassMap方法 143 | ``` 144 | public function addClassMap(array $classMap) 145 | { 146 | if ($this->classMap) { 147 | $this->classMap = array_merge($this->classMap, $classMap); 148 | } else { 149 | $this->classMap = $classMap; 150 | } 151 | } 152 | ``` 153 | 获取classMap方法如下 154 | ``` 155 | public function getClassMap() 156 | { 157 | return $this->classMap; 158 | } 159 | ``` 160 | # PSR4 161 | 如果classMap查找模式找不到的话,会去使用PSR4模式 162 | ``` 163 | private function findFileWithExtension($class, $ext) 164 | { 165 | // PSR-4 lookup 166 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 167 | $first = $class[0]; 168 | if (isset($this->prefixLengthsPsr4[$first])) { 169 | $subPath = $class; 170 | while (false !== $lastPos = strrpos($subPath, '\\')) { 171 | $subPath = substr($subPath, 0, $lastPos); 172 | $search = $subPath . '\\'; 173 | if (isset($this->prefixDirsPsr4[$search])) { 174 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 175 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 176 | if (file_exists($file = $dir . $pathEnd)) { 177 | return $file; 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | // PSR-4 fallback dirs 185 | foreach ($this->fallbackDirsPsr4 as $dir) { 186 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 187 | return $file; 188 | } 189 | } 190 | .... 191 | } 192 | ``` 193 | 代码不多但是逻辑比较绕,总体来说就是如下: 194 | 如果要找a\b\c\obj这个类 195 | - 变成a\b\c\obj.php 196 | - 获取第一个字符,为a,判断prefixLengthsPsr4属性是否存在键为a的 197 | - 获取a\b\c\,判断prefixDirsPsr4属性是否存在a\b\c\ 198 | - 如果prefixDirsPsr4设置为如下,则会找__DIR__ . '/../my/test/obj.php'是否存在,不存在则会找__DIR__.'/../XXX/obj.php'是否存在 199 | ``` 200 | public prefixDirsPsr4 = [ 201 | "a\\b\\c\\" => [ 202 | 0 => __DIR__ . '/..' . '/my/test', 203 | 1 => __DIR__ . '/..' . '/xxx', 204 | ], 205 | "a\\b\\" => [ 206 | 0 => __DIR__ . '/..' . '/ok', 207 | 1 => __DIR__ . '/..' . '/notok', 208 | ], 209 | ]; 210 | ``` 211 | - 如果找不到的话会获取a\b,然后会找__DIR__.'/../ok/c/obj.php' 212 | 基本的PSR4找不到的话,会去使用fallbackDirsPsr4模式,默认这个是fallbackDirsPsr4是空的,需要自己去配置 213 | ``` 214 | // PSR-4 fallback dirs 215 | foreach ($this->fallbackDirsPsr4 as $dir) { 216 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 217 | return $file; 218 | } 219 | } 220 | ``` 221 | # PSR0 222 | 如果PSR4也找不到的话会使用PSR0 223 | ``` 224 | // PSR-0 lookup 225 | if (false !== $pos = strrpos($class, '\\')) { 226 | // namespaced class name 227 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 228 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 229 | } else { 230 | // PEAR-like class name 231 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 232 | } 233 | 234 | if (isset($this->prefixesPsr0[$first])) { 235 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 236 | if (0 === strpos($class, $prefix)) { 237 | foreach ($dirs as $dir) { 238 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 239 | return $file; 240 | } 241 | } 242 | } 243 | } 244 | } 245 | 246 | // PSR-0 fallback dirs 247 | foreach ($this->fallbackDirsPsr0 as $dir) { 248 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 249 | return $file; 250 | } 251 | } 252 | 253 | // PSR-0 include paths. 254 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 255 | return $file; 256 | } 257 | ``` 258 | 如果要找a\b\c\obj这个类 259 | prefixesPsr0属性配置如下 260 | ``` 261 | public $prefixesPsr0 = [ 262 | "a"=>[ 263 | "a\\" => [ 264 | 0 => __DIR__.'/..'.'/my', 265 | ], 266 | 'a\\b' => [ 267 | 0 => __DIR__.'/..'.'/you', 268 | ] 269 | ] 270 | ]; 271 | ``` 272 | - 会取第一个字符为a,判断prefixesPsr0是否存在a键 273 | - 然后strpos("a\b\c\obj","a\\"),生效则去循环获取a\\下面数组的路径拼上obj.php文件是否存在 274 | - 不存在则,strpos("a\b\c\obj","a\\b"),生效则去循环获取a\\b下面数组的路径拼上obj.php文件是否存在 275 | 如果PSR0找不到,则会去找fallbackDirsPsr0和fallbackDirsPsr4类似 276 | ``` 277 | foreach ($this->fallbackDirsPsr0 as $dir) { 278 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 279 | return $file; 280 | } 281 | } 282 | ``` 283 | # 使用apcu扩展优化 284 | 默认composer是不会使用apcu进行查找优化的,使用这个优化需要设置apcuPrefix属性,然后开启apcu扩展 285 | 在classMap找不到情况下,会去查找apcu中是否有这个缓存 286 | ``` 287 | if (null !== $this->apcuPrefix) { 288 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 289 | if ($hit) { 290 | return $file; 291 | } 292 | } 293 | ``` 294 | 在PSR0或者PSR4找到了这个文件时候,会缓存进apcu 295 | ``` 296 | if (null !== $this->apcuPrefix) { 297 | apcu_add($this->apcuPrefix.$class, $file); 298 | } 299 | ``` 300 | -------------------------------------------------------------------------------- /img/connection1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhucola/php_frameworks_analysis/abd2d28f6a33bc9c8f78e90bf191662e958f262b/img/connection1.png -------------------------------------------------------------------------------- /phpfpm/reload操作发生502错误的问题分析.md: -------------------------------------------------------------------------------- 1 | nginx配置如下 2 | ``` 3 | server { 4 | listen 80; 5 | root /tmp; 6 | location / { 7 | fastcgi_pass 127.0.0.1:9000; 8 | fastcgi_index index.php; 9 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 10 | include fastcgi_params; 11 | } 12 | } 13 | server { 14 | listern 81; 15 | root /tmp; 16 | location / { 17 | fastcgi_pass 127.0.0.1:9001; 18 | fastcgi_index index.php; 19 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 20 | include fastcgi_params; 21 | } 22 | } 23 | ``` 24 | 配置两个fpm分别为master1和master2 25 | -master1的listen为127.0.0.1:9000,master1的listen为127.0.0.1:9001 26 | -其他均为默认配置 27 | -分别启动master1和master2 28 | ``` 29 | /usr/local/php/sbin/php-fpm --fpm-config /usr/local/php/etc/master1.conf 30 | /usr/local/php/sbin/php-fpm --fpm-config /usr/local/php/etc/master2.conf 31 | ``` 32 | 在/tmp中创建a.php文件 33 | ``` 34 | 'http://127.0.0.1/b.php' 38 | ]); 39 | curl_exec($curl); 40 | ``` 41 | 在/tmp中创建b.php文件 42 | ``` 43 | 'redis']); 22 | ``` 23 | 猜节点算法如下 24 | ``` 25 | $count = count($this->pool); 26 | $index = min((int) ($slot / (int) (16384 / $count)), $count - 1); 27 | $nodes = array_keys($this->pool); 28 | return $nodes[$index]; 29 | ``` 30 | * 将set命令格式化,如果命令是 31 | ``` 32 | $redis->set("a",1234); 33 | ``` 34 | 会变成 35 | ``` 36 | *3 37 | $3 38 | SET 39 | $1 40 | a 41 | $4 42 | 1234 43 | ``` 44 | * 创建与节点的连接,并且发送密码和数据库号(如果配置的话) 45 | 1. 如果连接失败,将该节点从配置列表中删除,然后从配置列表中随机取一个节点去做cluster slots操作,获取所有节点真实的slot信息、主从关系信息,然后重新去用真实的对应关系去匹配节点,用真实的节点重新连接 46 | 2. 然后节点再次连接失败,就直接失败了,代码写死了只有一次重试机会 47 | * 将格式化后的命令发给redis节点 48 | 1. 如果命令发送失败,将该节点从配置列表中删除,然后从配置列表中随机取一个节点重新连接,做cluster slots,获取所有节点真实的slot信息、主从关系信息,然后重新去用真实的对应关系去匹配节点,用真实的节点重新连接,可能重新选择的节点和发生失败的节点是一个,如果再次失败就直接异常 49 | * 这里第一次发送命令失败其实连接的是主节点,如果主节点宕机,redis会将从提升为主,然后PHP获取cluster slots信息的时候,会获取新提升为主节点的这个信息(比如配置了7000端口的master,连接上了7000端口但是在发送命令时候7000宕机,就会获取cluster slots,然后发现7000已经变成了slave,将7005提升成了master,就会去连接7005) 50 | * 读取redis节点的响应 51 | 1. 有MOVED情况,就是节点与slot的对应关系和真实的redis服务器不一样,处理MOVED信息,获取真实的slot和节点信息(从响应信息中获取) 52 | * 给MOVED的节点发cluster slots获取集群所有节点的slot信息、主从关系信息,然后重新用真实的槽节点去发信息 53 | * 如果给MOVED节点发cluster slots失败,就配置的节点列表中删除此节点,并且随机返回一个节点去做cluster slots,然后重新发命令(有可能再次发的节点还是MOVED失败的节点,然后再次失败就是不可用了;或者再次发的节点是已经被提升为master的slave节点) 54 | 2. 响应成功,收到OK 55 | MOVED的响应 56 | ``` 57 | - MOVED 15495 127.0.0.1:7002 58 | ``` 59 | 成功的响应 60 | ``` 61 | + OK 62 | ``` 63 | # CRC16算法源码 64 | 根据key获取slot的方法在predis\src\Cluster\ClusterStrategy.php里面,getSlot 65 | ``` 66 | public function getSlot(CommandInterface $command) 67 | { 68 | $slot = $command->getSlot(); 69 | if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) { 70 | 71 | $key = call_user_func($this->commands[$cmdID], $command); 72 | if (isset($key)) { 73 | $slot = $this->getSlotByKey($key); 74 | $command->setSlot($slot); 75 | } 76 | } 77 | 78 | return $slot; 79 | } 80 | ``` 81 | 会调用getSlotByKey方法 82 | ``` 83 | public function getSlotByKey($key) 84 | { 85 | $key = $this->extractKeyTag($key); //获取key 86 | $slot = $this->hashGenerator->hash($key) & 0x3FFF; //做hash后和16383取余 87 | 88 | return $slot; 89 | } 90 | ``` 91 | 核心的CRC16算法在predis\src\Cluster\Hash\CRC16.php,可以直接拿来用 92 | ``` 93 | private static $CCITT_16 = array( 94 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 95 | 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 96 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 97 | 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 98 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 99 | 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 100 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 101 | 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 102 | 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 103 | 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 104 | 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 105 | 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 106 | 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 107 | 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 108 | 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 109 | 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 110 | 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 111 | 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 112 | 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 113 | 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 114 | 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 115 | 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 116 | 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 117 | 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 118 | 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 119 | 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 120 | 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 121 | 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 122 | 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 123 | 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 124 | 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 125 | 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0, 126 | ); 127 | public function hash($value) 128 | { 129 | // CRC-CCITT-16 algorithm 130 | $crc = 0; 131 | $CCITT_16 = self::$CCITT_16; 132 | 133 | $value = (string) $value; 134 | $strlen = strlen($value); 135 | 136 | for ($i = 0; $i < $strlen; ++$i) { 137 | $crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF; 138 | } 139 | 140 | return $crc; 141 | } 142 | ``` 143 | 这个算法和redis算slot的结果是一样的,a的在PHP中算的slot也是15495 144 | ``` 145 | 127.0.0.1:7001> set a 123 146 | -> Redirected to slot [15495] located at 127.0.0.1:7002 147 | OK 148 | ``` 149 | # 核心逻辑源码 150 | Cluster执行一条命令走的是predis\src\Connection\Aggregate\PredisCluster.php里面的executeCommand方法 151 | ``` 152 | public function executeCommand(CommandInterface $command) 153 | { 154 | $response = $this->retryCommandOnFailure($command, __FUNCTION__); 155 | 156 | if ($response instanceof ErrorResponseInterface) { //move或者master不可用会执行 157 | return $this->onErrorResponse($command, $response); 158 | } 159 | 160 | return $response; 161 | } 162 | ``` 163 | retryCommandOnFailure方法就是去连接客户端、执行命令、捕获异常方法,这个方法居然用到了goto,这是我第一次在PHP程序里面看到goto 164 | ``` 165 | private function retryCommandOnFailure(CommandInterface $command, $method) 166 | { 167 | $failure = false; 168 | RETRY_COMMAND: { 169 | try { 170 | $response = $this->getConnection($command)->$method($command); 171 | } catch (ConnectionException $exception) { 172 | $connection = $exception->getConnection(); 173 | $connection->disconnect(); 174 | 175 | $this->remove($connection); 176 | 177 | if ($failure) { //注意这里下下面的$failure=true,所以如果第一次连接异常再次连接再异常的话,就直接不可用了 178 | throw $exception; 179 | } elseif ($this->useClusterSlots) { 180 | $this->askSlotsMap(); 181 | } 182 | 183 | $failure = true; 184 | 185 | goto RETRY_COMMAND; 186 | } 187 | } 188 | 189 | return $response; 190 | } 191 | ``` 192 | 由于php客户端不知道这个slot应该连接哪个redis节点,所以predis需要去猜一个节点 193 | ``` 194 | public function getConnection(CommandInterface $command) 195 | { 196 | //获取slot 197 | $slot = $this->strategy->getSlot($command); 198 | if (!isset($slot)) { 199 | throw new NotSupportedException( 200 | "Cannot use '{$command->getId()}' with redis-cluster." 201 | ); 202 | } 203 | 204 | if (isset($this->slots[$slot])) { 205 | //如果这个slot和节点有对应关系 206 | return $this->slots[$slot]; 207 | } else { 208 | //根据slot来猜一个节点 209 | return $this->getConnectionBySlot($slot); 210 | } 211 | } 212 | public function getConnectionBySlot($slot) 213 | { 214 | //判断slot是否合法 215 | if ($slot < 0x0000 || $slot > 0x3FFF) { 216 | throw new \OutOfBoundsException("Invalid slot [$slot]."); 217 | } 218 | 219 | if (isset($this->slots[$slot])) { 220 | return $this->slots[$slot]; 221 | } 222 | //猜一个节点 223 | $connectionID = $this->guessNode($slot); 224 | if (!$connection = $this->getConnectionById($connectionID)) { 225 | $connection = $this->createConnection($connectionID); 226 | $this->pool[$connectionID] = $connection; 227 | } 228 | //先存一个slot和猜出来的节点的对应关系 229 | return $this->slots[$slot] = $connection; 230 | } 231 | ``` 232 | 然后就是连接服务、发送命令、处理响应操作 233 | 会先将命令格式化,处理成redis能读懂的格式 234 | ``` 235 | public function writeRequest(CommandInterface $command) 236 | { 237 | $commandID = $command->getId(); 238 | $arguments = $command->getArguments(); 239 | 240 | $cmdlen = strlen($commandID); 241 | $reqlen = count($arguments) + 1; 242 | 243 | $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; 244 | 245 | foreach ($arguments as $argument) { 246 | $arglen = strlen($argument); 247 | $buffer .= "\${$arglen}\r\n{$argument}\r\n"; 248 | } 249 | $this->write($buffer); 250 | } 251 | ``` 252 | 然后去连接redis 253 | ``` 254 | protected function tcpStreamInitializer(ParametersInterface $parameters) 255 | { 256 | if (!filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 257 | $address = "tcp://$parameters->host:$parameters->port"; 258 | } else { 259 | $address = "tcp://[$parameters->host]:$parameters->port"; 260 | } 261 | 262 | $flags = STREAM_CLIENT_CONNECT; 263 | 264 | if (isset($parameters->async_connect) && $parameters->async_connect) { 265 | $flags |= STREAM_CLIENT_ASYNC_CONNECT; 266 | } 267 | if (isset($parameters->persistent)) { 268 | if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { 269 | $flags |= STREAM_CLIENT_PERSISTENT; 270 | 271 | if ($persistent === null) { 272 | $address = "{$address}/{$parameters->persistent}"; 273 | } 274 | } 275 | } 276 | $resource = $this->createStreamSocket($parameters, $address, $flags); 277 | 278 | return $resource; 279 | } 280 | protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) 281 | { 282 | $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); 283 | if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags)) { 284 | $this->onConnectionError(trim($errstr), $errno); 285 | } 286 | 287 | if (isset($parameters->read_write_timeout)) { 288 | $rwtimeout = (float) $parameters->read_write_timeout; 289 | $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; 290 | $timeoutSeconds = floor($rwtimeout); 291 | $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; 292 | stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); 293 | } 294 | 295 | if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) { 296 | $socket = socket_import_stream($resource); 297 | socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay); 298 | } 299 | 300 | return $resource; 301 | } 302 | ``` 303 | 最后发命令并读取响应 304 | ``` 305 | public function read() 306 | { 307 | $socket = $this->getResource(); 308 | $chunk = fgets($socket); 309 | if ($chunk === false || $chunk === '') { 310 | $this->onConnectionError('Error while reading line from the server.'); 311 | } 312 | $prefix = $chunk[0]; 313 | $payload = substr($chunk, 1, -2); 314 | 315 | switch ($prefix) { 316 | case '+': 317 | return StatusResponse::get($payload); 318 | 319 | case '$': 320 | $size = (int) $payload; 321 | 322 | if ($size === -1) { 323 | return; 324 | } 325 | 326 | $bulkData = ''; 327 | $bytesLeft = ($size += 2); 328 | 329 | do { 330 | $chunk = fread($socket, min($bytesLeft, 4096)); 331 | 332 | if ($chunk === false || $chunk === '') { 333 | $this->onConnectionError('Error while reading bytes from the server.'); 334 | } 335 | 336 | $bulkData .= $chunk; 337 | $bytesLeft = $size - strlen($bulkData); 338 | } while ($bytesLeft > 0); 339 | 340 | return substr($bulkData, 0, -2); 341 | 342 | case '*': 343 | $count = (int) $payload; 344 | 345 | if ($count === -1) { 346 | return; 347 | } 348 | 349 | $multibulk = array(); 350 | 351 | for ($i = 0; $i < $count; ++$i) { 352 | $multibulk[$i] = $this->read(); 353 | } 354 | 355 | return $multibulk; 356 | 357 | case ':': 358 | $integer = (int) $payload; 359 | return $integer == $payload ? $integer : $payload; 360 | 361 | case '-': 362 | return new ErrorResponse($payload); 363 | 364 | default: 365 | $this->onProtocolError("Unknown response prefix: '$prefix'."); 366 | 367 | return; 368 | } 369 | } 370 | ``` 371 | 如果有异常,比如发生了MOVED,就会去处理报错信息 372 | ``` 373 | protected function onMovedResponse(CommandInterface $command, $details) 374 | { 375 | list($slot, $connectionID) = explode(' ', $details, 2); 376 | if (!$connection = $this->getConnectionById($connectionID)) { 377 | $connection = $this->createConnection($connectionID); 378 | } 379 | 380 | if ($this->useClusterSlots) { 381 | $this->askSlotsMap($connection); 382 | } 383 | $this->move($connection, $slot); 384 | $response = $this->executeCommand($command); 385 | 386 | return $response; 387 | } 388 | ``` 389 | 然后用MOVED指向的真实节点去获取所有节点的槽、主从对应关系,会发送一个cluster slots命令 390 | ``` 391 | public function askSlotsMap(NodeConnectionInterface $connection = null) 392 | { 393 | if (!$connection && !$connection = $this->getRandomConnection()) { 394 | return array(); 395 | } 396 | $this->resetSlotsMap(); 397 | 398 | $response = $this->queryClusterNodeForSlotsMap($connection); 399 | foreach ($response as $slots) { 400 | // We only support master servers for now, so we ignore subsequent 401 | // elements in the $slots array identifying slaves. 402 | list($start, $end, $master) = $slots; 403 | if ($master[0] === '') { 404 | $this->setSlots($start, $end, (string) $connection); 405 | } else { 406 | $this->setSlots($start, $end, "{$master[0]}:{$master[1]}"); 407 | } 408 | } 409 | return $this->slotsMap; 410 | } 411 | ``` 412 | 然后会再次处理发送的set命令,再次走到executeCommand里面 413 | -------------------------------------------------------------------------------- /predis/redisSentinel模式源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [哨兵的问题](#哨兵的问题) 3 | * [predis存在的问题](#predis存在的问题) 4 | * [整体运行流程](#整体流程) 5 | * [核心逻辑源码](#核心逻辑源码) 6 | 7 | # 哨兵的问题 8 | 哨兵存在的问题 9 | - 哨兵宕机 10 | - 通过哨兵获取节点,然后操作节点时候节点宕机 11 | - 通过哨兵获取master节点,发送写命令,但是此时master节点已经变成了slave节点,不可写 12 | 13 | ## 对于哨兵宕机问题: 14 | 可以使用多节点哨兵来监控主从节点,如一个哨兵节点连接异常等,可以使用下一个哨兵节点 15 | 16 | ## 对于通过哨兵获取节点,然后操作节点时候节点宕机问题 17 | 可能节点已宕机,哨兵还在主观下线状态,重新从哨兵获取主从节点关系,直到节点进入客观下线状态并且故障转移结束 18 | 19 | ## 通过哨兵获取master节点,发送写命令,但是此时master节点已经变成了slave节点,不可写问题 20 | 可能节点已宕机并且重新上线,上线后会变成slave节点,需要重新从哨兵获取主从节点关系 21 | 22 | 以上问题predis框架都进行了处理 23 | # predis存在的问题 24 | 如果配置为如下 25 | ``` 26 | 'sentinel', 34 | 'service' => 'mymaster', //哨兵中监控的主节点名称 35 | 'parameters' => [ 36 | 'password' => 123456, //主从节点的密码 37 | ], 38 | ]; 39 | 40 | $client = new Predis\Client($sentinels, $options); 41 | 42 | $client -> set("name",123); 43 | $client -> get("name"); 44 | ``` 45 | 每次predis会通过第一个哨兵配置去获取主从节点信息,第一个哨兵压力很大,可以优化为 46 | ``` 47 | $sentinels = shuffle(['tcp://192.168.124.10:26379', 'tcp://192.168.124.10:26380', 'tcp://192.168.124.10:26381']); 48 | ``` 49 | # 整体运行流程 50 | 51 | * 初始化predis项目 52 | * 判断是否已经有连接过的redis接点,获取命令是可读还是可写 53 | 1. 如果节点是master则直接使用master节点去读写 54 | 2. 如果节点是slave,命令是读命令则直接使用slave节点 55 | 3. 如果节点是slave,命令是写命令则通过哨兵获取master节点 56 | - 如果没有连接的redis节点,客户端连接配置列表中的第一个哨兵,并将这个哨兵从配置列表属性中移除 57 | 1. 需要获取master节点,向哨兵发送sentinel get-master-addr-by-name $service 58 | 2. 需要获取slave节点,向哨兵发送sentinel slaves $service,过滤掉slave的状态为s_down、o_down、disconnected的节点 59 | - 如果连接哨兵异常,则使用配置列表的第一个哨兵继续连接,并将这个哨兵从配置列表中移除,直到哨兵节点可用或者将全部配置列表属性为空 60 | - 向redis节点发送命令 61 | - 如果redis节点异常,则默认停止1秒,继续连接哨兵获取节点,然后使用获取的节点发送命令,重试20次,每次重试停止1秒 62 | 63 | # 核心逻辑源码 64 | 获取节点发送命令 65 | ``` 66 | private function retryCommandOnFailure(CommandInterface $command, $method) 67 | { 68 | $retries = 0; 69 | SENTINEL_RETRY: { 70 | try { 71 | $response = $this->getConnection($command)->$method($command); 72 | } catch (CommunicationException $exception) { 73 | //这里是redis节点异常重试机制 74 | $this->wipeServerList(); 75 | $exception->getConnection()->disconnect(); 76 | //默认重试20次 77 | if ($retries == $this->retryLimit) { 78 | throw $exception; 79 | } 80 | //默认每次重试停止1秒,让哨兵进行故障转移 81 | usleep($this->retryWait * 1000); 82 | 83 | ++$retries; 84 | goto SENTINEL_RETRY; 85 | } 86 | } 87 | 88 | return $response; 89 | } 90 | ``` 91 | 获取redis节点 92 | ``` 93 | public function getConnection(CommandInterface $command) 94 | { 95 | //获取redis节点 96 | $connection = $this->getConnectionInternal($command); 97 | if (!$connection->isConnected()) { 98 | // When we do not have any available slave in the pool we can expect 99 | // read-only operations to hit the master server. 100 | $expectedRole = $this->strategy->isReadOperation($command) && $this->slaves ? 'slave' : 'master'; 101 | $this->assertConnectionRole($connection, $expectedRole); 102 | } 103 | 104 | return $connection; 105 | } 106 | private function getConnectionInternal(CommandInterface $command) 107 | { 108 | //如果还没有连接过的redis节点 109 | if (!$this->current) { 110 | //判断命令是可读还是可写,读命令连slave,写命令连master 111 | if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { 112 | $this->current = $slave; 113 | } else { 114 | $this->current = $this->getMaster(); 115 | } 116 | return $this->current; 117 | } 118 | //如果连接过redis节点,并且是master节点 119 | if ($this->current === $this->master) { 120 | return $this->current; 121 | } 122 | //如果连接的是slave redis节点,并且命令是写操作 123 | if (!$this->strategy->isReadOperation($command)) { 124 | //重新连接master节点 125 | $this->current = $this->getMaster(); 126 | } 127 | 128 | return $this->current; 129 | } 130 | ``` 131 | 通过哨兵获取节点信息 132 | ``` 133 | public function getSentinelConnection() 134 | { 135 | if (!$this->sentinelConnection) { 136 | if (!$this->sentinels) { 137 | throw new \Predis\ClientException('No sentinel server available for autodiscovery.'); 138 | } 139 | //从哨兵配置列表中获取第一个哨兵,并且移除 140 | $sentinel = array_shift($this->sentinels); 141 | $this->sentinelConnection = $this->createSentinelConnection($sentinel); 142 | } 143 | 144 | return $this->sentinelConnection; 145 | } 146 | protected function querySentinelForMaster(NodeConnectionInterface $sentinel, $service) 147 | { 148 | //向哨兵发送sentinel get-master-addr-by-name $service命令,获取master 149 | 节点信息 150 | $payload = $sentinel->executeCommand( 151 | RawCommand::create('SENTINEL', 'get-master-addr-by-name', $service) 152 | ); 153 | if ($payload === null) { 154 | throw new ServerException('ERR No such master with that name'); 155 | } 156 | 157 | if ($payload instanceof ErrorResponseInterface) { 158 | $this->handleSentinelErrorResponse($sentinel, $payload); 159 | } 160 | 161 | return array( 162 | 'host' => $payload[0], 163 | 'port' => $payload[1], 164 | 'alias' => 'master', 165 | ); 166 | } 167 | public function getMaster() 168 | { 169 | if ($this->master) { 170 | return $this->master; 171 | } 172 | if ($this->updateSentinels) { 173 | $this->updateSentinels(); 174 | } 175 | 176 | SENTINEL_QUERY: { 177 | $sentinel = $this->getSentinelConnection(); 178 | try { 179 | //获取主节点信息 180 | $masterParameters = $this->querySentinelForMaster($sentinel, $this->service); 181 | $masterConnection = $this->connectionFactory->create($masterParameters); 182 | $this->add($masterConnection); 183 | } catch (ConnectionException $exception) { 184 | $this->sentinelConnection = null; 185 | 186 | goto SENTINEL_QUERY; 187 | } 188 | } 189 | 190 | return $masterConnection; 191 | } 192 | public function getSlaves() 193 | { 194 | if ($this->slaves) { 195 | return array_values($this->slaves); 196 | } 197 | 198 | if ($this->updateSentinels) { 199 | $this->updateSentinels(); 200 | } 201 | 202 | SENTINEL_QUERY: { 203 | $sentinel = $this->getSentinelConnection(); 204 | 205 | try { 206 | //获取slave节点信息 207 | $slavesParameters = $this->querySentinelForSlaves($sentinel, $this->service); 208 | //将所有slave节点信息添加进属性里面 209 | foreach ($slavesParameters as $slaveParameters) { 210 | $this->add($this->connectionFactory->create($slaveParameters)); 211 | } 212 | } catch (ConnectionException $exception) { 213 | $this->sentinelConnection = null; 214 | 215 | goto SENTINEL_QUERY; 216 | } 217 | } 218 | 219 | return array_values($this->slaves ?: array()); 220 | } 221 | protected function pickSlave() 222 | { 223 | if ($slaves = $this->getSlaves()) { 224 | //会随机拿一个slave节点 225 | return $slaves[rand(1, count($slaves)) - 1]; 226 | } 227 | } 228 | ``` 229 | -------------------------------------------------------------------------------- /workerman/TcpConnection类的生存周期.md: -------------------------------------------------------------------------------- 1 | 最近发现了一个问题,TcpConnection类是如何被销毁的,在反复翻看源码之后终于找到了答案,不禁感叹一下wm作者的牛逼 2 | 3 | 在master进程创建了mainSocket之后,会fork子进程,然后子进程会将mainSocket扔到IO里面,进行read的事件监听 4 | ``` 5 | //每个子进程都会将mainSocket扔到IO里面,然后IO去监听read事件 6 | public function resumeAccept() 7 | { 8 | // Register a listener to be notified when server socket is ready to read. 9 | if (static::$globalEvent && true === $this->_pauseAccept && $this->_mainSocket) { 10 | if ($this->transport !== 'udp') { 11 | static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection')); 12 | } else { 13 | static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptUdpConnection')); 14 | } 15 | $this->_pauseAccept = false; 16 | } 17 | } 18 | ``` 19 | 然后如果有新连接请求进来,read事件会触发回调acceptConnection,具体做的步骤如下 20 | - accept一个新的资源类型new_socket, 21 | - 惊群操作处理 22 | - 实例化TcpConnection类 23 | - 将new_socket扔到IO里面,监听read事件,与之绑定的回调为TcpConnection的非静态方法baseRead 24 | - 将TcpConnection对象保存在TcpConnection类的静态属性connections 25 | - 将TcpConnection对象保存在Worker对象的worker非静态属性connections里面 26 | ``` 27 | public function acceptConnection($socket) 28 | { 29 | \set_error_handler(function(){}); 30 | //接受请求 31 | $new_socket = stream_socket_accept($socket, 0, $remote_address); 32 | \restore_error_handler(); 33 | //惊群处理 34 | if (!$new_socket) { 35 | return; 36 | } 37 | 38 | // TcpConnection. 39 | $connection = new TcpConnection($new_socket, $remote_address); 40 | 将TcpConnection对象保存在Worker对象的worker非静态属性connections里面 41 | $this->connections[$connection->id] = $connection; 42 | $connection->worker = $this; 43 | .... 44 | } 45 | public function __construct($socket, $remote_address = '') 46 | { 47 | ... 48 | //将new_socket扔到IO里面,监听read事件,与之绑定的回调为TcpConnection的非静态方法baseRead 49 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); 50 | ... 51 | //将TcpConnection对象保存在TcpConnection类的静态属性connections 52 | static::$connections[$this->id] = $this; 53 | } 54 | ``` 55 | 此时TcpConnection类引用规则如下 56 | - 自身的$this 57 | - baseRead绑定了一个$this 58 | - TcpConnection类的静态属性connections有一个$this 59 | - Worker对象的worker非静态属性connections有一个$this 60 | 61 | 如果new_socket有close行为,会触发TcpConnection的destroy方法,然后会有两个unset操作 62 | - unset掉TcpConnection类的静态属性connections的$this 63 | - unset掉Worker对象的worker非静态属性connections的$this 64 | 65 | 那么问题是自身的$this和baseRead的$this是如何销毁的呢???? 66 | 67 | 核心就在于向IO扔mainSocket和new_socket时候,如果事件被触发,会用call_user_func_array调用 68 | ``` 69 | //IO select 70 | if ($read) { 71 | foreach ($read as $fd) { 72 | $fd_key = (int)$fd; 73 | if (isset($this->_allEvents[$fd_key][self::EV_READ])) { 74 | \call_user_func_array($this->_allEvents[$fd_key][self::EV_READ][0], 75 | array($this->_allEvents[$fd_key][self::EV_READ][1])); 76 | } 77 | } 78 | } 79 | 80 | if ($write) { 81 | foreach ($write as $fd) { 82 | $fd_key = (int)$fd; 83 | if (isset($this->_allEvents[$fd_key][self::EV_WRITE])) { 84 | \call_user_func_array($this->_allEvents[$fd_key][self::EV_WRITE][0], 85 | array($this->_allEvents[$fd_key][self::EV_WRITE][1])); 86 | } 87 | } 88 | } 89 | 90 | if($except) { 91 | foreach($except as $fd) { 92 | $fd_key = (int) $fd; 93 | if(isset($this->_allEvents[$fd_key][self::EV_EXCEPT])) { 94 | \call_user_func_array($this->_allEvents[$fd_key][self::EV_EXCEPT][0], 95 | array($this->_allEvents[$fd_key][self::EV_EXCEPT][1])); 96 | } 97 | } 98 | } 99 | ``` 100 | 在第一次触发mainSocket的read事件后,call_user_func_array触发acceptConnection方法,执行结束后TcpConnection本身的$this就已经被销毁了,但是还有其他引用存在 101 | 在资源close后的destroy方法中,会销毁掉TcpConnection类的静态属性connections的$this和worker非静态属性connections的$this 102 | 在new_socket触发read事件后,call_user_func_array触发baseRead方法,执行结束后与baseRead关联的$this也会被销毁 103 | 同样写操作也是使用call_user_func_array调用的,执行结束后也会销毁 104 | 105 | 这样当TcpConnection的全部引用都销毁后会触发TcpConnction的魔术方法 106 | ``` 107 | public function __destruct() 108 | { 109 | static $mod; 110 | self::$statistics['connection_count']--; 111 | if (Worker::getGracefulStop()) { 112 | if (!isset($mod)) { 113 | $mod = ceil((self::$statistics['connection_count'] + 1) / 3); 114 | } 115 | 116 | if (0 === self::$statistics['connection_count'] % $mod) { 117 | Worker::log('worker[' . \posix_getpid() . '] remains ' . self::$statistics['connection_count'] . ' connection(s)'); 118 | } 119 | 120 | if(0 === self::$statistics['connection_count']) { 121 | Worker::stopAll(); 122 | } 123 | } 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /yii2/[Cache与过滤器一]数据缓存源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [文件缓存](#文件缓存) 3 | * [Redis缓存](#Redis缓存) 4 | 5 | **缓存只讲一下get、set、gc方法,其他方法源码很简单,不做涉及** 6 | 7 | # 文件缓存 8 | 缓存组件通过依赖注入到组件中,默认使用的是FileCache缓存 9 | ``` 10 | 'components' => [ 11 | .... 12 | 'cache' => [ 13 | 'class' => 'yii\caching\FileCache', 14 | ], 15 | .... 16 | ], 17 | ``` 18 | 这样就可以在应用中通过cache组件来使用缓存了 19 | ``` 20 | $cache = Yii::$app->get("cache"); 21 | $cache->set("a",1); 22 | echo $cache->get("a"); 23 | ``` 24 | 缓存类都要继承于底层yii\caching\Cache类,源码很简单,涉及的方法和属性也比较少,如果自己封装一套cache需要强制实现抽象方法 25 | ``` 26 | abstract protected function getValue($key); 27 | abstract protected function setValue($key, $value, $duration); 28 | abstract protected function addValue($key, $value, $duration); 29 | abstract protected function deleteValue($key); 30 | abstract protected function flushValues(); 31 | ``` 32 | 因为FileCache类有init,就是在实例化后需要走的第一个方法 33 | ``` 34 | public function init() 35 | { 36 | parent::init(); 37 | //缓存路径别名,可以配成@开头的,也可以配置成全路径的 38 | $this->cachePath = Yii::getAlias($this->cachePath); 39 | if (!is_dir($this->cachePath)) { 40 | //如果目录不存在就创建目录 41 | FileHelper::createDirectory($this->cachePath, $this->dirMode, true); 42 | } 43 | } 44 | ``` 45 | 默认的缓存路径在 46 | ``` 47 | public $cachePath = '@runtime/cache'; 48 | ``` 49 | 缓存文件后缀为 50 | ``` 51 | public $cacheFileSuffix = '.bin'; 52 | ``` 53 | 目录深度为 54 | ``` 55 | public $directoryLevel = 1; 56 | ``` 57 | 文件权限为 58 | ``` 59 | public $dirMode = 0775; 60 | ``` 61 | 还需要执行一下父类的构造函数,去判断igbinary扩展是否可用,igbinary相比于serialize长度更短,大小更小 62 | ``` 63 | public function init() 64 | { 65 | parent::init(); 66 | $this->_igbinaryAvailable = \extension_loaded('igbinary'); 67 | } 68 | ``` 69 | 创建目录是一个递归操作 70 | ``` 71 | public static function createDirectory($path, $mode = 0775, $recursive = true) 72 | { 73 | if (is_dir($path)) { 74 | return true; 75 | } 76 | $parentDir = dirname($path); 77 | // recurse if parent dir does not exist and we are not at the root of the file system. 78 | if ($recursive && !is_dir($parentDir) && $parentDir !== $path) { 79 | static::createDirectory($parentDir, $mode, true); 80 | } 81 | try { 82 | if (!mkdir($path, $mode)) { 83 | return false; 84 | } 85 | } catch (\Exception $e) { 86 | if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288 87 | throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e); 88 | } 89 | } 90 | try { 91 | return chmod($path, $mode); 92 | } catch (\Exception $e) { 93 | throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e); 94 | } 95 | } 96 | ``` 97 | 设置缓存的set操作如下 98 | ``` 99 | public function set($key, $value, $duration = null, $dependency = null) 100 | { 101 | //过期时间 102 | if ($duration === null) { 103 | $duration = $this->defaultDuration; 104 | } 105 | //依赖 106 | if ($dependency !== null && $this->serializer !== false) { 107 | $dependency->evaluateDependency($this); 108 | } 109 | //对值进行序列号 110 | if ($this->serializer === null) { 111 | $value = serialize([$value, $dependency]); 112 | } elseif ($this->serializer !== false) { 113 | $value = call_user_func($this->serializer[0], [$value, $dependency]); 114 | } 115 | //构建key,因为key可以是一个数组 116 | $key = $this->buildKey($key); 117 | 118 | return $this->setValue($key, $value, $duration); 119 | } 120 | ``` 121 | 因为key不仅仅可以是字符串,yii缓存的key还可以是数组,所以需要构建key 122 | ``` 123 | public function buildKey($key) 124 | { 125 | if (is_string($key)) { 126 | //长度大于32就直接md5 127 | $key = ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key); 128 | } else { 129 | if ($this->_igbinaryAvailable) { 130 | $serializedKey = igbinary_serialize($key); 131 | } else { 132 | $serializedKey = serialize($key); 133 | } 134 | 135 | $key = md5($serializedKey); 136 | } 137 | //键名拼键前缀 138 | return $this->keyPrefix . $key; 139 | } 140 | ``` 141 | 获取缓存的get操作如下 142 | ``` 143 | public function get($key) 144 | { 145 | $key = $this->buildKey($key); 146 | $value = $this->getValue($key); 147 | if ($value === false || $this->serializer === false) { 148 | return $value; 149 | } elseif ($this->serializer === null) { 150 | $value = unserialize($value); 151 | } else { 152 | $value = call_user_func($this->serializer[1], $value); 153 | } 154 | if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->isChanged($this))) { 155 | return $value[0]; 156 | } 157 | 158 | return false; 159 | } 160 | ``` 161 | Cache的get、set会执行真正缓存相关类的getValue、setValue 162 | ``` 163 | protected function setValue($key, $value, $duration) 164 | { 165 | //gc逻辑 166 | $this->gc(); 167 | //获取缓存文件绝对路径 168 | $cacheFile = $this->getCacheFile($key); 169 | if ($this->directoryLevel > 0) { 170 | //缓存文件深度 171 | @FileHelper::createDirectory(dirname($cacheFile), $this->dirMode, true); 172 | } 173 | if (is_file($cacheFile) && function_exists('posix_geteuid') && fileowner($cacheFile) !== posix_geteuid()) { 174 | @unlink($cacheFile); 175 | } 176 | if (@file_put_contents($cacheFile, $value, LOCK_EX) !== false) { 177 | if ($this->fileMode !== null) { 178 | @chmod($cacheFile, $this->fileMode); 179 | } 180 | if ($duration <= 0) { 181 | $duration = 31536000; // 1 year 182 | } 183 | //更新修改时间 184 | return @touch($cacheFile, $duration + time()); 185 | } 186 | 187 | $error = error_get_last(); 188 | Yii::warning("Unable to write cache file '{$cacheFile}': {$error['message']}", __METHOD__); 189 | return false; 190 | } 191 | ``` 192 | 缓存文件是有深度的,会根据key来做深度 193 | ``` 194 | protected function getCacheFile($key) 195 | { 196 | if ($this->directoryLevel > 0) { 197 | $base = $this->cachePath; 198 | for ($i = 0; $i < $this->directoryLevel; ++$i) { 199 | if (($prefix = substr($key, $i + $i, 2)) !== false) { 200 | $base .= DIRECTORY_SEPARATOR . $prefix; 201 | } 202 | } 203 | 204 | return $base . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; 205 | } 206 | 207 | return $this->cachePath . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; 208 | } 209 | ``` 210 | 设置缓存有几率走gc操作,也可以使用flush方法来强制执行gc 211 | ``` 212 | public function gc($force = false, $expiredOnly = true) 213 | { 214 | if ($force || mt_rand(0, 1000000) < $this->gcProbability) { 215 | $this->gcRecursive($this->cachePath, $expiredOnly); 216 | } 217 | } 218 | protected function gcRecursive($path, $expiredOnly) 219 | { 220 | if (($handle = opendir($path)) !== false) { 221 | while (($file = readdir($handle)) !== false) { 222 | if ($file[0] === '.') { 223 | continue; 224 | } 225 | $fullPath = $path . DIRECTORY_SEPARATOR . $file; 226 | if (is_dir($fullPath)) { 227 | $this->gcRecursive($fullPath, $expiredOnly); 228 | if (!$expiredOnly) { 229 | if (!@rmdir($fullPath)) { 230 | $error = error_get_last(); 231 | Yii::warning("Unable to remove directory '{$fullPath}': {$error['message']}", __METHOD__); 232 | } 233 | } 234 | } elseif (!$expiredOnly || $expiredOnly && @filemtime($fullPath) < time()) { 235 | if (!@unlink($fullPath)) { 236 | $error = error_get_last(); 237 | Yii::warning("Unable to remove file '{$fullPath}': {$error['message']}", __METHOD__); 238 | } 239 | } 240 | } 241 | closedir($handle); 242 | } 243 | } 244 | ``` 245 | getValue方法比较简单 246 | ``` 247 | protected function getValue($key) 248 | { 249 | $cacheFile = $this->getCacheFile($key); 250 | 251 | if (@filemtime($cacheFile) > time()) { 252 | $fp = @fopen($cacheFile, 'r'); 253 | if ($fp !== false) { 254 | @flock($fp, LOCK_SH); 255 | $cacheValue = @stream_get_contents($fp); 256 | @flock($fp, LOCK_UN); 257 | @fclose($fp); 258 | return $cacheValue; 259 | } 260 | } 261 | 262 | return false; 263 | } 264 | ``` 265 | # Redis缓存 266 | 使用Redis缓存需要安装yii-redis,相关的redis基本源码可以参考[Connection源码](/yii2/%5Bredis%5DConnection源码.md) 267 | Redis缓存是可以使用从读主写模式的,默认是无主从关系,需要配置 268 | ``` 269 | public function actionTest(){ 270 | $cache = Yii::$app->get("cache"); 271 | $cache->enableReplicas = true; 272 | $cache->replicas = [ 273 | ['hostname' => '192.168.124.10','port' => 6380,'database' => 0], //也可以在web.php中配置cache信息 274 | ]; 275 | $key = [3,4,5]; 276 | $value = [1,2,3]; 277 | $cache->set($key,$value); //写操作走6379,在web.php中配置的cache信息 278 | var_dump($cache->get($key)); 279 | } 280 | ``` 281 | 获取从节点的代码如下 282 | ``` 283 | protected function getReplica() 284 | { 285 | if ($this->enableReplicas === false) { 286 | return $this->redis; 287 | } 288 | if ($this->_replica !== null) { 289 | return $this->_replica; 290 | } 291 | 292 | if (empty($this->replicas)) { 293 | return $this->_replica = $this->redis; 294 | } 295 | 296 | $replicas = $this->replicas; 297 | //打乱顺序 298 | shuffle($replicas); 299 | $config = array_shift($replicas); 300 | $this->_replica = Instance::ensure($config, Connection::className()); 301 | return $this->_replica; 302 | } 303 | ``` 304 | 因为使用了redis组件,所以redis属性可以直接操作各种原始方法 305 | ``` 306 | $cache = Yii::$app->get("cache"); 307 | $cache -> redis -> get("a"); 308 | ``` 309 | 其他cache相关的get、set等操作其实就是redis的原始操作 310 | -------------------------------------------------------------------------------- /yii2/[Cache与过滤器三]Page缓存源码.md: -------------------------------------------------------------------------------- 1 | **PageCache还是需要走到PHP里面取做cache,应该使用nginx的proxy-cache来做PageCache** 2 | **不管是yii的PageCache还是nginx的proxy-cache,都是缓存了所有的数据,如果涉及到动态的信息(用户信息、订单详情等),需要做动态处理,推荐ajax** 3 | **源码分析可以不用看了,只要知道是用ob_start和ob_get_clean来做的,并且缓存的key是根据路由来做的就行了** 4 | 5 | ## 目录 6 | * [执行流程](#执行流程) 7 | * [源码分析](#源码分析) 8 | 9 | # 执行流程 10 | - 项目初始化,路由解析 11 | - 控制器初始化,走到base\Controller里面的runAction方法,执行beforeAction事件 12 | - 加载过滤器行为,将PageCache的beforeAction事件注入,并且执行 13 | - 判断requestedRoute(就是真实uri)拼的键是否在缓存中存在,存在则直接获取缓存数据,并且将beforeAction返回false,就是直接走到response组件的send了,不会再继续加载控制器方法了 14 | - 如果缓存不存在,将页面响应信息存缓存 15 | 16 | # 源码分析 17 | PageCache其实就是一个过滤器 18 | ``` 19 | public function behaviors(){ 20 | return [ 21 | [ 22 | 'class' => 'yii\filters\PageCache', 23 | 'only' => ['cacae*',"e"], 24 | 'varyByRoute'=>true, 25 | 'duration' => 60 26 | ] 27 | ]; 28 | } 29 | ``` 30 | 依赖于beforeAction事件,在项目初始化后,路由解析后,会走到底层控制器base\Controller的runAction 31 | ``` 32 | public function runAction($id, $params = []) 33 | { 34 | $action = $this->createAction($id); 35 | if ($action === null) { 36 | throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id); 37 | } 38 | Yii::debug('Route to run: ' . $action->getUniqueId(), __METHOD__); 39 | 40 | if (Yii::$app->requestedAction === null) { 41 | Yii::$app->requestedAction = $action; 42 | } 43 | 44 | $oldAction = $this->action; 45 | $this->action = $action; 46 | 47 | $modules = []; 48 | $runAction = true; 49 | 50 | // call beforeAction on modules 51 | foreach ($this->getModules() as $module) { 52 | if ($module->beforeAction($action)) { 53 | array_unshift($modules, $module); 54 | } else { 55 | $runAction = false; 56 | break; 57 | } 58 | } 59 | 60 | $result = null; 61 | if ($runAction && $this->beforeAction($action)) { 62 | 63 | // run the action 64 | $result = $action->runWithParams($params); 65 | $result = $this->afterAction($action, $result); 66 | 67 | // call afterAction on modules 68 | foreach ($modules as $module) { 69 | /* @var $module Module */ 70 | $result = $module->afterAction($action, $result); 71 | } 72 | } 73 | if ($oldAction !== null) { 74 | $this->action = $oldAction; 75 | } 76 | 77 | return $result; 78 | } 79 | ``` 80 | 具体的就是这段代码 81 | ``` 82 | if ($runAction && $this->beforeAction($action)) { 83 | ``` 84 | 会去执行beforeAction事件 85 | ``` 86 | public function beforeAction($action) 87 | { 88 | $event = new ActionEvent($action); 89 | $this->trigger(self::EVENT_BEFORE_ACTION, $event); 90 | return $event->isValid; 91 | } 92 | ``` 93 | 事件会去加载行为,具体的源码逻辑在 94 | * [Behavior行为源码](yii2/%5B关键概念一%5DBehavior行为源码.md) 95 | * [Events事件源码](yii2/%5B关键概念二%5DEvents事件源码.md) 96 | * [过滤器源码](yii2/%5BCache与过滤器二%5D过滤器Filters源码.md) 97 | 这里就不讲过滤器底层逻辑了,直接将PageCahe的逻辑 98 | ``` 99 | public function beforeAction($action) 100 | { 101 | if (!$this->enabled) { 102 | return true; 103 | } 104 | //实例化cache组件 105 | $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface'); 106 | //依赖关系 107 | if (is_array($this->dependency)) { 108 | $this->dependency = Yii::createObject($this->dependency); 109 | } 110 | //获取response组件 111 | $response = Yii::$app->getResponse(); 112 | //拼缓存的键 113 | $data = $this->cache->get($this->calculateCacheKey()); 114 | //判断缓存是否可用 115 | if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== static::PAGE_CACHE_VERSION) { 116 | $this->view->pushDynamicContent($this); 117 | //新开启一层ob_start 118 | ob_start(); 119 | ob_implicit_flush(false); 120 | //注入事件 121 | $response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']); 122 | Yii::debug('Valid page content is not found in the cache.', __METHOD__); 123 | //给底层过滤器true,让应用去执行控制器方法 124 | return true; 125 | } 126 | //缓存可用,直接给response组件赋值 127 | $this->restoreResponse($response, $data); 128 | Yii::debug('Valid page content is found in the cache.', __METHOD__); 129 | //给底层过滤器false,让应用跳过控制器方法 130 | return false; 131 | } 132 | ``` 133 | PageCache的键名是根据路由组件拼的 134 | ``` 135 | protected function calculateCacheKey() 136 | { 137 | $key = [__CLASS__]; 138 | if ($this->varyByRoute) { 139 | $key[] = Yii::$app->requestedRoute; 140 | } 141 | return array_merge($key, (array)$this->variations); 142 | } 143 | ``` 144 | requestRoute这个值在路径解析后被赋值 145 | ``` 146 | public function handleRequest($request) 147 | { 148 | if (empty($this->catchAll)) { 149 | try { 150 | list($route, $params) = $request->resolve(); 151 | } catch (UrlNormalizerRedirectException $e) { 152 | $url = $e->url; 153 | if (is_array($url)) { 154 | if (isset($url[0])) { 155 | // ensure the route is absolute 156 | $url[0] = '/' . ltrim($url[0], '/'); 157 | } 158 | $url += $request->getQueryParams(); 159 | } 160 | 161 | return $this->getResponse()->redirect(Url::to($url, $e->scheme), $e->statusCode); 162 | } 163 | } else { 164 | $route = $this->catchAll[0]; 165 | $params = $this->catchAll; 166 | unset($params[0]); 167 | } 168 | try { 169 | Yii::debug("Route requested: '$route'", __METHOD__); 170 | //在这里被赋值 171 | $this->requestedRoute = $route; 172 | 173 | $result = $this->runAction($route, $params); 174 | 175 | if ($result instanceof Response) { 176 | return $result; 177 | } 178 | 179 | $response = $this->getResponse(); 180 | if ($result !== null) { 181 | $response->data = $result; 182 | } 183 | 184 | return $response; 185 | } catch (InvalidRouteException $e) { 186 | throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'), $e->getCode(), $e); 187 | } 188 | } 189 | ``` 190 | 如果缓存组件可用,就会将缓存中的各种响应信息赋值给response组件中,跳过控制器方法 191 | ``` 192 | protected function restoreResponse($response, $data) 193 | { 194 | foreach (['format', 'version', 'statusCode', 'statusText', 'content'] as $name) { 195 | $response->{$name} = $data[$name]; 196 | } 197 | foreach (['headers', 'cookies'] as $name) { 198 | if (isset($data[$name]) && is_array($data[$name])) { 199 | $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray())); 200 | } 201 | } 202 | if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) { 203 | $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders'], true); 204 | } 205 | $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null); 206 | } 207 | ``` 208 | 具体真正响应的逻辑在Response组件中 209 | ``` 210 | public function send() 211 | { 212 | if ($this->isSent) { 213 | return; 214 | } 215 | $this->trigger(self::EVENT_BEFORE_SEND); 216 | $this->prepare(); 217 | $this->trigger(self::EVENT_AFTER_PREPARE); 218 | $this->sendHeaders(); 219 | $this->sendContent(); 220 | $this->trigger(self::EVENT_AFTER_SEND); 221 | $this->isSent = true; 222 | } 223 | ``` 224 | 如果缓存不可用,核心逻辑就是ob_get_clean,因为之前已经开过一次ob_start了,所以会获取到要响应的所有数据,将其保存起来 225 | ``` 226 | public function cacheResponse() 227 | { 228 | $this->view->popDynamicContent(); 229 | $beforeCacheResponseResult = $this->beforeCacheResponse(); 230 | if ($beforeCacheResponseResult === false) { 231 | echo $this->updateDynamicContent(ob_get_clean(), $this->getDynamicPlaceholders()); 232 | return; 233 | } 234 | 235 | $response = Yii::$app->getResponse(); 236 | $response->off(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']); 237 | $data = [ 238 | 'cacheVersion' => static::PAGE_CACHE_VERSION, 239 | 'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null, 240 | 'content' => ob_get_clean(), 241 | ]; 242 | if ($data['content'] === false || $data['content'] === '') { 243 | return; 244 | } 245 | 246 | $data['dynamicPlaceholders'] = $this->getDynamicPlaceholders(); 247 | foreach (['format', 'version', 'statusCode', 'statusText'] as $name) { 248 | $data[$name] = $response->{$name}; 249 | } 250 | $this->insertResponseCollectionIntoData($response, 'headers', $data); 251 | $this->insertResponseCollectionIntoData($response, 'cookies', $data); 252 | $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency); 253 | $data['content'] = $this->updateDynamicContent($data['content'], $this->getDynamicPlaceholders()); 254 | echo $data['content']; 255 | } 256 | ``` 257 | -------------------------------------------------------------------------------- /yii2/[Cache与过滤器二]过滤器Filters源码.md: -------------------------------------------------------------------------------- 1 | # 过滤器执行基本流程源码 2 | 要了解过滤器是如何执行的,需要知道yii2的事件和行为概念 3 | * [Behavior行为源码](yii2/%5B关键概念一%5DBehavior行为源码.md) 4 | * [Events事件源码](yii2/%5B关键概念二%5DEvents事件源码.md) 5 | 6 | 7 | 也需要简单了解下控制中的方法是如何执行的 8 | - 组件、路由、配置初始化等等 9 | - 执行base\Controller.php的runAction方法 10 | - 和过滤器有关的就是执行$this的beforeAction 11 | - 会调用beforeAction事件,加载行为,将行为中的beforeAction事件也注册进去 12 | - 按beforeAction事件的加载顺序执行事件(先执行的是过滤器的beforeAction事件) 13 | ``` 14 | yiisoft\yii2\base\Controller.php 15 | 16 | public function runAction($id, $params = []) 17 | { 18 | .... 19 | if ($runAction && $this->beforeAction($action)) { 20 | // run the action 21 | $result = $action->runWithParams($params); 22 | 23 | $result = $this->afterAction($action, $result); 24 | 25 | // call afterAction on modules 26 | foreach ($modules as $module) { 27 | /* @var $module Module */ 28 | $result = $module->afterAction($action, $result); 29 | } 30 | } 31 | .... 32 | } 33 | 34 | public function beforeAction($action) 35 | { 36 | $event = new ActionEvent($action); 37 | $this->trigger(self::EVENT_BEFORE_ACTION, $event); 38 | return $event->isValid; 39 | } 40 | ``` 41 | 在执行到trigger时候,也就是说明需要框架执行beforeAction事件,需要调用到Component类的trigger方法 42 | ``` 43 | public function trigger($name, Event $event = null) 44 | { 45 | $this->ensureBehaviors(); 46 | 47 | $eventHandlers = []; 48 | foreach ($this->_eventWildcards as $wildcard => $handlers) { 49 | if (StringHelper::matchWildcard($wildcard, $name)) { 50 | $eventHandlers = array_merge($eventHandlers, $handlers); 51 | } 52 | } 53 | 54 | if (!empty($this->_events[$name])) { 55 | $eventHandlers = array_merge($eventHandlers, $this->_events[$name]); 56 | } 57 | if (!empty($eventHandlers)) { 58 | if ($event === null) { 59 | $event = new Event(); 60 | } 61 | if ($event->sender === null) { 62 | $event->sender = $this; 63 | } 64 | $event->handled = false; 65 | $event->name = $name; 66 | foreach ($eventHandlers as $handler) { 67 | $event->data = $handler[1]; 68 | call_user_func($handler[0], $event); 69 | // stop further handling if the event is handled 70 | if ($event->handled) { 71 | return; 72 | } 73 | } 74 | } 75 | 76 | // invoke class-level attached handlers 77 | Event::trigger($this, $name, $event); 78 | } 79 | ``` 80 | 可见trigger源码的第一行就是去加载行为,所以如果控制器里面这样定义 81 | ``` 82 | class TestController extends Controller{ 83 | public function behaviors(){ 84 | return [ 85 | [ 86 | 'class' => 'yii\filters\AjaxFilter', 87 | 'only' => ['show', 'view'], 88 | ] 89 | ]; 90 | } 91 | } 92 | ``` 93 | 会将AjaxFilter过滤器当成行为加载进去,行为加载后需要执行行为的attach方法去注册行为的事件 94 | ``` 95 | private function attachBehaviorInternal($name, $behavior) 96 | { 97 | if($this instanceof Controller){ 98 | } 99 | if (!($behavior instanceof Behavior)) { 100 | $behavior = Yii::createObject($behavior); 101 | } 102 | if (is_int($name)) { 103 | //行为的事件注册进控制器 104 | $behavior->attach($this); 105 | //行为加载进控制器 106 | $this->_behaviors[] = $behavior; 107 | } else { 108 | if (isset($this->_behaviors[$name])) { 109 | //行为加载进控制器 110 | $this->_behaviors[$name]->detach(); 111 | } 112 | //行为的事件注册进控制器 113 | $behavior->attach($this); 114 | $this->_behaviors[$name] = $behavior; 115 | } 116 | 117 | return $behavior; 118 | } 119 | ``` 120 | 所有的过滤器都要继承于ActionFilter类,在ActionFilter类中有attach方法,就是重新了行为Behavior的attach 121 | ``` 122 | public function attach($owner) 123 | { 124 | //$owner是调用这个过滤器的类 125 | $this->owner = $owner; 126 | //将beforeAction事件注册进去 127 | $owner->on(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']); 128 | } 129 | ``` 130 | beforeAction事件注册进去后,调用beforeAction事件会执行beforeFilter方法 131 | ``` 132 | public function beforeFilter($event) 133 | { 134 | //判断这个过滤器是否可用,on属性和except属性判断 135 | if (!$this->isActive($event->action)) { 136 | return; 137 | } 138 | //判断这个过滤器是否可用 139 | $event->isValid = $this->beforeAction($event->action); 140 | if ($event->isValid) { 141 | //将afterAction事件注册进去 142 | $this->owner->on(Controller::EVENT_AFTER_ACTION, [$this, 'afterFilter'], null, false); 143 | } else { 144 | //如果过滤器不可用,那么不会再执行相同事件 145 | $event->handled = true; 146 | } 147 | } 148 | ``` 149 | 过滤器是可以执行on和except属性的,on属性可以写明那些方法是可以使用过滤器,except是指明那些方法不可以使用这个过滤器 150 | ``` 151 | protected function isActive($action) 152 | { 153 | //获取方法名,如果是actionTest方法,$id会是test 154 | $id = $this->getActionId($action); 155 | if (empty($this->only)) { 156 | $onlyMatch = true; 157 | } else { 158 | $onlyMatch = false; 159 | //正则匹配是否被on属性匹配 160 | foreach ($this->only as $pattern) { 161 | if (StringHelper::matchWildcard($pattern, $id)) { 162 | $onlyMatch = true; 163 | break; 164 | } 165 | } 166 | } 167 | 168 | $exceptMatch = false; 169 | //正则匹配是否被except属性匹配 170 | foreach ($this->except as $pattern) { 171 | if (StringHelper::matchWildcard($pattern, $id)) { 172 | $exceptMatch = true; 173 | break; 174 | } 175 | } 176 | 177 | return !$exceptMatch && $onlyMatch; 178 | } 179 | ``` 180 | 每个过滤器都会有beforeAction方法,如Ajax过滤器 181 | ``` 182 | public function beforeAction($action) 183 | { 184 | //如果请求不是以ajax发过来的会直接抛异常 185 | if ($this->request->getIsAjax()) { 186 | return true; 187 | } 188 | 189 | throw new BadRequestHttpException($this->errorMessage); 190 | } 191 | ``` 192 | -------------------------------------------------------------------------------- /yii2/[Cache与过滤器四]HTTP缓存源码.md: -------------------------------------------------------------------------------- 1 | **这个cache的难点就是在于以什么标准去算Last-Modify,如果以文件修改时间来当做Last-Modify,那么PHP获取一个动态的数据库数据,那么客户端刷新会得不到新的数据库数据** 2 | 3 | ## 目录 4 | * [HttpCache基本知识](#HttpCache基本知识) 5 | * [执行流程](#执行流程) 6 | * [源码分析](#源码分析) 7 | 8 | # HttpCache基本知识 9 | [HttpCache基本知识](https://github.com/Zhucola/advanced-nginx/blob/master/HTTP%E7%BC%93%E5%AD%98%E4%B8%8Eheader%E6%A8%A1%E5%9D%97.md) 10 | # 执行流程 11 | - 判断HttpCache是否可用,根据请求方法和lastModify、etagSeed回调 12 | - 算出lastModify和etag 13 | - 发送响应头Cache-Control,默认让客户端缓存3600秒 14 | - 发送响应头ETag 15 | - 判断是否发生304 16 | - 发送响应头Last-Modify 17 | - 响应304或者执行后续流程 18 | 19 | # 源码分析 20 | HTTP缓存是一个过滤器,使用了request、response组件,我们直接从HTTP缓存过滤器的beforeAction开始,beforeAction是执行控制器具体方法前要执行的 21 | ``` 22 | public function beforeAction($action) 23 | { 24 | if (!$this->enabled) { 25 | return true; 26 | } 27 | 28 | $verb = Yii::$app->getRequest()->getMethod(); 29 | if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { 30 | return true; 31 | } 32 | 33 | $lastModified = $etag = null; 34 | if ($this->lastModified !== null) { 35 | $lastModified = call_user_func($this->lastModified, $action, $this->params); 36 | } 37 | if ($this->etagSeed !== null) { 38 | $seed = call_user_func($this->etagSeed, $action, $this->params); 39 | if ($seed !== null) { 40 | $etag = $this->generateEtag($seed); 41 | } 42 | } 43 | $this->sendCacheControlHeader(); 44 | $response = Yii::$app->getResponse(); 45 | if ($etag !== null) { 46 | $response->getHeaders()->set('Etag', $etag); 47 | } 48 | $cacheValid = $this->validateCache($lastModified, $etag); 49 | // https://tools.ietf.org/html/rfc7232#section-4.1 50 | if ($lastModified !== null && (!$cacheValid || ($cacheValid && $etag === null))) { 51 | $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); 52 | } 53 | if ($cacheValid) { 54 | $response->setStatusCode(304); 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | ``` 61 | 可见HTTP缓存只能用于请求方法为GET或者HEAD,或者定义了lastModify或者etagSeed回调的情况 62 | ``` 63 | $verb = Yii::$app->getRequest()->getMethod(); 64 | if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { 65 | return true; 66 | } 67 | ``` 68 | etag算法是如下 69 | ``` 70 | protected function generateEtag($seed) 71 | { 72 | $etag = '"' . rtrim(base64_encode(sha1($seed, true)), '=') . '"'; 73 | return $this->weakEtag ? 'W/' . $etag : $etag; 74 | } 75 | ``` 76 | 默认会让客户端缓存资源3600秒 77 | ``` 78 | public $cacheControlHeader = 'public, max-age=3600'; 79 | protected function sendCacheControlHeader() 80 | { 81 | if ($this->sessionCacheLimiter !== null) { 82 | if ($this->sessionCacheLimiter === '' && !headers_sent() && Yii::$app->getSession()->getIsActive()) { 83 | header_remove('Expires'); 84 | header_remove('Cache-Control'); 85 | header_remove('Last-Modified'); 86 | header_remove('Pragma'); 87 | } 88 | 89 | Yii::$app->getSession()->setCacheLimiter($this->sessionCacheLimiter); 90 | } 91 | 92 | $headers = Yii::$app->getResponse()->getHeaders(); 93 | if ($this->cacheControlHeader !== null) { 94 | $headers->set('Cache-Control', $this->cacheControlHeader); 95 | } 96 | } 97 | ``` 98 | 判断是否发生304响应的逻辑为如下,etag会比lastModify的优先级高 99 | ``` 100 | protected function validateCache($lastModified, $etag) 101 | { 102 | if (Yii::$app->request->headers->has('If-None-Match')) { 103 | return $etag !== null && in_array($etag, Yii::$app->request->getETags(), true); 104 | } elseif (Yii::$app->request->headers->has('If-Modified-Since')) { 105 | return $lastModified !== null && @strtotime(Yii::$app->request->headers->get('If-Modified-Since')) >= $lastModified; 106 | } 107 | 108 | return false; 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /yii2/[redis]Connection源码.md: -------------------------------------------------------------------------------- 1 | yii2安装后不会有yii-redis组件,需要手动安装该组件 2 | ``` 3 | composer require yiisoft/yii2-redis 4 | ``` 5 | 然后在组件配置中添加redis配件 6 | ``` 7 | 'redis' => [ 8 | 'class' => 'yii\redis\Connection', 9 | 'hostname' => '192.168.124.10', 10 | 'port' => 6379, 11 | 'database' => 0, 12 | ], 13 | ``` 14 | 然后就可以在控制器里面使用redis了 15 | ``` 16 | public function actionRedis(){ 17 | $redis = Yii::$app->get("redis"); 18 | $redis->set("g",1); 19 | return $redis->get("g"); 20 | } 21 | ``` 22 | yii/redis/Connection是redis的基础组件,底层的会话、Mutex、Cache都是依赖这个组件 23 | 没有redis扩展也可以使用,因为底层使用的是stream_socket_client连接redis客户端 24 | ``` 25 | public function open() 26 | { 27 | if ($this->_socket !== false) { 28 | return; 29 | } 30 | $connection = ($this->unixSocket ?: $this->hostname . ':' . $this->port) . ', database=' . $this->database; 31 | //记录日志 32 | \Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__); 33 | //连接客户端 34 | $this->_socket = @stream_socket_client( 35 | $this->unixSocket ? 'unix://' . $this->unixSocket : 'tcp://' . $this->hostname . ':' . $this->port, 36 | //连接失败的错误号 37 | $errorNumber, 38 | //连接失败的错误信息 39 | $errorDescription, 40 | //连接超时时间 41 | $this->connectionTimeout ? $this->connectionTimeout : ini_get('default_socket_timeout'), 42 | $this->socketClientFlags 43 | ); 44 | if ($this->_socket) { 45 | if ($this->dataTimeout !== null) { 46 | //读、写操作的超时时间 47 | stream_set_timeout($this->_socket, $timeout = (int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); 48 | } 49 | if ($this->password !== null) { 50 | //密码 51 | $this->executeCommand('AUTH', [$this->password]); 52 | } 53 | if ($this->database !== null) { 54 | //redis数据库 55 | $this->executeCommand('SELECT', [$this->database]); 56 | } 57 | //连接成功的事件 58 | $this->initConnection(); 59 | } else { 60 | \Yii::error("Failed to open redis DB connection ($connection): $errorNumber - $errorDescription", __CLASS__); 61 | $message = YII_DEBUG ? "Failed to open redis DB connection ($connection): $errorNumber - $errorDescription" : 'Failed to open DB connection.'; 62 | throw new Exception($message, $errorDescription, $errorNumber); 63 | } 64 | } 65 | protected function initConnection() 66 | { 67 | //连接redis成功的事件 68 | $this->trigger(self::EVENT_AFTER_OPEN); 69 | } 70 | ``` 71 | 如果要使用命令,那么会被__call魔术方法执行 72 | ``` 73 | public function __call($name, $params) 74 | { 75 | //格式化命令,可以理解为mb_ucwords 76 | $redisCommand = strtoupper(Inflector::camel2words($name, false)); 77 | //判断命令是否可用 78 | if (in_array($redisCommand, $this->redisCommands)) { 79 | //命令可用,执行 80 | return $this->executeCommand($redisCommand, $params); 81 | } else { 82 | //命令不可用,调用父类的魔术方法 83 | return parent::__call($name, $params); 84 | } 85 | } 86 | ``` 87 | 给redis服务发命令的代码十分有意思 88 | ``` 89 | public function executeCommand($name, $params = []) 90 | { 91 | $this->open(); 92 | 93 | //这块是非常有意思的代码 94 | $params = array_merge(explode(' ', $name), $params); 95 | $command = ''; 96 | $paramsCount = 0; 97 | foreach ($params as $arg) { 98 | if ($arg === null) { 99 | continue; 100 | } 101 | 102 | $paramsCount++; 103 | $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; 104 | } 105 | $command = '*' . $paramsCount . "\r\n" . $command; 106 | \Yii::trace("Executing Redis Command: {$name}", __METHOD__); 107 | //重发机制 108 | if ($this->retries > 0) { 109 | $tries = $this->retries; 110 | while ($tries-- > 0) { 111 | try { 112 | //成功了直接return 113 | return $this->sendCommandInternal($command, $params); 114 | } catch (SocketException $e) { 115 | \Yii::error($e, __METHOD__); 116 | // backup retries, fail on commands that fail inside here 117 | $retries = $this->retries; 118 | $this->retries = 0; 119 | //关闭连接 120 | $this->close(); 121 | //重新建立连接 122 | $this->open(); 123 | $this->retries = $retries; 124 | } 125 | } 126 | } 127 | //非重发机制 128 | return $this->sendCommandInternal($command, $params); 129 | } 130 | ``` 131 | 核心就是executeCommand方法的那个foreach,如果给redis发一个hmset a a 1 b 2 c 3,php-redis是这样的 132 | ``` 133 | $redis=new Redis(); 134 | $redis->connect(...); 135 | $redis->hmset("a",["a"=>1,"b"=>2,"c"=>3]); 136 | ``` 137 | 但是yii的redis是 138 | ``` 139 | $redis = Yii::$app->get("redis"); 140 | $redis->hmset("a","a",1,"b",2,"c",3); 141 | ``` 142 | 给redis发过去的是这样的格式 143 | ``` 144 | *8 //代表有8个数据组组成,就是HMSET a a 1 b 2 c 3,一共8个 145 | $5 //代表HMSET一共5个字节 146 | HMSET 147 | $1 148 | a 149 | $1 150 | a 151 | $1 152 | 1 153 | $1 154 | b 155 | $1 156 | 2 157 | $1 158 | c 159 | $1 160 | 3 161 | 162 | ``` 163 | 最后将数据发给redis服务 164 | ``` 165 | private function sendCommandInternal($command, $params) 166 | { 167 | $written = @fwrite($this->_socket, $command); 168 | if ($written === false) { 169 | throw new SocketException("Failed to write to socket.\nRedis command was: " . $command); 170 | } 171 | if ($written !== ($len = mb_strlen($command, '8bit'))) { 172 | throw new SocketException("Failed to write to socket. $written of $len bytes written.\nRedis command was: " . $command); 173 | } 174 | return $this->parseResponse(implode(' ', $params)); 175 | } 176 | ``` 177 | 然后需要得到redis服务的响应 178 | ``` 179 | private function parseResponse($command) 180 | { 181 | if (($line = fgets($this->_socket)) === false) { 182 | throw new SocketException("Failed to read from socket.\nRedis command was: " . $command); 183 | } 184 | $type = $line[0]; 185 | $line = mb_substr($line, 1, -2, '8bit'); 186 | switch ($type) { 187 | case '+': // Status reply 188 | if ($line === 'OK' || $line === 'PONG') { 189 | return true; 190 | } else { 191 | return $line; 192 | } 193 | case '-': // Error reply 194 | throw new Exception("Redis error: " . $line . "\nRedis command was: " . $command); 195 | case ':': // Integer reply 196 | // no cast to int as it is in the range of a signed 64 bit integer 197 | return $line; 198 | case '$': // Bulk replies 199 | if ($line == '-1') { 200 | return null; 201 | } 202 | $length = (int)$line + 2; 203 | $data = ''; 204 | while ($length > 0) { 205 | if (($block = fread($this->_socket, $length)) === false) { 206 | throw new SocketException("Failed to read from socket.\nRedis command was: " . $command); 207 | } 208 | $data .= $block; 209 | $length -= mb_strlen($block, '8bit'); 210 | } 211 | 212 | return mb_substr($data, 0, -2, '8bit'); 213 | case '*': // Multi-bulk replies 214 | $count = (int) $line; 215 | $data = []; 216 | for ($i = 0; $i < $count; $i++) { 217 | $data[] = $this->parseResponse($command); 218 | } 219 | 220 | return $data; 221 | default: 222 | throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); 223 | } 224 | } 225 | ``` 226 | 简单来说,给redis发一个hmset命令,redis会返回 227 | ``` 228 | +OK 229 | 230 | ``` 231 | 然后在判断返回的信息是否正确 232 | -------------------------------------------------------------------------------- /yii2/[关键概念一]Behavior行为源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [PHP的多继承概述](#PHP的多继承概述) 3 | * [Behavior行为](#Behavior行为) 4 | 5 | # PHP的多继承 6 | 我们知道PHP是不支持多继承的,一个子类只能继承一个父类 7 | ``` 8 | class A{} 9 | class B extends A{} 10 | ``` 11 | 但是我们可以使用traits来模拟多继承,很多框架内部都是涉及到了triats,比如yii2的底层base\Model类 12 | ``` 13 | class Model extends Component implements StaticInstanceInterface, IteratorAggregate, ArrayAccess, Arrayable 14 | { 15 | use ArrayableTrait; 16 | use StaticInstanceTrait; 17 | ``` 18 | traits就是一个类可以附加多个traits,模拟了多继承,其实traits有很多事项需要注意,比如trait和class都有相同的属性需要特殊处理等等,详细的知识点可以参考[PHP手册](https://www.php.net/manual/en/language.oop5.traits.php) 19 | ``` 20 | name; 31 | } 32 | 33 | public function sing(){ 34 | echo 3; 35 | } 36 | } 37 | $a = new a; 38 | $a->test(); //1 39 | $a->sing(); //3 40 | ``` 41 | traits不能再继承了,一旦注册进类就无法移除,所以yii2的行为更加灵活 42 | # Behavior行为 43 | 在项目目录创建behavior目录,创建Test.php文件 44 | ``` 45 | "eventsgogo", 59 | ]; 60 | } 61 | 62 | public function sing(){ 63 | return __FUNCTION__; 64 | } 65 | 66 | public function getage(){ 67 | return $this->age; 68 | } 69 | 70 | public function eventsgogo($event){ 71 | echo "events"; 72 | } 73 | } 74 | ``` 75 | 然后在控制器中将行为注册进去 76 | ``` 77 | use app\behaviors\Test; 78 | 79 | class TestController extends Controller{ 80 | public function behaviors(){ 81 | return [ 82 | 'myBehavior4' => [ 83 | 'class' => Test::className(), 84 | "name" => 21313131, 85 | ] 86 | ]; 87 | } 88 | 89 | public function actionX(){ 90 | $this->name; //行为的属性 91 | $this->sing(); //行为的方法 92 | $this->age; //行为的属性 93 | } 94 | } 95 | ``` 96 | 因为控制器Controller继承于Component,Component是实现行为的底层架构,使用的就是魔术方法,当调用一个不属于本类的方法会被魔术get捕获到 97 | ``` 98 | //如果调用$this->age; 99 | 100 | public function __get($name) 101 | { 102 | //会判断本类是否有getage方法,大小写不冲突 103 | $getter = 'get' . $name; 104 | if (method_exists($this, $getter)) { 105 | // read property, e.g. getName() 106 | return $this->$getter(); 107 | } 108 | 109 | //开始去找行为的age属性 110 | $this->ensureBehaviors(); 111 | foreach ($this->_behaviors as $behavior) { 112 | if ($behavior->canGetProperty($name)) { 113 | return $behavior->$name; 114 | } 115 | } 116 | //如果找不到并且本类有setage方法,报错 117 | if (method_exists($this, 'set' . $name)) { 118 | throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); 119 | } 120 | //找不到,将异常抛出去 121 | throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); 122 | } 123 | ``` 124 | 创建行为的方法如下,需要注意的是,如果在控制器中调用,这里的$this是控制器的 125 | ``` 126 | public function ensureBehaviors() 127 | { 128 | if ($this->_behaviors === null) { 129 | //初始化数组 130 | $this->_behaviors = []; 131 | //遍历behaviors 132 | foreach ($this->behaviors() as $name => $behavior) { 133 | //注册行为 134 | $this->attachBehaviorInternal($name, $behavior); 135 | } 136 | } 137 | } 138 | ``` 139 | 默认的底层behaviors是返回一个空数组,所以我们需要重写这个方法 140 | ``` 141 | public function behaviors() 142 | { 143 | return []; 144 | } 145 | ``` 146 | 注册的方法如下 147 | ``` 148 | private function attachBehaviorInternal($name, $behavior) 149 | { 150 | if (!($behavior instanceof Behavior)) { 151 | //实例化行为对象 152 | $behavior = Yii::createObject($behavior); 153 | } 154 | if (is_int($name)) { 155 | $behavior->attach($this); 156 | //追加行为 157 | $this->_behaviors[] = $behavior; 158 | } else { 159 | //判断是否应该覆盖已注册的行为 160 | if (isset($this->_behaviors[$name])) { 161 | $this->_behaviors[$name]->detach(); 162 | } 163 | $behavior->attach($this); 164 | $this->_behaviors[$name] = $behavior; 165 | } 166 | 167 | return $behavior; 168 | } 169 | ``` 170 | attach和detach方法涉及到了yii2的事件概念,这里先不展开讨论,可以先理解为将行为中的事件注册或者移除 171 | ``` 172 | public function attach($owner) 173 | { 174 | $this->owner = $owner; 175 | foreach ($this->events() as $event => $handler) { 176 | $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); 177 | } 178 | } 179 | public function detach() 180 | { 181 | if ($this->owner) { 182 | foreach ($this->events() as $event => $handler) { 183 | $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); 184 | } 185 | $this->owner = null; 186 | } 187 | } 188 | ``` 189 | 这里需要注意,行为底层有一个共有方法owner,不要在注册行为的类中声明owner属性,这样行为的owner会被覆盖 190 | ``` 191 | class Behavior extends BaseObject{ 192 | public $owner; 193 | ``` 194 | 魔术get还调用了canGetProperty方法,这个方法是yii属性注入的核心方法,在base\BaseObj.php里面 195 | ``` 196 | public function canGetProperty($name, $checkVars = true) 197 | { 198 | return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name); 199 | } 200 | ``` 201 | 所以如果行为类里面有非公有非静态属性,可以声明get前缀的方法 202 | ``` 203 | class Obj extends Behavior{ 204 | private $incr = 0; 205 | public function getIncr(){ 206 | return $sex++; 207 | } 208 | } 209 | ``` 210 | 除了可以覆盖behaviors方法来声明行为,还可以调用attachBehavior和attachBehaviors来注册行为 211 | ``` 212 | public function attachBehavior($name, $behavior) 213 | { 214 | $this->ensureBehaviors(); 215 | //注册行为 216 | return $this->attachBehaviorInternal($name, $behavior); 217 | } 218 | public function attachBehaviors($behaviors) 219 | { 220 | $this->ensureBehaviors(); 221 | foreach ($behaviors as $name => $behavior) { 222 | //注册行为 223 | $this->attachBehaviorInternal($name, $behavior); 224 | } 225 | } 226 | ``` 227 | 如果需要移除行为,可以使用detachBehavior或者detachBehaviors方法 228 | ``` 229 | public function detachBehavior($name) 230 | { 231 | $this->ensureBehaviors(); 232 | if (isset($this->_behaviors[$name])) { 233 | $behavior = $this->_behaviors[$name]; 234 | unset($this->_behaviors[$name]); 235 | $behavior->detach(); 236 | return $behavior; 237 | } 238 | 239 | return null; 240 | } 241 | public function detachBehaviors() 242 | { 243 | $this->ensureBehaviors(); 244 | foreach ($this->_behaviors as $name => $behavior) { 245 | $this->detachBehavior($name); 246 | } 247 | } 248 | ``` 249 | 获取行为可以使用getBehavior或者getBehaviors方法,因为有属性注入,所以直接调用behaviors属性也是可以的 250 | ``` 251 | public function getBehavior($name) 252 | { 253 | $this->ensureBehaviors(); 254 | return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null; 255 | } 256 | public function getBehaviors() 257 | { 258 | $this->ensureBehaviors(); 259 | return $this->_behaviors; 260 | } 261 | ``` 262 | -------------------------------------------------------------------------------- /yii2/[关键概念三]别名与类自动加载源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [别名Aliases](#别名Aliases) 3 | * [类自动加载](#类自动加载) 4 | 5 | # 别名Aliases 6 | 别名是底层BaseYii提供的功能,涉及到的函数都是静态的,Yii类与BaseYii的关系是继承关系,源码为 7 | ``` 8 | class Yii extends \yii\BaseYii 9 | { 10 | } 11 | ``` 12 | 可以设置别名,如 13 | ``` 14 | Yii::setAlias("@a","aaaa"); 15 | Yii::setAlias("@a/b","aaaa/bbb"); 16 | Yii::setAlias("@c","@a/u"); 17 | ``` 18 | 涉及的方法为 19 | ``` 20 | public static $aliases = ['@yii' => __DIR__]; 21 | public static function setAlias($alias, $path) 22 | { 23 | //可见第一个参数是否是@开头无所谓,即使没有@开头也会被强制加上 24 | if (strncmp($alias, '@', 1)) { 25 | $alias = '@' . $alias; 26 | } 27 | $pos = strpos($alias, '/'); 28 | $root = $pos === false ? $alias : substr($alias, 0, $pos); 29 | if ($path !== null) { //创建别名操作 30 | //别名的值是否还包含别名 31 | $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); 32 | //如果要创建的别名不存在 33 | if (!isset(static::$aliases[$root])) { 34 | if ($pos === false) { 35 | static::$aliases[$root] = $path; 36 | } else { 37 | static::$aliases[$root] = [$alias => $path]; 38 | } 39 | } elseif (is_string(static::$aliases[$root])) { //要创建的别名存在 40 | if ($pos === false) { 41 | static::$aliases[$root] = $path; 42 | } else { 43 | static::$aliases[$root] = [ 44 | $alias => $path, 45 | $root => static::$aliases[$root], 46 | ]; 47 | } 48 | } else { //要创建的别名存在 49 | static::$aliases[$root][$alias] = $path; 50 | krsort(static::$aliases[$root]); 51 | } 52 | } elseif (isset(static::$aliases[$root])) { //删除别名操作 53 | if (is_array(static::$aliases[$root])) { 54 | unset(static::$aliases[$root][$alias]); 55 | } elseif ($pos === false) { 56 | unset(static::$aliases[$root]); 57 | } 58 | } 59 | } 60 | ``` 61 | 根据源码,如 62 | ``` 63 | Yii::setAlias("@ab","abc"); 64 | Yii::setAlias("@ab/x","123"); 65 | var_dump(Yii::$aliases); 66 | ``` 67 | 首先会创建一个@ab的别名,值为abc; 68 | ``` 69 | array (size=1) 70 | '@ab' => string 'abc' (length=3) 71 | ``` 72 | 然后存入$ab/x别名,底层的会将存@ab的结构改为数组 73 | ``` 74 | array (size=1) 75 | '@ab' => 76 | array (size=2) 77 | '@ab/x' => string '123' (length=3) 78 | '@ab' => string 'abc' (length=3) 79 | ``` 80 | 如果存入操作为 81 | ``` 82 | Yii::setAlias("@ab/x","abc"); 83 | Yii::setAlias("@ab/y","123"); 84 | var_dump(Yii::$aliases); 85 | ``` 86 | 那么底层的结构为 87 | ``` 88 | array (size=1) 89 | '@ab' => 90 | array (size=2) 91 | '@ab/y' => string '123' (length=3) 92 | '@ab/x' => string 'abc' (length=3) 93 | ``` 94 | 删除一个别名也是setAlias 95 | ``` 96 | Yii::setAlias("@a",123); //创建别名 97 | Yii::setAlias("@a"); //删除别名 98 | ``` 99 | 取出别名的操作为getAlias 100 | ``` 101 | public static function getAlias($alias, $throwException = true) 102 | { 103 | //取出别名的第一个参数需要使用@开头 104 | if (strncmp($alias, '@', 1)) { 105 | // not an alias 106 | return $alias; 107 | } 108 | 109 | $pos = strpos($alias, '/'); 110 | $root = $pos === false ? $alias : substr($alias, 0, $pos); 111 | //判断根别名是否存在 112 | if (isset(static::$aliases[$root])) { 113 | if (is_string(static::$aliases[$root])) { 114 | return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos); 115 | } 116 | foreach (static::$aliases[$root] as $name => $path) { 117 | if (strpos($alias . '/', $name . '/') === 0) { 118 | return $path . substr($alias, strlen($name)); 119 | } 120 | } 121 | } 122 | //如果查找不到别名则异常 123 | if ($throwException) { 124 | throw new InvalidArgumentException("Invalid path alias: $alias"); 125 | } 126 | 127 | return false; 128 | } 129 | ``` 130 | 如 131 | ``` 132 | Yii::setAlias("@a/b","123"); 133 | $res = Yii::getAlias("@a/b/ccc"); //123/ccc 134 | ``` 135 | # 类自动加载 136 | yii是依赖于composer的,composer是有自动加载机制的,Yii在composer的类自动加载基础上又做了一层自己的加载机制 137 | ``` 138 | class Yii extends \yii\BaseYii 139 | { 140 | } 141 | 142 | spl_autoload_register(['Yii', 'autoload'], true, true); 143 | Yii::$classMap = require __DIR__ . '/classes.php'; 144 | ``` 145 | spl_autoload_register底层是一个队列,可以注册多个加载机制,第三个参数为true表示将这个队列放在最前面 146 | 所以yii的加载机制为 147 | - 先用自己的类加载机制 148 | - 自己的找不到就用composer的类加载机制 149 | Yii使用了classMap机制类来做类与文件路径的映射,这种加载机制非常快,但是缺点是代码量非常大 150 | ``` 151 | return [ 152 | 'yii\base\Action' => YII2_PATH . '/base/Action.php', 153 | 'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php', 154 | 'yii\base\ActionFilter' => YII2_PATH . '/base/ActionFilter.php', 155 | 'yii\base\Application' => YII2_PATH . '/base/Application.php', 156 | 'yii\base\ArrayAccessTrait' => YII2_PATH . '/base/ArrayAccessTrait.php', 157 | 'yii\base\Arrayable' => YII2_PATH . '/base/Arrayable.php', 158 | 'yii\base\ArrayableTrait' => YII2_PATH . '/base/ArrayableTrait.php', 159 | 'yii\base\BaseObject' => YII2_PATH . '/base/BaseObject.php', 160 | 'yii\base\Behavior' => YII2_PATH . '/base/Behavior.php', 161 | 'yii\base\BootstrapInterface' => YII2_PATH . '/base/BootstrapInterface.php', 162 | 'yii\base\Component' => YII2_PATH . '/base/Component.php', 163 | 'yii\base\Configurable' => YII2_PATH . '/base/Configurable.php', 164 | 'yii\base\Controller' => YII2_PATH . '/base/Controller.php', 165 | 'yii\base\DynamicContentAwareInterface' => YII2_PATH . '/base/DynamicContentAwareInterface.php', 166 | 'yii\base\DynamicContentAwareTrait' => YII2_PATH . '/base/DynamicContentAwareTrait.php', 167 | 'yii\base\DynamicModel' => YII2_PATH . '/base/DynamicModel.php', 168 | 'yii\base\ErrorException' => YII2_PATH . '/base/ErrorException.php', 169 | 'yii\base\ErrorHandler' => YII2_PATH . '/base/ErrorHandler.php', 170 | 'yii\base\Event' => YII2_PATH . '/base/Event.php', 171 | ... 172 | ``` 173 | 底层autoload逻辑为 174 | ``` 175 | public static function autoload($className) 176 | { 177 | if (isset(static::$classMap[$className])) { //如果在classMap中存在 178 | $classFile = static::$classMap[$className]; 179 | if ($classFile[0] === '@') { //如果这个类的文件地址是一个别名,则获取这个别名对应的值 180 | $classFile = static::getAlias($classFile); 181 | } 182 | } elseif (strpos($className, '\\') !== false) { //如果className不包含\\,则使用别名找对应的值 183 | $classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false); 184 | if ($classFile === false || !is_file($classFile)) { 185 | return; 186 | } 187 | } else { 188 | return; 189 | } 190 | 191 | include $classFile; 192 | 193 | if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && !trait_exists($className, false)) { 194 | throw new UnknownClassException("Unable to find '$className' in file: $classFile. Namespace missing?"); 195 | } 196 | } 197 | ``` 198 | -------------------------------------------------------------------------------- /yii2/[关键概念二]Events事件源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [附加事件处理器](#附加事件处理器) 3 | * [类级别事件处理器](#类级别事件处理器) 4 | 5 | 6 | **这篇写的并不好,因为事件的底层代码比较简单+写作是心情不好,不知道怎么讲这个源码,只是复制粘贴下,有兴趣的同学可以自己去追下源码** 7 | 其实事件就是将一个回调或者是类方法存在一个类属性里面(附加行为),可以去删除某个附加的行为(删除行为),也可以执行这个行为(执行回调或者类方法) 8 | 模拟一个简单的事件,原理和yii的一样 9 | ``` 10 | class obj{ 11 | public $events = []; 12 | 13 | public function on($name,$func){ 14 | $this->events[$name] = $func; 15 | } 16 | 17 | public function off($name){ 18 | if(isset($this->events[$name])){ 19 | unset($this->events[$name]); 20 | } 21 | } 22 | 23 | public function trigger($name){ 24 | call_user_func($this->events[$name]); 25 | } 26 | } 27 | $func = function(){ 28 | echo 1; 29 | }; 30 | $obj = new Obj(); 31 | $obj -> on("a",$func); 32 | $obj -> on("b",$func); 33 | $obj -> off("b"); 34 | $obj -> trigger("a"); 35 | ``` 36 | # 附加事件处理器 37 | 控制器中代码例子如下 38 | ``` 39 | public function actionE(){ 40 | $func = function(){ 41 | echo 1; 42 | }; 43 | $this->on("a",[TestController::class,"eventTestStatic"]); 44 | $this->on("b",[$this,"eventTest"]); 45 | $this->on("c",function(){ 46 | echo 2; 47 | }); 48 | $this->on("c",$func); 49 | $this->trigger("a"); 50 | $this->trigger("c"); 51 | $this->off("a",[TestController::class,"eventTestStatic"]); 52 | $this->off("c",$func); 53 | return false; 54 | } 55 | 56 | public function eventTest(){ 57 | echo __FUNCTION__; 58 | } 59 | 60 | public static function eventTestStatic($events){ 61 | echo 3; 62 | } 63 | ``` 64 | 事件注册使用的on方法来附加处理器到事件上,事件的底层代码都Component类上,所以继承了Component的类都会有事件功能 65 | ``` 66 | public function on($name, $handler, $data = null, $append = true) 67 | { 68 | //注入行为Behaviors,因为行为类也可能有事件 69 | $this->ensureBehaviors(); 70 | if (strpos($name, '*') !== false) { 71 | if ($append || empty($this->_eventWildcards[$name])) { 72 | $this->_eventWildcards[$name][] = [$handler, $data]; 73 | } else { 74 | array_unshift($this->_eventWildcards[$name], [$handler, $data]); 75 | } 76 | return; 77 | } 78 | 79 | if ($append || empty($this->_events[$name])) { 80 | $this->_events[$name][] = [$handler, $data]; 81 | } else { 82 | array_unshift($this->_events[$name], [$handler, $data]); 83 | } 84 | } 85 | ``` 86 | 事件使用两个私有属性来保存,一个是_evevts,存储的是所有不带星号的事件;一个是_eventWildcards,存储的是有星号的事件 87 | 区别如下,会输出12 88 | ``` 89 | public function actionE(){ 90 | $this->on("test*",function(){ 91 | echo 1; 92 | }); 93 | $this->on("test_abc",function(){ 94 | echo 2; 95 | }); 96 | $this->trigger("test_abc"); 97 | } 98 | ``` 99 | 存储事件的都是二维数组,也就是说可以附加相同名字的事件,在执行的时候会按照附加的顺序来执行(但是也可以避免,下面会讲) 100 | 删除事件调用的是off方法 101 | ``` 102 | public function off($name, $handler = null) 103 | { 104 | //注入行为Behaviors,因为行为类也可能有事件 105 | $this->ensureBehaviors(); 106 | if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) { 107 | return false; 108 | } 109 | //将所有name的事件全部删除 110 | if ($handler === null) { 111 | unset($this->_events[$name], $this->_eventWildcards[$name]); 112 | return true; 113 | } 114 | 115 | $removed = false; 116 | // plain event names 117 | if (isset($this->_events[$name])) { 118 | foreach ($this->_events[$name] as $i => $event) { 119 | //删除name中匹配到的事件 120 | if ($event[0] === $handler) { 121 | unset($this->_events[$name][$i]); 122 | $removed = true; 123 | } 124 | } 125 | if ($removed) { 126 | $this->_events[$name] = array_values($this->_events[$name]); 127 | return $removed; 128 | } 129 | } 130 | 131 | // wildcard event names 132 | if (isset($this->_eventWildcards[$name])) { 133 | foreach ($this->_eventWildcards[$name] as $i => $event) { 134 | if ($event[0] === $handler) { 135 | unset($this->_eventWildcards[$name][$i]); 136 | $removed = true; 137 | } 138 | } 139 | if ($removed) { 140 | $this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]); 141 | // remove empty wildcards to save future redundant regex checks: 142 | if (empty($this->_eventWildcards[$name])) { 143 | unset($this->_eventWildcards[$name]); 144 | } 145 | } 146 | } 147 | 148 | return $removed; 149 | } 150 | ``` 151 | 这里需要注意一下,比如如下代码 152 | ``` 153 | public function actionE(){ 154 | $this->on("abcd",function(){echo1;}); 155 | $this->off("abcd",function(){echo1;});//无法将abcd的回调删除 156 | } 157 | ``` 158 | 因为 159 | ``` 160 | $a = function(){ 161 | echo 1; 162 | }; 163 | $b = function(){ 164 | echo 1; 165 | }; 166 | var_dump($a===$b); //false 167 | ``` 168 | 如果要删除回调需要更改附加代码 169 | ``` 170 | public function actionE(){ 171 | $func = function(){ 172 | echo 1; 173 | }; 174 | $this->on("abcd",$func); 175 | $this->off("abcd",$func); 176 | } 177 | ``` 178 | 触发事件为trigger方法 179 | ``` 180 | public function trigger($name, Event $event = null) 181 | { 182 | //行为 183 | $this->ensureBehaviors(); 184 | 185 | $eventHandlers = []; 186 | foreach ($this->_eventWildcards as $wildcard => $handlers) { 187 | //正则匹配,如注册了test*事件和test1事件,在调用test1事件时候也会调用test* 188 | if (StringHelper::matchWildcard($wildcard, $name)) { 189 | //事件合并 190 | $eventHandlers = array_merge($eventHandlers, $handlers); 191 | } 192 | } 193 | 194 | if (!empty($this->_events[$name])) { 195 | $eventHandlers = array_merge($eventHandlers, $this->_events[$name]); 196 | } 197 | 198 | if (!empty($eventHandlers)) { 199 | if ($event === null) { 200 | $event = new Event(); 201 | } 202 | if ($event->sender === null) { 203 | $event->sender = $this; 204 | } 205 | $event->handled = false; 206 | $event->name = $name; 207 | foreach ($eventHandlers as $handler) { 208 | $event->data = $handler[1]; 209 | call_user_func($handler[0], $event); 210 | //是否执行下一个事件了 211 | if ($event->handled) { 212 | return; 213 | } 214 | } 215 | } 216 | 217 | // invoke class-level attached handlers 218 | Event::trigger($this, $name, $event); 219 | } 220 | ``` 221 | 上面说过事件是按照顺序来执行的,但是可以指定handler为true,如 222 | ``` 223 | public function actionE(){ 224 | 225 | $this->on("a",[TestController::class,"eventTestStatic"]); 226 | $this->on("a",[$this,"eventTest"]); 227 | $this->trigger("a"); 228 | return false; 229 | } 230 | 231 | public function eventTest(){ 232 | echo __FUNCTION__; 233 | } 234 | 235 | public static function eventTestStatic($events){ 236 | $events->handled = true; 237 | echo 1; 238 | } 239 | ``` 240 | 在trigger源码内部,执行了call_user_func后handler会变成true,所以直接就true了 241 | 242 | # 类级别事件处理器 243 | 如果需要一个类的所有实例而不是一个指定的实例都响应一个被触发的事件,就需要类级别事件处理器了 244 | 如想要在所有继承于web\Controller的类都能使用a事件,可以写如下代码 245 | ``` 246 | use yii\base\Event; 247 | use yii\web\Controller; 248 | ... 249 | public function actionE(){ 250 | Event::on(Controller::class,"a",function(){echo 123;}); 251 | Event::trigger(TestController::class,"a"); 252 | } 253 | ``` 254 | 相对于实例级别的on方法,类级别的on方法维护的是一个三维数组,并且属性是静态的 255 | ``` 256 | public static function on($class, $name, $handler, $data = null, $append = true) 257 | { 258 | $class = ltrim($class, '\\'); 259 | 260 | if (strpos($class, '*') !== false || strpos($name, '*') !== false) { 261 | if ($append || empty(self::$_eventWildcards[$name][$class])) { 262 | self::$_eventWildcards[$name][$class][] = [$handler, $data]; 263 | } else { 264 | array_unshift(self::$_eventWildcards[$name][$class], [$handler, $data]); 265 | } 266 | return; 267 | } 268 | 269 | if ($append || empty(self::$_events[$name][$class])) { 270 | self::$_events[$name][$class][] = [$handler, $data]; 271 | } else { 272 | array_unshift(self::$_events[$name][$class], [$handler, $data]); 273 | } 274 | } 275 | ``` 276 | 删除方法也是off,和实例级别的off方法类似 277 | ``` 278 | public static function off($class, $name, $handler = null) 279 | { 280 | $class = ltrim($class, '\\'); 281 | if (empty(self::$_events[$name][$class]) && empty(self::$_eventWildcards[$name][$class])) { 282 | return false; 283 | } 284 | if ($handler === null) { 285 | unset(self::$_events[$name][$class]); 286 | unset(self::$_eventWildcards[$name][$class]); 287 | return true; 288 | } 289 | 290 | // plain event names 291 | if (isset(self::$_events[$name][$class])) { 292 | $removed = false; 293 | foreach (self::$_events[$name][$class] as $i => $event) { 294 | if ($event[0] === $handler) { 295 | unset(self::$_events[$name][$class][$i]); 296 | $removed = true; 297 | } 298 | } 299 | if ($removed) { 300 | self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); 301 | return $removed; 302 | } 303 | } 304 | 305 | // wildcard event names 306 | $removed = false; 307 | if (isset(self::$_eventWildcards[$name][$class])) { 308 | foreach (self::$_eventWildcards[$name][$class] as $i => $event) { 309 | if ($event[0] === $handler) { 310 | unset(self::$_eventWildcards[$name][$class][$i]); 311 | $removed = true; 312 | } 313 | } 314 | if ($removed) { 315 | self::$_eventWildcards[$name][$class] = array_values(self::$_eventWildcards[$name][$class]); 316 | // remove empty wildcards to save future redundant regex checks : 317 | if (empty(self::$_eventWildcards[$name][$class])) { 318 | unset(self::$_eventWildcards[$name][$class]); 319 | if (empty(self::$_eventWildcards[$name])) { 320 | unset(self::$_eventWildcards[$name]); 321 | } 322 | } 323 | } 324 | } 325 | 326 | return $removed; 327 | } 328 | ``` 329 | 类级别还有一个全部删除事件的方法 330 | ``` 331 | public static function offAll() 332 | { 333 | self::$_events = []; 334 | self::$_eventWildcards = []; 335 | } 336 | ``` 337 | -------------------------------------------------------------------------------- /yii2/[安全一]用户认证源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [整体逻辑流程](#整体逻辑流程) 3 | * [认证组件源码简单介绍](#认证组件简单介绍源码) 4 | * [identityClass简单源码介绍](#identityClass简单源码介绍) 5 | * [用户信息注册进认证组件源码](#用户信息注册进认证组件源码) 6 | * [不注册直接从认证组件获取用户信息源码](#不注册直接从认证组件获取用户信息源码) 7 | 8 | # 整体逻辑流程 9 | 获取认证信息 10 | - 从session获取键为__id的值 11 | - 通过__id保存的id去查对应的具体用户认证信息 12 | - 判断session中的__expire和__absoluteExpire是否大于当前时间戳,有大于则说明会话过期,直接进行logout退出操作 13 | - 延迟__expire会话中的时间戳 14 | - 如果session中没认证信息就去查cookie 15 | - 从cookie中获取_identity对应的id 16 | - 通过_identity的id去查对应的具体用户认证信息 17 | - 判断cookie中的AuthKey是否与认证信息一致 18 | - 如果cookie中的AuthKey不一致或者cookie中存的信息不合法则删除cookie 19 | - 如果session中有认证信息则延长cookie的生存周期 20 | 21 | 退出 22 | - 删cookie 23 | - 删sesison中的__expire和__absoluteExpire 24 | - 更新session_id 25 | - session_destory 26 | 27 | 注册认证信息 28 | - 删sesison中的__expire和__absoluteExpire 29 | - 更新session_id 30 | - 设置sesison中的__expire和__absoluteExpire 31 | - 设置cookie 32 | 33 | # 认证组件源码简单介绍 34 | yii封装了一个用户认证类,该类是一个可以在配置文件web.php中配置的组件 35 | 默认配置如下 36 | ``` 37 | 'components' => [ 38 | ... 39 | 'user' => [ 40 | 'identityClass' => 'app\models\User', 41 | 'enableAutoLogin' => true, 42 | ], 43 | ... 44 | ] 45 | ``` 46 | 认证组件的class属性在入口文件执行底层base\Application合并 47 | ``` 48 | foreach ($this->coreComponents() as $id => $component) { 49 | if (!isset($config['components'][$id])) { 50 | $config['components'][$id] = $component; 51 | } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) { 52 | $config['components'][$id]['class'] = $component['class']; 53 | } 54 | } 55 | ``` 56 | 所以默认的配置为 57 | ``` 58 | 'components' => [ 59 | ... 60 | 'user' => [ 61 | 'class' => 'yii\web\User', 62 | 'identityClass' => 'app\models\User', 63 | 'enableAutoLogin' => true, 64 | ], 65 | ... 66 | ] 67 | ``` 68 | yii\web\User继承Component,所以可以使用属性注入、事件和行为 69 | ``` 70 | class User extends Component 71 | { 72 | const EVENT_BEFORE_LOGIN = 'beforeLogin'; 73 | const EVENT_AFTER_LOGIN = 'afterLogin'; 74 | const EVENT_BEFORE_LOGOUT = 'beforeLogout'; 75 | const EVENT_AFTER_LOGOUT = 'afterLogout'; 76 | ``` 77 | 可用的属性注入有 78 | ``` 79 | protected function getAccessChecker() 80 | protected function getAuthManager() 81 | protected function getIdentityAndDurationFromCookie() 82 | public function setReturnUrl($url) 83 | public function getReturnUrl($defaultUrl = null) 84 | public function getId() 85 | public function getIsGuest() 86 | public function setIdentity($identity) 87 | public function getIdentity($autoRenew = true) 88 | ``` 89 | 需要注意的是user组件有init方法,这个方法是底层方法,就是执行完构造函数后会由底层直接调用 90 | ``` 91 | public function init() 92 | { 93 | parent::init(); 94 | //是否有认证逻辑类 95 | if ($this->identityClass === null) { 96 | throw new InvalidConfigException('User::identityClass must be set.'); 97 | } 98 | //如果用cookie存信息的话,需要有cookie相关的属性 99 | if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { 100 | throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); 101 | } 102 | if (!empty($this->accessChecker) && is_string($this->accessChecker)) { 103 | $this->accessChecker = Yii::createObject($this->accessChecker); 104 | } 105 | } 106 | ``` 107 | # identityClass简单源码介绍 108 | User组件有一个核心属性是identityClass,identityClass类其实就是获取用户信息,一般都会封装成和数据库关联的,从数据库直接获取用户信息,为了方便起见我们使用yii安装后的默认identitiyClass属性app\models\User 109 | 值得注意的是,identityClass必须实现接口IdentityInterface 110 | 默认的identityClass内部源码非常简单 111 | ``` 112 | private static $users = [ 113 | '100' => [ 114 | 'id' => '100', 115 | 'username' => 'admin', 116 | 'password' => 'admin', 117 | 'authKey' => 'test100key', 118 | 'accessToken' => '100-token', 119 | ], 120 | '101' => [ 121 | 'id' => '101', 122 | 'username' => 'demo', 123 | 'password' => 'demo', 124 | 'authKey' => 'test101key', 125 | 'accessToken' => '101-token', 126 | ], 127 | ]; 128 | //通过id获取用户信息,就是静态user属性的key 129 | public static function findIdentity($id) 130 | { 131 | return isset(self::$users[$id]) ? new static(self::$users[$id]) : null; 132 | } 133 | //通过accessToken获取用户信息 134 | public static function findIdentityByAccessToken($token, $type = null) 135 | { 136 | foreach (self::$users as $user) { 137 | if ($user['accessToken'] === $token) { 138 | return new static($user); 139 | } 140 | } 141 | 142 | return null; 143 | } 144 | //通过用户名获取用户属性,默认是不区分大小写的 145 | public static function findByUsername($username) 146 | { 147 | foreach (self::$users as $user) { 148 | if (strcasecmp($user['username'], $username) === 0) { 149 | return new static($user); 150 | } 151 | } 152 | 153 | return null; 154 | } 155 | //判断authKey是否一致 156 | public function validateAuthKey($authKey) 157 | { 158 | return $this->authKey === $authKey; 159 | } 160 | //判断密码是否一致 161 | public function validatePassword($password) 162 | { 163 | return $this->password === $password; 164 | } 165 | ``` 166 | # 用户信息注册进认证组件源码 167 | 先从用户登录开始,控制器代码如下 168 | ``` 169 | class TestController extends Controller{ 170 | public function actionD(){ 171 | $user = Yii::$app->get("user"); 172 | //使用安装yii后默认的models\User 173 | $identity = $user->identityClass::findIdentity(101); 174 | //将用户信息注册进用户认证 175 | $user->login($identity); 176 | } 177 | } 178 | ``` 179 | login方法的源码如下 180 | ``` 181 | public function login(IdentityInterface $identity, $duration = 0) 182 | { 183 | //执行beforeLogin事件,根据事件的isValid来判断是否可以登录 184 | if ($this->beforeLogin($identity, false, $duration)) { 185 | //将旧的用户认证信息从会话中删除,并且可选的是重新将authTimeoutParam、absoluteAuthTimeoutParam生存周期存进会话 186 | $this->switchIdentity($identity, $duration); 187 | //获取认证id 188 | $id = $identity->getId(); 189 | //从request组件中获取客户端ip 190 | $ip = Yii::$app->getRequest()->getUserIP(); 191 | //就是日志信息会有所不同 192 | if ($this->enableSession) { 193 | $log = "User '$id' logged in from $ip with duration $duration."; 194 | } else { 195 | $log = "User '$id' logged in from $ip. Session not enabled."; 196 | } 197 | //重新更新csrf 198 | $this->regenerateCsrfToken(); 199 | //存日志 200 | Yii::info($log, __METHOD__); 201 | //执行登录后事件 202 | $this->afterLogin($identity, false, $duration); 203 | } 204 | //判断登录认证信息是否可用,也就是是否登录成功了 205 | return !$this->getIsGuest(); 206 | } 207 | ``` 208 | 用户注册登录认证的时候,会有以下操作 209 | - 原有cookie中的认证信息删除(可选) 210 | - 更新session_id(可选) 211 | - 从session中删除__id和__expire属性 212 | - 将__id存进session,值是用户认证id 213 | - 重新从当前时间开始存authTimeoutParam和absoluteAuthTimeoutParam(可选) 214 | - 将认证信息存进cookie(可选) 215 | ``` 216 | public function switchIdentity($identity, $duration = 0) 217 | { 218 | //存用户认证信息进类属性$this->_identity 219 | $this->setIdentity($identity); 220 | //如果不使用session则直接return 221 | if (!$this->enableSession) { 222 | return; 223 | } 224 | //是否将旧的cookie里面存的认证信息删除 225 | if ($this->enableAutoLogin && ($this->autoRenewCookie || $identity === null)) { 226 | $this->removeIdentityCookie(); 227 | } 228 | //获取session组件 229 | $session = Yii::$app->getSession(); 230 | //是否更新session id 231 | if (!YII_ENV_TEST) { 232 | $session->regenerateID(true); 233 | } 234 | //删除session中的值__id 235 | $session->remove($this->idParam); 236 | //删除session中的值__expire 237 | $session->remove($this->authTimeoutParam); 238 | 239 | if ($identity) { 240 | //设置会话信息__id为认证信息的id 241 | $session->set($this->idParam, $identity->getId()); 242 | if ($this->authTimeout !== null) { 243 | //存__expire到session 244 | $session->set($this->authTimeoutParam, time() + $this->authTimeout); 245 | } 246 | if ($this->absoluteAuthTimeout !== null) { 247 | //存__absoluteExpire到session 248 | $session->set($this->absoluteAuthTimeoutParam, time() + $this->absoluteAuthTimeout); 249 | } 250 | //存cookie,key为_identity 251 | if ($this->enableAutoLogin && $duration > 0) { 252 | $this->sendIdentityCookie($identity, $duration); 253 | } 254 | } 255 | } 256 | ``` 257 | 也就是说如果使用enableAutoLogin并且cookie的周期设置大于0,会话中会有如下信息 258 | - session名字__expire,值为当前时间+authTimeout属性 259 | - session名字__absoluteExpire,值为当前时间+absoluteAuthTimeout属性 260 | - cookie名字为_identity,值为安全加密后的值 261 | cookie存储的具体源码如下,就是将用户认证的信息存入cookie,然后用底层的cookie加密 262 | ``` 263 | protected function sendIdentityCookie($identity, $duration) 264 | { 265 | $cookie = Yii::createObject(array_merge($this->identityCookie, [ 266 | 'class' => 'yii\web\Cookie', 267 | 'value' => json_encode([ 268 | $identity->getId(), 269 | $identity->getAuthKey(), 270 | $duration, 271 | ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 272 | 'expire' => time() + $duration, 273 | ])); 274 | Yii::$app->getResponse()->getCookies()->add($cookie); 275 | } 276 | ``` 277 | 这里要说一下会存两个session,值都是时间戳,具体区别如下 278 | - expire属性在登录后会判断当前时间戳是否小于__expire对应会话中的时间戳,小于则说明认证过期,大于则重新将当前时间戳+authTimeout存入会话 279 | - absoluteExpire属性在登录后会判断当前时间戳是否小于__expire对应会话中的时间戳,小于则说明认证过期 280 | 281 | # 不注册直接从认证组件获取用户信息源码 282 | 控制器代码如下 283 | ``` 284 | class TestController extends Controller 285 | { 286 | public function actionD(){ 287 | $user = Yii::$app->get("user"); 288 | var_dump($user->identity); 289 | return 123; 290 | } 291 | } 292 | ``` 293 | 会通过属性注入调用getIdentity方法 294 | ``` 295 | private $_identity = false; 296 | public function getIdentity($autoRenew = true) 297 | { 298 | if ($this->_identity === false) { 299 | if ($this->enableSession && $autoRenew) { 300 | try { 301 | $this->_identity = null; 302 | //从会话中获取认证信息 303 | $this->renewAuthStatus(); 304 | } catch (\Exception $e) { 305 | $this->_identity = false; 306 | throw $e; 307 | } catch (\Throwable $e) { 308 | $this->_identity = false; 309 | throw $e; 310 | } 311 | } else { 312 | return null; 313 | } 314 | } 315 | 316 | return $this->_identity; 317 | } 318 | protected function renewAuthStatus() 319 | { 320 | //获取session组件 321 | $session = Yii::$app->getSession(); 322 | //如果cookie中有session_id或者已经session_start则从session中获取__id 323 | $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null; 324 | 325 | if ($id === null) { 326 | $identity = null; 327 | } else { 328 | /* @var $class IdentityInterface */ 329 | $class = $this->identityClass; 330 | //通过会话中的认证id去查具体的认证信息 331 | $identity = $class::findIdentity($id); 332 | } 333 | ////存用户认证信息进类属性$this->_identity 334 | $this->setIdentity($identity); 335 | if ($identity !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) { 336 | $expire = $this->authTimeout !== null ? $session->get($this->authTimeoutParam) : null; 337 | $expireAbsolute = $this->absoluteAuthTimeout !== null ? $session->get($this->absoluteAuthTimeoutParam) : null; 338 | //当前时间戳是否小于__expire对应会话中的时间戳,小于则说明认证过期,大于则重新将当前时间戳+authTimeout存入会话 339 | //当前时间戳是否小于__expire对应会话中的时间戳,小于则说明认证过期 340 | if ($expire !== null && $expire < time() || $expireAbsolute !== null && $expireAbsolute < time()) { 341 | $this->logout(false); 342 | } elseif ($this->authTimeout !== null) { 343 | $session->set($this->authTimeoutParam, time() + $this->authTimeout); 344 | } 345 | } 346 | 347 | if ($this->enableAutoLogin) { 348 | if ($this->getIsGuest()) { 349 | //如果session中没有认证信息,就从cookie中查 350 | $this->loginByCookie(); 351 | } elseif ($this->autoRenewCookie) { 352 | //延迟cookie生成周期 353 | $this->renewIdentityCookie(); 354 | } 355 | } 356 | } 357 | ``` 358 | 如果选择了enableAutoLogin,那么如果从session中查不到认证信息就去查cookie,如果查到了认证信息就去验证cookie的生存周期 359 | ``` 360 | protected function renewIdentityCookie() 361 | { 362 | $name = $this->identityCookie['name']; 363 | //获取一开始存进去的cookie认证信息 364 | $value = Yii::$app->getRequest()->getCookies()->getValue($name); 365 | if ($value !== null) { 366 | $data = json_decode($value, true); 367 | if (is_array($data) && isset($data[2])) { 368 | $cookie = Yii::createObject(array_merge($this->identityCookie, [ 369 | 'class' => 'yii\web\Cookie', 370 | 'value' => $value, 371 | //验证cookie生存周期 372 | 'expire' => time() + (int) $data[2], 373 | ])); 374 | Yii::$app->getResponse()->getCookies()->add($cookie); 375 | } 376 | } 377 | } 378 | protected function loginByCookie() 379 | { 380 | $data = $this->getIdentityAndDurationFromCookie(); 381 | if (isset($data['identity'], $data['duration'])) { 382 | $identity = $data['identity']; 383 | $duration = $data['duration']; 384 | if ($this->beforeLogin($identity, true, $duration)) { 385 | $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); 386 | $id = $identity->getId(); 387 | $ip = Yii::$app->getRequest()->getUserIP(); 388 | Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); 389 | $this->afterLogin($identity, true, $duration); 390 | } 391 | } 392 | } 393 | protected function getIdentityAndDurationFromCookie() 394 | { 395 | //获取一开始存入cookie的认证信息 396 | $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']); 397 | if ($value === null) { 398 | return null; 399 | } 400 | $data = json_decode($value, true); 401 | if (is_array($data) && count($data) == 3) { 402 | list($id, $authKey, $duration) = $data; 403 | /* @var $class IdentityInterface */ 404 | $class = $this->identityClass; 405 | //通过cookie中的认证id去查具体的认证信息 406 | $identity = $class::findIdentity($id); 407 | if ($identity !== null) { 408 | if (!$identity instanceof IdentityInterface) { 409 | throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface."); 410 | } elseif (!$identity->validateAuthKey($authKey)) { 411 | //还会验证cookie中的AuthKey 412 | Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); 413 | } else { 414 | return ['identity' => $identity, 'duration' => $duration]; 415 | } 416 | } 417 | } 418 | //删除本次cookie信息 419 | $this->removeIdentityCookie(); 420 | return null; 421 | } 422 | ``` 423 | -------------------------------------------------------------------------------- /yii2/[数据库一]连接数据库源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [连接总体流程](#连接总体流程) 3 | * [master与slave连接的差异](#master与slave连接的差异) 4 | * [涉及方法](#涉及方法) 5 | * [属性注入](#属性注入) 6 | * [源码细节](#源码细节) 7 | 8 |  9 | # 连接总体流程 10 | 1.随机打乱从库配置(master可以选择是否打乱,slave一定会打乱;如果master没有配置数组则直接使用$dsn和$username作为master的配置,也就是一主) 11 | 2.遍历配置 12 | * 如果该库的配置在serverStatusCache缓存中生效则说明过期时间内该配置不可用,直接continue 13 | * 如果缓存无值则去实例化PDO(会新实例化一个类$db = Yii::createObject($config)) 14 | 15 | 3.实例化PDO 16 | * 记录info日志 17 | * 记录实例化性能分析日志(可选,根据$enableProfiling) 18 | * new PDO 19 | * 设置PDO的ATTR_ERRMODE、ATTR_EMULATE_PREPARES、字符集属性 20 | * 执行afterOpen事件 21 | 22 | 4.实例化失败 23 | * 记录serverStatusCache缓存,标识该配置600秒(默认)内不可用 24 | 25 | 26 | # master与slave连接的差异 27 | 1.slave连接会先判断是否可以使用slave从库($this->enableSlaves) 28 | 2.slave如果连接不上会判断是否进而连接master 29 | 3.一定会打乱slave配置数组 30 | 4.如果没有master配置数组,则直接使用$this->dns和$this->root 31 | 5.master连接可以选择是否打乱配置数组 32 | 33 | 34 | # 涉及方法 35 | * getSlavePdo($fallbackToMaster = true),会调用getSlave(false),如果从库连不上就去连接master(根据参数$fallbackToMaster),返回的是PDO类 36 | * getSlave($fallbackToMaster = true),会调用openFromPool,从slave配置数组中随机连接一个slave,返回的是Connection类 37 | * getMasterPdo(),会调用open(),如果master配置数据为空则直接使用$dns进行连接,如果master配置数组不为空则遍历连接master,返回PDO类 38 | * getMaster(),遍历连接master,返回Connection类 39 | * openFromPool(array $pool, array $sharedConfig),随机打乱配置信息 40 | * openFromPoolSequentially(array $pool, array $sharedConfig),不随机打乱配置信息,遍历配置信息,连接PDO;内部还有serverStatusCache去缓存服务器可用状态 41 | * open(),连接master或者slave,会记录info和Profiling日志(可选) 42 | * createPdoInstance(),实例化PDO 43 | * initConnection(),设置PDO配置和执行afterOpen事件 44 | 45 | 46 | # 属性注入 47 | 因为Connection继承Component类,可以使用属性注入,所以 48 | ``` 49 | $db->master; //和$db->getMaster();一样 50 | $db->slave; //和$db->getSlave();一样 51 | $db->masterPdo; //和$db->getMasterPdo();一样 52 | $db->slavePdo; //和$db->getSlavePdo();一样 53 | ``` 54 | # 源码细节 55 | yii2中可以配置一主多从配置,在连接从库方面数据库配置如下(控制器中连接配置) 56 | ``` 57 | 'mysql:host=192.168.124.10;dbname=test', 67 | 'username' => 'root', 68 | 'password' => '', 69 | 'charset' => 'utf8', 70 | 'enableSlaves'=>true, //可以使用从库 71 | 'serverRetryInterval'=>600, //其中一个从库配置不可用,将缓存不可用状态600秒 72 | 'enableProfiling'=>true, //默认配置,将记录连接数据库、执行语句等的性能分析日志 73 | 'emulatePrepare'=>true, //true为开启本地模拟prepare 74 | 'slaveConfig'=>[ //从库slaves属性通用配置 75 | 'username' => 'root', 76 | 'password' => '', 77 | 'attributes' => [ 78 | PDO::ATTR_TIMEOUT => 10, 79 | ], 80 | ], 81 | 'slaves'=>[ //从库列表 82 | ["dsn"=>"mysql:host=192.168.124.11;dbname=test"], 83 | ["dsn"=>"mysql:host=192.168.124.12;dbname=test"], 84 | [ 85 | "dsn"=>"mysql:host=192.168.124.13;dbname=test", 86 | 'username' => 'main', 87 | 'password' => '123456', 88 | ], 89 | ], 90 | 'masters'=>[ //主库列表 91 | ["dsn"=>"mysql:host=192.168.124.11;dbname=test"], 92 | ["dsn"=>"mysql:host=192.168.124.12;dbname=test"], 93 | [ 94 | "dsn"=>"mysql:host=192.168.124.13;dbname=test", 95 | 'username' => 'main', 96 | 'password' => '123456', 97 | ], 98 | ], 99 | 'masterConfig'=>[ //主库master属性通用配置 100 | 'username' => 'root', 101 | 'password' => '', 102 | 'attributes' => [ 103 | PDO::ATTR_TIMEOUT => 10, 104 | ], 105 | ], 106 | ]); 107 | $slave = $db->getSlavePdo(); 108 | $slave = $db->getSlave(); 109 | return 123; 110 | } 111 | } 112 | ``` 113 | 可以看到数据库操作的类是\yii\db\Connection,该类继承Component类,可见可以使用属性注入、行为和事件 114 | 115 | 针对Connection的属性注入,只有以下属性是私有的,以下属性一般不会在外部进行操作 116 | ``` 117 | private $_transaction; 118 | private $_schema; 119 | private $_driverName; 120 | private $_master = false; 121 | private $_slave = false; 122 | private $_queryCacheInfo = []; 123 | ``` 124 | 125 | 针对Connection的事件,可以注册以下事件 126 | ``` 127 | const EVENT_AFTER_OPEN = 'afterOpen'; //连接数据库后的事件 128 | const EVENT_BEGIN_TRANSACTION = 'beginTransaction'; //开启事务的事件 129 | const EVENT_COMMIT_TRANSACTION = 'commitTransaction'; //提交事务的事件 130 | const EVENT_ROLLBACK_TRANSACTION = 'rollbackTransaction'; //回滚的事件 131 | ``` 132 | 133 | Connection类使用的mysql操作对象是PDO,涉及方法有 134 | ``` 135 | public function getSlavePdo($fallbackToMaster = true) 136 | public function getSlave($fallbackToMaster = true) 137 | ``` 138 | 追进在getSlavePdo方法,可见当slave连接不可用时候,会默认连接主库($fallbackToMaster=true) 139 | ``` 140 | public function getSlavePdo($fallbackToMaster = true) 141 | { 142 | $db = $this->getSlave(false); //进行slave连接 143 | if ($db === null) { 144 | return $fallbackToMaster ? $this->getMasterPdo() : null; //当slave不可用时候,是否连接主库 145 | } 146 | 147 | return $db->pdo; //返回数据库连接资源,从库和主库都连接不上的话会返回null 148 | } 149 | ``` 150 | 追进getSlave方法 151 | ``` 152 | public function getSlave($fallbackToMaster = true) 153 | { 154 | if (!$this->enableSlaves) {//判断是否可以使用slave 155 | return $fallbackToMaster ? $this : null; 156 | } 157 | 158 | if ($this->_slave === false) { //如果还没有连接过slave库,就进行连接 159 | $this->_slave = $this->openFromPool($this->slaves, $this->slaveConfig); //将slave配置信息给openFromPool方法 160 | } 161 | return $this->_slave === null && $fallbackToMaster ? $this : $this->_slave; 162 | } 163 | ``` 164 | 追进openFromPool方法,可见该方法就是将$this->slaves从库dsn配置打乱,让第一次连接slave随机化 165 | ``` 166 | protected function openFromPool(array $pool, array $sharedConfig) 167 | { 168 | shuffle($pool); //打乱从库配置 169 | return $this->openFromPoolSequentially($pool, $sharedConfig); 170 | } 171 | ``` 172 | openFromPoolSequentially方法 173 | ``` 174 | protected function openFromPoolSequentially(array $pool, array $sharedConfig) 175 | { 176 | if (empty($pool)) { //是否有slave配置池,如果没有的话就是最后返回给$this->_slave为null 177 | return null; 178 | } 179 | 180 | if (!isset($sharedConfig['class'])) { //判断$this->slaveConfig属性是否有class,可以设置class将从库的连接配置成自己重新的类 181 | $sharedConfig['class'] = get_class($this); 182 | } 183 | //服务状态缓存,使用依赖注入获取cache缓存类 184 | $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache; 185 | //遍历slave配置池 186 | foreach ($pool as $config) { 187 | //合并配置 188 | $config = array_merge($sharedConfig, $config); 189 | if (empty($config['dsn'])) { 190 | throw new InvalidConfigException('The "dsn" option must be specified.'); 191 | } 192 | $key = [__METHOD__, $config['dsn']]; 193 | //这里就是判断缓存是否有值,如果有的话说明在过期时间内该配置的slave不可用 194 | if ($cache instanceof CacheInterface && $cache->get($key)) { 195 | // should not try this dead server now 196 | continue; 197 | } 198 | //通过依赖注入创建了一个类,该类专门是这个slave的 199 | $db = Yii::createObject($config); 200 | try { 201 | $db->open(); 202 | return $db; 203 | } catch (\Exception $e) { 204 | //记录日志 205 | Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__); 206 | if ($cache instanceof CacheInterface) { 207 | //将该配置的slave服务不可用状态存缓存,值是1,过期时间的$this->serverRetryInterval秒 208 | $cache->set($key, 1, $this->serverRetryInterval); 209 | } 210 | } 211 | } 212 | return null; 213 | } 214 | ``` 215 | 在TestController控制器的配置中,可见会随机打乱slaves属性,如果有任何一个从库连接上了就是直接返回,如果有连接不上的就会将不可用状态存缓存,然后继续循环 216 | 217 | slaveConfig属性是一个从库的通用配置,会循环的去array_merge()属性slaves 218 | 219 | 所以配置 220 | ``` 221 | 'slaveConfig'=>[ //从库slaves属性通用配置 222 | 'username' => 'root', 223 | 'password' => '', 224 | 'attributes' => [ 225 | PDO::ATTR_TIMEOUT => 10, 226 | ], 227 | ], 228 | 'slaves'=>[ //从库列表 229 | ["dsn"=>"mysql:host=192.168.124.11;dbname=test"], 230 | ["dsn"=>"mysql:host=192.168.124.12;dbname=test"], 231 | [ 232 | "dsn"=>"mysql:host=192.168.124.13;dbname=test", 233 | 'username' => 'main', 234 | 'password' => '123456', 235 | 'class'=> yii\overload\myDB 236 | ], 237 | ] 238 | ``` 239 | 最后生成的配置为(这个配置会被shuffle函数打乱顺序) 240 | ``` 241 | 'slaves'=>[ //从库列表 242 | [ 243 | "dsn"=>"mysql:host=192.168.124.11;dbname=test", 244 | 'username' => 'root', 245 | 'password' => '', 246 | 'attributes' => [ 247 | PDO::ATTR_TIMEOUT => 10, 248 | ], 249 | 'class' => 'yii\db\Connection', 250 | ], 251 | [ 252 | "dsn"=>"mysql:host=192.168.124.12;dbname=test" 253 | 'username' => 'root', 254 | 'password' => '', 255 | 'attributes' => [ 256 | PDO::ATTR_TIMEOUT => 10, 257 | ], 258 | 'class' => 'yii\db\Connection', 259 | ], 260 | [ 261 | "dsn"=>"mysql:host=192.168.124.13;dbname=test", 262 | 'username' => 'main', 263 | 'password' => '123456', 264 | 'class'=> yii\overload\myDB 265 | ], 266 | ] 267 | ``` 268 | 在open方法中,因为是重新new,所以$this->pdo和$this->master都是null 269 | ``` 270 | public function open() 271 | { 272 | //因为是重新new,所以$this->pdo和$this->master都是null 273 | if ($this->pdo !== null) { 274 | return; 275 | } 276 | //因为是重新new,所以$this->pdo和$this->master都是null 277 | if (!empty($this->masters)) { 278 | $db = $this->getMaster(); 279 | if ($db !== null) { 280 | $this->pdo = $db->pdo; 281 | return; 282 | } 283 | 284 | throw new InvalidConfigException('None of the master DB servers is available.'); 285 | } 286 | 287 | if (empty($this->dsn)) { 288 | throw new InvalidConfigException('Connection::dsn cannot be empty.'); 289 | } 290 | 291 | $token = 'Opening DB connection: ' . $this->dsn; 292 | $enableProfiling = $this->enableProfiling; 293 | try { 294 | //记录日志 295 | Yii::info($token, __METHOD__); 296 | //如果开启了性能分析,则记录性能分析日志(性能分析开启) 297 | if ($enableProfiling) { 298 | Yii::beginProfile($token, __METHOD__); 299 | } 300 | 301 | $this->pdo = $this->createPdoInstance(); 302 | $this->initConnection(); 303 | //如果开启了性能分析,则记录性能分析日志(性能分析关闭) 304 | if ($enableProfiling) { 305 | Yii::endProfile($token, __METHOD__); 306 | } 307 | } catch (\PDOException $e) { 308 | if ($enableProfiling) { 309 | Yii::endProfile($token, __METHOD__); 310 | } 311 | 312 | throw new Exception($e->getMessage(), $e->errorInfo, (int) $e->getCode(), $e); 313 | } 314 | } 315 | ``` 316 | 在createPdoInstance方法中,这个没什么好说的,就是执行new PDO 317 | ``` 318 | protected function createPdoInstance() 319 | { 320 | $pdoClass = $this->pdoClass; 321 | if ($pdoClass === null) { 322 | $pdoClass = 'PDO'; 323 | if ($this->_driverName !== null) { 324 | $driver = $this->_driverName; 325 | } elseif (($pos = strpos($this->dsn, ':')) !== false) { 326 | $driver = strtolower(substr($this->dsn, 0, $pos)); 327 | } 328 | if (isset($driver)) { 329 | if ($driver === 'mssql' || $driver === 'dblib') { 330 | $pdoClass = 'yii\db\mssql\PDO'; 331 | } elseif ($driver === 'sqlsrv') { 332 | $pdoClass = 'yii\db\mssql\SqlsrvPDO'; 333 | } 334 | } 335 | } 336 | 337 | $dsn = $this->dsn; 338 | if (strncmp('sqlite:@', $dsn, 8) === 0) { 339 | $dsn = 'sqlite:' . Yii::getAlias(substr($dsn, 7)); 340 | } 341 | 342 | return new $pdoClass($dsn, $this->username, $this->password, $this->attributes); 343 | } 344 | ``` 345 | 在initConnection方法中,这个也没什么好说的,就是去设置PDO属性和执行afterOpen事件 346 | ``` 347 | protected function initConnection() 348 | { 349 | $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 350 | if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) { 351 | $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); 352 | } 353 | if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'], true)) { 354 | $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset)); 355 | } 356 | $this->trigger(self::EVENT_AFTER_OPEN); 357 | } 358 | ``` 359 | 主库连接getMaterPdo()方法 360 | ``` 361 | public function getMasterPdo() 362 | { 363 | $this->open(); 364 | return $this->pdo; 365 | } 366 | ``` 367 | 主库连接getMaster()方法 368 | ``` 369 | public function getMaster() 370 | { 371 | if ($this->_master === false) { 372 | $this->_master = $this->shuffleMasters //是否随机打乱master配置数组 373 | ? $this->openFromPool($this->masters, $this->masterConfig) 374 | : $this->openFromPoolSequentially($this->masters, $this->masterConfig); 375 | } 376 | 377 | return $this->_master; 378 | } 379 | ``` 380 | -------------------------------------------------------------------------------- /yii2/[数据库三]事务源码.md: -------------------------------------------------------------------------------- 1 | # 事务与事务嵌套 2 | yii有两种事务模式,一种是需要自己commit和捕获异常rollback,还有一种令是使用回调的方式,如控制器代码如下: 3 | ``` 4 | public function actionTest() 5 | { 6 | //db组件配置 7 | $db = new \yii\db\Connection([ 8 | 'dsn' => 'mysql:host=192.168.0.10;dbname=test', 9 | 'username' => 'root', 10 | 'password' => '', 11 | 'charset' => 'utf8', 12 | 'enableSlaves'=>true, //可以使用从库 13 | 'serverRetryInterval'=>600, //其中一个从库配置不可用,将缓存不可用状态600秒 14 | 'enableProfiling'=>true, //默认配置,将记录连接数据库、执行语句等的性能分析日志 15 | 'emulatePrepare'=>true, //true为开启本地模拟prepare 16 | 'shuffleMasters'=>false, 17 | 'serverStatusCache'=>false, 18 | 'slaveConfig'=>[ //从库slaves属性通用配置 19 | 'username' => 'root', 20 | 'password' => '', 21 | 'attributes' => [ 22 | PDO::ATTR_TIMEOUT => 1, 23 | ], 24 | ], 25 | 'slaves'=>[ //从库列表 26 | ["dsn"=>"mysql:host=192.168.0.10;dbname=test"], 27 | ], 28 | 'masters'=>[ //主库列表 29 | ["dsn"=>"mysql:host=192.168.0.10;dbname=test"], 30 | ], 31 | 'masterConfig'=>[ //主库master属性通用配置 32 | 'username' => 'root', 33 | 'password' => '', 34 | 'attributes' => [ 35 | PDO::ATTR_TIMEOUT => 1, 36 | ], 37 | ], 38 | ]); 39 | //第一种事务模式,需要自己去commit和捕获异常rollback 40 | $transaction = $db->beginTransaction(); 41 | try { 42 | $command1 = $db->createCommand("insert into a([[age]]) value(:age)"); 43 | $res1 = $command1->bindValue(":age",111)->execute(); 44 | $command2 = $db->createCommand("update a set age = 1"); 45 | $res2 = $command2->execute(); 46 | $transaction->commit(); 47 | } catch (\Exception $e) { 48 | $transaction->rollback(); 49 | } 50 | //第二种事务模式,不用自己去捕获异常然后去rollback 51 | $db->transaction(function($db){ 52 | $command1 = $db->createCommand("insert into a([[age]]) value(:age)"); 53 | $res1 = $command1->bindValue(":age",111)->execute(); 54 | $command2 = $db->createCommand("update a set age = 1"); 55 | $res2 = $command2->execute(); 56 | }); 57 | return 123; 58 | } 59 | ``` 60 | 我们先从第一种模式的源码开始看,第一个方法是beginTransaction 61 | 从源码分析可知,事务一定会去连接master,具体master和slave连接源码分析请看我的另一篇连接数据库的源码分析文章 62 | ``` 63 | public function beginTransaction($isolationLevel = null) 64 | { 65 | //事务一定会去连接master 66 | $this->open(); 67 | //已经开启了事务就不用再去实例化Transaction类了 68 | if (($transaction = $this->getTransaction()) === null) { 69 | $transaction = $this->_transaction = new Transaction(['db' => $this]); 70 | } 71 | $transaction->begin($isolationLevel); 72 | //返回Transaction对象 73 | return $transaction; 74 | } 75 | ``` 76 | 可见事务的核心是一个单独的Transaction类,该类继承与BaseObject所以只有属性注入,无行为behavior和事件Events 77 | ``` 78 | class Transaction extends \yii\base\BaseObject 79 | { 80 | //以下几个常量都是隔离级别 81 | const READ_UNCOMMITTED = 'READ UNCOMMITTED'; 82 | const READ_COMMITTED = 'READ COMMITTED'; 83 | const REPEATABLE_READ = 'REPEATABLE READ'; 84 | const SERIALIZABLE = 'SERIALIZABLE'; 85 | public $db; 86 | //事务层级,用来模拟多层级事务 87 | private $_level = 0; 88 | ``` 89 | 其实yii的事务是有几个Events事件的,只不过事件标识是挂在Connection类的 90 | ``` 91 | class Connection extends Component 92 | { 93 | const EVENT_AFTER_OPEN = 'afterOpen'; 94 | //事务开启事件标识 95 | const EVENT_BEGIN_TRANSACTION = 'beginTransaction'; 96 | //事务提交事件标识 97 | const EVENT_COMMIT_TRANSACTION = 'commitTransaction'; 98 | //事务回滚事件标识 99 | const EVENT_ROLLBACK_TRANSACTION = 'rollbackTransaction'; 100 | ``` 101 | 回到Transaction类的begin方法 102 | 我们知道mysql是不支持多事务层级的,在第一次begin开启事务之后,再次begin会commit第一次的事务,yii根据事务层级level和savepoint来模拟多事务层级 103 | 单事务源码逻辑: 104 | 1.第一次开启事务,事务层级level是1(自增) 105 | 2.提交或者回滚,事务层级level为0(自减),会真正的commit或者rollback 106 | 107 | 108 | 109 | 多事务源码逻辑 110 | 1.第一次开启事务,事务层级level是1(自增) 111 | 2.再次开启事务,事务层级level是2(自增),savepoint名字是LEVEL2 112 | 3.再次开启事务,事务层级level是3(自增),savepoint名字是LEVEL3 113 | 4.提交,事务层级level是2(自减),删除名字是LEVEL3的savepint,不会真正的commit 114 | 5.回滚,事务层级level是1(自减),回滚名字是LEVEL2的savepoint,会真实rollback同时也会把LEVEL2的savepoint删除 115 | 6.提交或者回滚,因为事务层级level是1自减为0,所以表示是最外层事务了,会真正的commit或者rollback 116 | ``` 117 | public function begin($isolationLevel = null) 118 | { 119 | if ($this->db === null) { 120 | throw new InvalidConfigException('Transaction::db must be set.'); 121 | } 122 | //连接master 123 | $this->db->open(); 124 | //如果是第一次开启事务 125 | if ($this->_level === 0) { 126 | if ($isolationLevel !== null) { 127 | //设置事务隔离级别 128 | $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel); 129 | } 130 | //记录debug级别日志 131 | Yii::debug('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__); 132 | //触发开启事件事件 133 | $this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION); 134 | //pdo开启事务 135 | $this->db->pdo->beginTransaction(); 136 | //事务层级为1 137 | $this->_level = 1; 138 | 139 | return; 140 | } 141 | //如果不是第一次开启事务(多层级事务) 142 | $schema = $this->db->getSchema(); 143 | //判断db组件配置是否支持使用多层级事务 144 | if ($schema->supportsSavepoint()) { 145 | //记录debug级别日志 146 | Yii::debug('Set savepoint ' . $this->_level, __METHOD__); 147 | //打一个savepoint点,注意savepoint点的名字是和事务层级有关 148 | $schema->createSavepoint('LEVEL' . $this->_level); 149 | } else { 150 | Yii::info('Transaction not started: nested transaction not supported', __METHOD__); 151 | throw new NotSupportedException('Transaction not started: nested transaction not supported.'); 152 | } 153 | //多层级事务,自增事务层级 154 | $this->_level++; 155 | } 156 | ``` 157 | 提交commit的源码 158 | ``` 159 | public function commit() 160 | { 161 | if (!$this->getIsActive()) { 162 | throw new Exception('Failed to commit transaction: transaction was inactive.'); 163 | } 164 | //自减 165 | $this->_level--; 166 | //只剩下了一个事务 167 | if ($this->_level === 0) { 168 | Yii::debug('Commit transaction', __METHOD__); 169 | //真实提交 170 | $this->db->pdo->commit(); 171 | //触发提交事件 172 | $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION); 173 | return; 174 | } 175 | //多层级事务 176 | $schema = $this->db->getSchema(); 177 | //判断是否支持多层级事务 178 | if ($schema->supportsSavepoint()) { 179 | Yii::debug('Release savepoint ' . $this->_level, __METHOD__); 180 | //删除savepint,根据事务层级来删 181 | $schema->releaseSavepoint('LEVEL' . $this->_level); 182 | } else { 183 | Yii::info('Transaction not committed: nested transaction not supported', __METHOD__); 184 | } 185 | } 186 | ``` 187 | 回滚rollback源码 188 | ``` 189 | public function rollBack() 190 | { 191 | if (!$this->getIsActive()) { 192 | // do nothing if transaction is not active: this could be the transaction is committed 193 | // but the event handler to "commitTransaction" throw an exception 194 | return; 195 | } 196 | //自减 197 | $this->_level--; 198 | //只剩下了一个事务 199 | if ($this->_level === 0) { 200 | Yii::debug('Roll back transaction', __METHOD__); 201 | //真实回滚 202 | $this->db->pdo->rollBack(); 203 | //触发回滚事件 204 | $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION); 205 | return; 206 | } 207 | 208 | $schema = $this->db->getSchema(); 209 | //多层级事务 210 | //判断是否支持多层级事务 211 | if ($schema->supportsSavepoint()) { 212 | Yii::debug('Roll back to savepoint ' . $this->_level, __METHOD__); 213 | //回滚savepoint,会真实回滚 214 | $schema->rollBackSavepoint('LEVEL' . $this->_level); 215 | } else { 216 | Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__); 217 | } 218 | } 219 | ``` 220 | yii的第二种事务方式是不用自己捕获事务,实现的方案就是源码给封装好了try,源码很简单就不多做解释 221 | ``` 222 | public function transaction(callable $callback, $isolationLevel = null) 223 | { 224 | //开事务 225 | $transaction = $this->beginTransaction($isolationLevel); 226 | $level = $transaction->level; 227 | 228 | try { 229 | //执行回调 230 | $result = call_user_func($callback, $this); 231 | //成功的话就commit 232 | if ($transaction->isActive && $transaction->level === $level) { 233 | $transaction->commit(); 234 | } 235 | } catch (\Exception $e) { 236 | //失败就回滚 237 | $this->rollbackTransactionOnLevel($transaction, $level); 238 | throw $e; 239 | } catch (\Throwable $e) { 240 | //代码语法级别失败就回滚 241 | $this->rollbackTransactionOnLevel($transaction, $level); 242 | throw $e; 243 | } 244 | 245 | return $result; 246 | } 247 | ... 248 | private function rollbackTransactionOnLevel($transaction, $level) 249 | { 250 | if ($transaction->isActive && $transaction->level === $level) { 251 | // https://github.com/yiisoft/yii2/pull/13347 252 | try { 253 | $transaction->rollBack(); 254 | } catch (\Exception $e) { 255 | \Yii::error($e, __METHOD__); 256 | // hide this exception to be able to continue throwing original exception outside 257 | } 258 | } 259 | } 260 | ``` 261 | -------------------------------------------------------------------------------- /yii2/[数据库二]执行sql源码.md: -------------------------------------------------------------------------------- 1 | yii对比与普通的原生pdo操作增加了 2 | - 引用表名、列名 3 | - 查询缓存 4 | - php数据类型和mysql-pdo属性类型的映射 5 | - pdo->execute异常下的重试回调 6 | - query类读slave,execute类写master 7 | - 日志、性能分析日志 8 | 9 | 控制器代码如下 10 | ``` 11 | class TestController extends Controller 12 | { 13 | public function actionD(){ 14 | $db = new \yii\db\Connection([ 15 | 'dsn' => 'mysql:host=192.168.124.10;dbname=test', 16 | 'username' => 'root', 17 | 'password' => '', 18 | 'charset' => 'utf8', 19 | 20 | ]); 21 | $command = $db->createCommand("select * from a"); 22 | $res1 = $command->queryAll(); 23 | $res2 = $command->queryOne(); 24 | $res3 = $command->queryColumn(); 25 | $command = $db->createCommand("select * from {{a}} where [[id]]=:id",[":id"=>"1"]); 26 | $res1 = $command->queryAll(); 27 | $command = $db->createCommand("select * from {{a}} where [[id]]=:id",[":id"=>"1"]); 28 | $res1 = $command->queryAll(); 29 | $command = $db->createCommand("update a set age = 123 where id=:id") 30 | ->bindValue(":id",1) 31 | ->execute(); 32 | return $this->asJson($res1); 33 | } 34 | } 35 | ``` 36 | 可见执行原生的sql都要通过createCommand()返回的对象来进行操作,createCommand()就是实例化了yii2\db\Command类 37 | ``` 38 | public function createCommand($sql = null, $params = []) 39 | { 40 | //获取数据库的驱动类型 41 | $driver = $this->getDriverName(); 42 | $config = ['class' => 'yii\db\Command']; 43 | if ($this->commandClass !== $config['class']) { 44 | $config['class'] = $this->commandClass; 45 | } elseif (isset($this->commandMap[$driver])) { 46 | //可以任意扩展最后实例化的Command类,就是通过将$this->commandMap[$driver]设置为自定义的数组类型 47 | $config = !is_array($this->commandMap[$driver]) ? ['class' => $this->commandMap[$driver]] : $this->commandMap[$driver]; 48 | } 49 | $config['db'] = $this; 50 | $config['sql'] = $sql; 51 | //实例化 52 | $command = Yii::createObject($config); 53 | //参数绑定 54 | return $command->bindValues($params); 55 | } 56 | ``` 57 | 获取数据库的驱动类型就是通过$dsn或者$masters、$masterConfig、$slaves、$slaveConfig配置来获取,比如 58 | ``` 59 | $dsn = "mysql:host=127.0.0.1;dbname=test" 60 | ``` 61 | 数据库驱动类型就是mysql,这里涉及到了连接数据库直接从pdo对象获取类型(如果$dsn没有":") 62 | ``` 63 | public function getDriverName() 64 | { 65 | if ($this->_driverName === null) { 66 | if (($pos = strpos($this->dsn, ':')) !== false) { 67 | $this->_driverName = strtolower(substr($this->dsn, 0, $pos)); 68 | } else { 69 | //如果dsn属性没有':',就去连接slave 70 | //直接通过pdo对象去拿驱动类型 71 | $this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME)); 72 | } 73 | } 74 | 75 | return $this->_driverName; 76 | } 77 | ``` 78 | Command类就是根据sql命令来实例化的,继承于Component,说明可以使用属性注入、方法和事件 79 | 但是Command没有可用的事件 80 | 因为Command的$this->_ sql属性是private,所以走了属性注入setSql()方法 81 | ``` 82 | public function setSql($sql) 83 | { 84 | if ($sql !== $this->_sql) { 85 | $this->cancel(); 86 | $this->reset(); 87 | $this->_sql = $this->db->quoteSql($sql); 88 | } 89 | return $this; 90 | } 91 | ``` 92 | yii使用了引用表名和列名,就是将表名table_name写成{{%table_name}},列column写成[[column]] 93 | quoteSql内部其实就是根据驱动类型实例化了Schema对象,去根据Schema去将列的前后拼上columnQuoteCharacter,表名前后拼上tableQuoteCharacter,然后如果表名里面有%,会将%转为表前缀,如: 94 | ``` 95 | $db->tablePrefix="main_"; 96 | ... 97 | $sql = 'select count([[id]]) from {{%table}}'; 98 | ... 99 | ``` 100 | 最后会转换成 101 | ``` 102 | select count `id` from `main_table` 103 | ``` 104 | cancel方法就是重置pdo的prepare操作 105 | ``` 106 | public function cancel() 107 | { 108 | $this->pdoStatement = null; 109 | } 110 | ``` 111 | reset方法就是重置与Command类相关的属性,这里需要注意也会将$this->_ retryHandler重置,所以如果使用异常重试机制,需要在createCommand后再设置一遍retryHandler 112 | ``` 113 | protected function reset() 114 | { 115 | $this->_sql = null; 116 | //参数绑定 117 | $this->_pendingParams = []; 118 | //参数绑定 119 | $this->params = []; 120 | $this->_refreshTableName = null; 121 | //事务层级 122 | $this->_isolationLevel = false; 123 | //pdo的execute方法抛出异常的重试回调 124 | $this->_retryHandler = null; 125 | } 126 | ``` 127 | 重新注册异常重试(控制器部分代码) 128 | ``` 129 | $command = $db->createCommand("select * from a"); 130 | $command->setRetryHandler(function(){ 131 | echo "语句执行发生了异常"; 132 | }); 133 | ``` 134 | 实例化最后Command类后还要调用Command->bindValues(),yii的参数绑定会用php的数据类型和mysql-pdo的数据类型做对应 135 | ``` 136 | public function bindValues($values) 137 | { 138 | if (empty($values)) { 139 | return $this; 140 | } 141 | 142 | $schema = $this->db->getSchema(); 143 | foreach ($values as $name => $value) { 144 | if (is_array($value)) { 145 | //这里会在以后的版本被废弃调 146 | $this->_pendingParams[$name] = $value; 147 | $this->params[$name] = $value[0]; 148 | } elseif ($value instanceof PdoValue) { 149 | $this->_pendingParams[$name] = [$value->getValue(), $value->getType()]; 150 | $this->params[$name] = $value->getValue(); 151 | } else { 152 | //获取值的类型,将PHP的类型和mysql-PDO的类型做对应 153 | $type = $schema->getPdoType($value); 154 | $this->_pendingParams[$name] = [$value, $type]; 155 | $this->params[$name] = $value; 156 | } 157 | } 158 | 159 | return $this; 160 | } 161 | ... 162 | public function getPdoType($data) 163 | { 164 | static $typeMap = [ 165 | // php type => PDO type 166 | 'boolean' => \PDO::PARAM_BOOL, 167 | 'integer' => \PDO::PARAM_INT, 168 | 'string' => \PDO::PARAM_STR, 169 | 'resource' => \PDO::PARAM_LOB, 170 | 'NULL' => \PDO::PARAM_NULL, 171 | ]; 172 | $type = gettype($data); 173 | //php的数据类型和mysql-pdo的数据类型做对应,如果对应不上就会认为是PDO::PARAM_STR类型 174 | return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; 175 | } 176 | ``` 177 | 然后执行queryAll、queryOne、queryColumn进行查询,可见其实调用的都是queryInternal 178 | ``` 179 | public function queryAll($fetchMode = null) 180 | { 181 | return $this->queryInternal('fetchAll', $fetchMode); 182 | } 183 | public function queryOne($fetchMode = null) 184 | { 185 | return $this->queryInternal('fetch', $fetchMode); 186 | } 187 | public function queryColumn() 188 | { 189 | return $this->queryInternal('fetchAll', \PDO::FETCH_COLUMN); 190 | } 191 | ``` 192 | queryInternal方法如下 193 | ``` 194 | protected function queryInternal($method, $fetchMode = null) 195 | { 196 | //拿到真实的参数绑定后的sql语句$rawSql,拿到是否使用性能分析标识$profile 197 | list($profile, $rawSql) = $this->logQuery('yii\db\Command::query'); 198 | if ($method !== '') { 199 | //这里就是去Connection类拿到QueryCache 200 | $info = $this->db->getQueryCacheInfo($this->queryCacheDuration, $this->queryCacheDependency); 201 | if (is_array($info)) { 202 | //cache对象 203 | $cache = $info[0]; 204 | $rawSql = $rawSql ?: $this->getRawSql(); 205 | //生成QueryCache键名 206 | $cacheKey = $this->getCacheKey($method, $fetchMode, $rawSql); 207 | //get缓存操作 208 | $result = $cache->get($cacheKey); 209 | if (is_array($result) && isset($result[0])) { 210 | Yii::debug('Query result served from cache', 'yii\db\Command::query'); 211 | return $result[0]; 212 | } 213 | } 214 | } 215 | //就是拿到$pdo->prepare()并且进行参数绑定,会根据sql类型进行判断是用slave的还是master的 216 | $this->prepare(true); 217 | 218 | try { 219 | //开启性能分析日志 220 | $profile and Yii::beginProfile($rawSql, 'yii\db\Command::query'); 221 | //这里就是真正执行sql了 222 | $this->internalExecute($rawSql); 223 | //这里不太清楚,不用太抠这些细节 224 | if ($method === '') { 225 | $result = new DataReader($this); 226 | } else { 227 | if ($fetchMode === null) { 228 | $fetchMode = $this->fetchMode; 229 | } 230 | $result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode); 231 | $this->pdoStatement->closeCursor(); 232 | } 233 | 234 | $profile and Yii::endProfile($rawSql, 'yii\db\Command::query'); 235 | } catch (Exception $e) { 236 | $profile and Yii::endProfile($rawSql, 'yii\db\Command::query'); 237 | throw $e; 238 | } 239 | //存缓存 240 | if (isset($cache, $cacheKey, $info)) { 241 | $cache->set($cacheKey, [$result], $info[1], $info[2]); 242 | Yii::debug('Saved query result in cache', 'yii\db\Command::query'); 243 | } 244 | 245 | return $result; 246 | } 247 | ``` 248 | prepare方法如下 249 | ``` 250 | public function prepare($forRead = null) 251 | { 252 | if ($this->pdoStatement) { 253 | $this->bindPendingParams(); 254 | return; 255 | } 256 | 257 | $sql = $this->getSql(); 258 | 259 | if ($this->db->getTransaction()) { 260 | // master is in a transaction. use the same connection. 261 | $forRead = false; 262 | } 263 | //这里就是根据sql类型来判断是连接slave还是master,从读主写 264 | if ($forRead || $forRead === null && $this->db->getSchema()->isReadQuery($sql)) { 265 | $pdo = $this->db->getSlavePdo(); 266 | } else { 267 | $pdo = $this->db->getMasterPdo(); 268 | } 269 | 270 | try { 271 | $this->pdoStatement = $pdo->prepare($sql); 272 | //参数绑定 273 | $this->bindPendingParams(); 274 | } catch (\Exception $e) { 275 | $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; 276 | $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; 277 | throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); 278 | } 279 | } 280 | ``` 281 | 如果不能根据参数来判断是读类型sql还是写类型sql,就是去isReadQuery(),可见select|show|describe都是读操作 282 | ``` 283 | public function isReadQuery($sql) 284 | { 285 | $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i'; 286 | return preg_match($pattern, $sql) > 0; 287 | } 288 | ``` 289 | internalExecute代码如下 290 | ``` 291 | protected function internalExecute($rawSql) 292 | { 293 | $attempt = 0; 294 | while (true) { 295 | try { 296 | //这里会进行模拟多层级事务,这块会放到事务部分去讲解 297 | if ( 298 | ++$attempt === 1 299 | && $this->_isolationLevel !== false 300 | && $this->db->getTransaction() === null 301 | ) { 302 | $this->db->transaction(function () use ($rawSql) { 303 | $this->internalExecute($rawSql); 304 | }, $this->_isolationLevel); 305 | } else { 306 | //就是$pdo->execute() 307 | $this->pdoStatement->execute(); 308 | } 309 | break; 310 | } catch (\Exception $e) { 311 | $rawSql = $rawSql ?: $this->getRawSql(); 312 | //这里返回了一个\yii\db\Exception 313 | $e = $this->db->getSchema()->convertException($e, $rawSql); 314 | //执行异常重试 315 | if ($this->_retryHandler === null || !call_user_func($this->_retryHandler, $e, $attempt)) { 316 | throw $e; 317 | } 318 | } 319 | } 320 | } 321 | ``` 322 | yii会认为调用query类的方法都是读操作并且连接slave库,调用execute的方法都是写操作去调用master库 323 | ``` 324 | public function execute() 325 | { 326 | $sql = $this->getSql(); 327 | list($profile, $rawSql) = $this->logQuery(__METHOD__); 328 | 329 | if ($sql == '') { 330 | return 0; 331 | } 332 | 333 | $this->prepare(false); 334 | 335 | try { 336 | $profile and Yii::beginProfile($rawSql, __METHOD__); 337 | 338 | $this->internalExecute($rawSql); 339 | $n = $this->pdoStatement->rowCount(); 340 | 341 | $profile and Yii::endProfile($rawSql, __METHOD__); 342 | 343 | $this->refreshTableSchema(); 344 | 345 | return $n; 346 | } catch (Exception $e) { 347 | $profile and Yii::endProfile($rawSql, __METHOD__); 348 | throw $e; 349 | } 350 | } 351 | ``` 352 | 这里说一下getSql方法和getRawSql方法的区别 353 | ``` 354 | public function getSql() 355 | { 356 | //如果sql是select * from a where id=:id,那么返回的就是select * from a where id=:id 357 | return $this->_sql; 358 | } 359 | ``` 360 | ``` 361 | public function getRawSql() 362 | { 363 | if (empty($this->params)) { 364 | return $this->_sql; 365 | } 366 | $params = []; 367 | foreach ($this->params as $name => $value) { 368 | if (is_string($name) && strncmp(':', $name, 1)) { 369 | $name = ':' . $name; 370 | } 371 | if (is_string($value)) { 372 | $params[$name] = $this->db->quoteValue($value); 373 | } elseif (is_bool($value)) { 374 | $params[$name] = ($value ? 'TRUE' : 'FALSE'); 375 | } elseif ($value === null) { 376 | $params[$name] = 'NULL'; 377 | } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) { 378 | $params[$name] = $value; 379 | } 380 | } 381 | if (!isset($params[1])) { 382 | //这里其实是重点 383 | //如果sql是select * from a where id=:id,参数绑定是:id=1,那么会变成select * from a where id=1 384 | return strtr($this->_sql, $params); 385 | } 386 | $sql = ''; 387 | foreach (explode('?', $this->_sql) as $i => $part) { 388 | $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; 389 | } 390 | 391 | return $sql; 392 | } 393 | ``` 394 | -------------------------------------------------------------------------------- /yii2/[数据库四]批处理查询源码.md: -------------------------------------------------------------------------------- 1 | yii的批处理查询使用PDO的fetch实现,fetch就是一个游标,每次读出一行然后移动游标到下一位(fetchAll是一次读出所有数据到内存),PDO原生代码如下 2 | ``` 3 | PDO::FETCH_ASSOC, 8 | PDO::ATTR_TIMEOUT=>1, 9 | PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, 10 | ]; 11 | try { 12 | $pdo = new PDO($dsn,"root","",$options); 13 | $pdo->quote("set name".$pdo->quote("utf8")); 14 | $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT,1); 15 | } catch (Exception $e) { 16 | var_dump($e->getMessage()); 17 | } 18 | $rawSql = "select * from a"; 19 | $pdoStatement = $pdo->prepare($rawSql); 20 | try { 21 | $pdoStatement->execute(); 22 | $res = []; 23 | $count = 0; 24 | while($count++ < $this->batchSize && ($row = $pdoStatement->fetch())){ 25 | $res[] = $row; 26 | } 27 | $pdoStatement->closeCursor(); 28 | } catch (Exception $e) { 29 | var_dump($e); 30 | $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; 31 | var_dump($message); 32 | } 33 | ``` 34 | 在yii中使用批处理控制器的代码如下 35 | ``` 36 | from("a"); 50 | foreach ($query->batch(2) as $item) { 51 | var_dump($item); 52 | } 53 | foreach ($query->each(2) as $item) { 54 | var_dump($item); 55 | } 56 | } 57 | ``` 58 | 追到代码里面,发现是通过查询构造器、BatchQueryResult类、DataReader类一起配合实现的 59 | 查询构造器里面实例化BatchQueryResult类代码如下 60 | ``` 61 | public function batch($batchSize = 100, $db = null) 62 | { 63 | return Yii::createObject([ 64 | 'class' => BatchQueryResult::className(), 65 | 'query' => $this, 66 | 'batchSize' => $batchSize, 67 | 'db' => $db, 68 | 'each' => false, 69 | ]); 70 | } 71 | public function each($batchSize = 100, $db = null) 72 | { 73 | return Yii::createObject([ 74 | 'class' => BatchQueryResult::className(), 75 | 'query' => $this, 76 | 'batchSize' => $batchSize, 77 | 'db' => $db, 78 | 'each' => true, 79 | ]); 80 | } 81 | ``` 82 | BatchQueryResult类是没有自己的构造方法的,继承于BaseObj,没有什么可用的属性注入,他实现了接口Iterator,可见是一个迭代器 83 | ``` 84 | class BatchQueryResult extends BaseObject implements \Iterator 85 | ``` 86 | 接口Iterator可以将类进行foreach操作,具体代码如下 87 | ``` 88 | class Obj implements Iterator{ 89 | public $arr = [1,2,3]; 90 | private $_key = 0; 91 | public function rewind(){ 92 | var_dump(__METHOD__); 93 | $this->_key = 0; 94 | } 95 | public function valid(){ 96 | var_dump(__METHOD__); 97 | return isset($this->arr[$this->_key]); 98 | } 99 | 100 | public function next(){ 101 | var_dump(__METHOD__); 102 | ++$this->_key; 103 | } 104 | 105 | public function current(){ 106 | var_dump(__METHOD__); 107 | return $this->arr[$this->_key]; 108 | } 109 | 110 | public function key() { 111 | var_dump(__METHOD__); 112 | return $this->_key; 113 | } 114 | } 115 | $obj = new Obj(); 116 | foreach($obj as $key=>$item){ 117 | var_dump($key."--->".$item); 118 | } 119 | ``` 120 | BatchQueryResult类的遍历初始化代码如下,也就是foreach需要执行的第一个方法 121 | ``` 122 | public function reset() 123 | { 124 | if ($this->_dataReader !== null) { 125 | //用于析构方法将游标关闭 126 | $this->_dataReader->close(); 127 | } 128 | $this->_dataReader = null; 129 | $this->_batch = null; 130 | $this->_value = null; 131 | $this->_key = null; 132 | } 133 | 134 | //foreach需要执行的第一个方法 135 | public function rewind() 136 | { 137 | $this->reset(); 138 | $this->next(); 139 | } 140 | 141 | public function next() 142 | { 143 | if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) { 144 | //实例化dataReader类 145 | $this->_batch = $this->fetchData(); 146 | //指针放到头 147 | reset($this->_batch); 148 | } 149 | 150 | if ($this->each) { 151 | $this->_value = current($this->_batch); 152 | if ($this->query->indexBy !== null) { 153 | $this->_key = key($this->_batch); 154 | } elseif (key($this->_batch) !== null) { 155 | $this->_key = $this->_key === null ? 0 : $this->_key + 1; 156 | } else { 157 | $this->_key = null; 158 | } 159 | } else { 160 | $this->_value = $this->_batch; 161 | $this->_key = $this->_key === null ? 0 : $this->_key + 1; 162 | } 163 | } 164 | ``` 165 | 与Command类建立联系的代码如下 166 | ``` 167 | protected function fetchData() 168 | { 169 | if ($this->_dataReader === null) { 170 | //可以理解为Yii::$app->get("db")->createCommand(sql)->query() 171 | $this->_dataReader = $this->query->createCommand($this->db)->query(); 172 | } 173 | $rows = []; 174 | $count = 0; 175 | //游标遍历 176 | while ($count++ < $this->batchSize && ($row = $this->_dataReader->read())) { 177 | $rows[] = $row; 178 | } 179 | //处理indexBy 180 | return $this->query->populate($rows); 181 | } 182 | ``` 183 | Command类与DataReader建立联系的代码如下(更详细的Command源码操作可以看以前的文章) 184 | 参数method是空,所以会直接实例化DataReade 185 | ``` 186 | protected function queryInternal($method, $fetchMode = null) 187 | { 188 | list($profile, $rawSql) = $this->logQuery('yii\db\Command::query'); 189 | if ($method !== '') { 190 | $info = $this->db->getQueryCacheInfo($this->queryCacheDuration, $this->queryCacheDependency); 191 | if (is_array($info)) { 192 | /* @var $cache \yii\caching\CacheInterface */ 193 | $cache = $info[0]; 194 | $rawSql = $rawSql ?: $this->getRawSql(); 195 | $cacheKey = $this->getCacheKey($method, $fetchMode, $rawSql); 196 | $result = $cache->get($cacheKey); 197 | if (is_array($result) && isset($result[0])) { 198 | Yii::debug('Query result served from cache', 'yii\db\Command::query'); 199 | return $result[0]; 200 | } 201 | } 202 | } 203 | 204 | $this->prepare(true); 205 | 206 | try { 207 | $profile and Yii::beginProfile($rawSql, 'yii\db\Command::query'); 208 | 209 | $this->internalExecute($rawSql); 210 | 211 | if ($method === '') { 212 | //这里就是建立联系的代码 213 | $result = new DataReader($this); 214 | } else { 215 | if ($fetchMode === null) { 216 | $fetchMode = $this->fetchMode; 217 | } 218 | $result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode); 219 | $this->pdoStatement->closeCursor(); 220 | } 221 | 222 | $profile and Yii::endProfile($rawSql, 'yii\db\Command::query'); 223 | } catch (Exception $e) { 224 | $profile and Yii::endProfile($rawSql, 'yii\db\Command::query'); 225 | throw $e; 226 | } 227 | 228 | if (isset($cache, $cacheKey, $info)) { 229 | $cache->set($cacheKey, [$result], $info[1], $info[2]); 230 | Yii::debug('Saved query result in cache', 'yii\db\Command::query'); 231 | } 232 | 233 | return $result; 234 | } 235 | ``` 236 | 这里要简单说一下,如下代码的作用相同,只不过query会返回一个DataReader类,里面有更灵活的pdo操作,有兴趣的同学可以追到里面去看一下 237 | ``` 238 | Yii::$app->get("db")->createCommand("select * from a")->queryAll(); 239 | Yii::$app->get("db")->createCommand("select * from a")->query()->readAll(); 240 | ``` 241 | 游标的执行代码 242 | ``` 243 | public function read() 244 | { 245 | return $this->_statement->fetch(); 246 | } 247 | ``` 248 | 可见DataReader类获取了游标的数据后会放到rows属性里面 249 | ``` 250 | $rows = []; 251 | $count = 0; 252 | while ($count++ < $this->batchSize && ($row = $this->_dataReader->read())) { 253 | $rows[] = $row; 254 | } 255 | ``` 256 | 每一次foreach的内部遍历其实就是操作获取的游标数据 257 | ``` 258 | public function key() 259 | { 260 | return $this->_key; 261 | } 262 | 263 | public function current() 264 | { 265 | return $this->_value; 266 | } 267 | 268 | public function valid() 269 | { 270 | return !empty($this->_batch); 271 | } 272 | ``` 273 | 最后BatchQueryResult类是有析构方法的 274 | ``` 275 | public function __destruct() 276 | { 277 | // make sure cursor is closed 278 | $this->reset(); 279 | } 280 | ``` 281 | 其实就是执行了pdo的closeCursor操作 282 | -------------------------------------------------------------------------------- /yii2/[模型层一]Model源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [Model类结构](#Model类架构) 3 | * [创建Model](#创建Model) 4 | * [属性](#属性) 5 | * [场景](#场景) 6 | * [验证](#验证) 7 | 8 | # Model类架构 9 | Model类源码在verdor/yiisoft/yii2/base/Model.php,该类和传统框架的模型层不一样,比如CI3的模型层Model是专门和数据库交互的,而Yii2的Model是用来处理业务数据、逻辑、验证、获取验证错误信息的 10 | 个人感觉yii2的Model层比较难理解,注释中作者写道 11 | ``` 12 | Model is the base class for data models 13 | You may directly use Model to store model data, or extend it with customization 14 | ``` 15 | 单纯的Model类无法和数据库交互,需要配合活动记录ActiveRecord 16 | ``` 17 | class Model extends Component implements StaticInstanceInterface, IteratorAggregate, ArrayAccess, Arrayable 18 | { 19 | use ArrayableTrait; //多继承trait 20 | use StaticInstanceTrait; //多继承trait 21 | const SCENARIO_DEFAULT = 'default'; //默认场景default 22 | const EVENT_BEFORE_VALIDATE = 'beforeValidate'; //开始验证事件 23 | const EVENT_AFTER_VALIDATE = 'afterValidate'; //结束验证事件 24 | private $_errors; //验证错误的错误信息 25 | private $_validators; //验证类 26 | private $_scenario = self::SCENARIO_DEFAULT; //场景 27 | ``` 28 | # 创建Model 29 | 可以在控制器里面直接new一个Model 30 | ``` 31 | use app\models\User; 32 | class TestController extends Controller{ 33 | public function actionModel(){ 34 | $user = new User(); 35 | } 36 | } 37 | ``` 38 | 也可以用静态方法实例化 39 | ``` 40 | use app\models\User; 41 | class TestController extends Controller{ 42 | public function actionModel(){ 43 | $user1 = User:instance(); 44 | $user2 = User:instance(true); 45 | } 46 | } 47 | ``` 48 | 因为base\Model使用了StaticInstanceTrait,StaticInstanceTrait是一个trait,里面只有一个静态方法instance 49 | 参数refresh为false可以防止多次调用造成的多次实例化,同时true可以强制重新实例化 50 | ``` 51 | trait StaticInstanceTrait 52 | { 53 | private static $_instances = []; 54 | public static function instance($refresh = false) 55 | { 56 | $className = get_called_class(); 57 | if ($refresh || !isset(self::$_instances[$className])) { 58 | self::$_instances[$className] = Yii::createObject($className); 59 | } 60 | return self::$_instances[$className]; 61 | } 62 | } 63 | ``` 64 | 由于是trait,所以可以在自己的Model里面重写instance方法 65 | ``` 66 | class User extends Model 67 | { 68 | public static function instance(){ 69 | echo "instance rewrite"; 70 | } 71 | } 72 | ``` 73 | 可以使用formName方法来获取去除命名空间的类名,formName方法是base\Model的底层方法,在需要记录日志的情况下非常有用 74 | ``` 75 | public function formName() 76 | { 77 | //反射本类 78 | $reflector = new ReflectionClass($this); 79 | //php版本在7.0.0以上并且不是匿名类 80 | if (PHP_VERSION_ID >= 70000 && $reflector->isAnonymous()) { 81 | throw new InvalidConfigException('The "formName()" method should be explicitly defined for anonymous models'); 82 | } 83 | //获取shortName 84 | return $reflector->getShortName(); 85 | } 86 | ``` 87 | # 属性 88 | 模型通过属性来代表业务数据,属性在Model里面需要定义成非静态共有方法 89 | 涉及属性的方法操作非常多,而且还有一个trait ArrayableTrait来多继承增加操作属性的方法 90 | ``` 91 | public function attributes() //获取全部所有非静态共有属性 92 | public function attributeLabels() //获取全部属性标签 93 | public function attributeHints() //获取全部属性hint 94 | public function isAttributeRequired($attribute) //判断$attribute是否是必填 95 | public function safeAttributes() //获取该场景下的所有safe属性,safe属性就是rule中定义的非!规则属性 96 | public function isAttributeActive($attribute) //判断$attribute是否在该场景下有校验规则 97 | public function getAttributeLabel($attribute) //获取$attribute的属性标签 98 | public function generateAttributeLabel($name) //获取$name的generate属性标签,就是mb_ucwords($name) 99 | public function getAttributes($names = null, $except = []) //获取属性,并且排除except数组里面的属性 100 | public function setAttributes($values, $safeOnly = true) //设置属性值,如果safeOnly为true则只能设置在该场景下rule中的属性 101 | public function fields() //获取属性数组 102 | public function getIterator() //属性迭代器,为IteratorAggregate接口需要继承的方法 103 | ``` 104 | attributes方法,获取全部所有非静态共有属性 105 | 其实php的get_class_vars方法也是获取全部共有属性,但是get_class_vars也获取的静态共有属性 106 | ``` 107 | public function attributes() 108 | { 109 | //反射该类 110 | $class = new ReflectionClass($this); 111 | $names = []; 112 | //获取全部共有属性 113 | foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { 114 | if (!$property->isStatic()) { 115 | //排除静态属性 116 | $names[] = $property->getName(); 117 | } 118 | } 119 | 120 | return $names; 121 | } 122 | ``` 123 | getAttributes方法感觉比较鸡肋,源码比较简单 124 | ``` 125 | public function getAttributes($names = null, $except = []) 126 | { 127 | $values = []; 128 | if ($names === null) { 129 | $names = $this->attributes(); //获取全部非静态共有属性 130 | } 131 | foreach ($names as $name) { 132 | $values[$name] = $this->$name; 133 | } 134 | foreach ($except as $name) { //排除属性 135 | unset($values[$name]); 136 | } 137 | 138 | return $values; 139 | } 140 | ``` 141 | fields也是获取全部非静态共有属性,只不过返回结构不同 142 | ``` 143 | public function fields() 144 | { 145 | $fields = $this->attributes(); 146 | 147 | return array_combine($fields, $fields); 148 | } 149 | ``` 150 | base\Model有trait ArrayableTrait,ArrayableTrait里面也有一个fields,区别就是Model的fields不会获取非静态共有属性 151 | ``` 152 | public function fields() 153 | { 154 | $fields = array_keys(Yii::getObjectVars($this)); 155 | return array_combine($fields, $fields); 156 | } 157 | ``` 158 | attributeLabels和attributeHints方法是获取全部属性标签和属性hint,需要自己去重写这两个方法 159 | hint其实没理解到底应该怎么用,其实我感觉直接使用标签就够了 160 | ``` 161 | public function attributeLabels() 162 | { 163 | return []; 164 | } 165 | public function attributeHints() 166 | { 167 | return []; 168 | } 169 | public function getAttributeLabel($attribute) //获取$attribute属性标签 170 | { 171 | $labels = $this->attributeLabels(); 172 | return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute); 173 | } 174 | public function getAttributeHint($attribute) //获取属性hints 175 | { 176 | $hints = $this->attributeHints(); 177 | return isset($hints[$attribute]) ? $hints[$attribute] : ''; 178 | } 179 | public function generateAttributeLabel($name) 180 | { 181 | return Inflector::camel2words($name, true); //其实就是mb_ucwords 182 | } 183 | ``` 184 | 重写 185 | ``` 186 | class User extends Model 187 | { 188 | public function attributeLabels() 189 | { 190 | return [ 191 | "name"=>"a name", //name被注册为一个属性标签 192 | ]; 193 | } 194 | 195 | public function attributeHints() 196 | { 197 | return [ 198 | "age"=>"x age", //age被注册为一个属性hint 199 | ]; 200 | } 201 | } 202 | ``` 203 | isAttributeRequired方法会判断参数$attribute是否需要被RequiredValidator类校验 204 | ``` 205 | public function isAttributeRequired($attribute) 206 | { 207 | //获取$attribute在该场景下的全部校验类 208 | foreach ($this->getActiveValidators($attribute) as $validator) { 209 | if ($validator instanceof RequiredValidator && $validator->when === null) { 210 | return true; 211 | } 212 | } 213 | 214 | return false; 215 | } 216 | ``` 217 | 获取safe属性 218 | ``` 219 | public function safeAttributes() 220 | { 221 | //获取本场景 222 | $scenario = $this->getScenario(); 223 | //获取场景与属性的对应关系 224 | $scenarios = $this->scenarios(); 225 | if (!isset($scenarios[$scenario])) { 226 | return []; 227 | } 228 | $attributes = []; 229 | foreach ($scenarios[$scenario] as $attribute) { 230 | if ($attribute[0] !== '!' && !in_array('!' . $attribute, $scenarios[$scenario])) { 231 | $attributes[] = $attribute; 232 | } 233 | } 234 | return $attributes; 235 | } 236 | ``` 237 | 关于safe属性,如果有如下配置,场景为default,那么safe属性就是name和sex 238 | ``` 239 | class User extends Model{ 240 | public function rules() 241 | { 242 | return [ 243 | [['name'], 'required', 'on' => 'default'], 244 | [['sex'], 'boolean', 'on' => ['default','login']], 245 | [['!height'], 'boolean', 'except'=>"unregister"], 246 | ]; 247 | } 248 | } 249 | ``` 250 | 还有一个属性是active属性,相对于safe属性,就是name、sex、height 251 | ``` 252 | public function activeAttributes() 253 | { 254 | $scenario = $this->getScenario(); 255 | $scenarios = $this->scenarios(); 256 | if (!isset($scenarios[$scenario])) { 257 | return []; 258 | } 259 | $attributes = array_keys(array_flip($scenarios[$scenario])); 260 | foreach ($attributes as $i => $attribute) { 261 | if ($attribute[0] === '!') { 262 | $attributes[$i] = substr($attribute, 1); 263 | } 264 | } 265 | return $attributes; 266 | } 267 | ``` 268 | 给属性赋值的操作是setAttributes 269 | ``` 270 | public function setAttributes($values, $safeOnly = true) 271 | { 272 | if (is_array($values)) { 273 | //判断是获取safe属性还是直接获取类的非静态共有方法 274 | $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes()); 275 | foreach ($values as $name => $value) { 276 | if (isset($attributes[$name])) { 277 | //给类属性赋值 278 | $this->$name = $value; 279 | } elseif ($safeOnly) { 280 | //对不安全的属性记录日志 281 | $this->onUnsafeAttribute($name, $value); 282 | } 283 | } 284 | } 285 | } 286 | //对不安全的属性记录日志,只在debug模式下会记录日志 287 | public function onUnsafeAttribute($name, $value) 288 | { 289 | if (YII_DEBUG) { 290 | Yii::debug("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); 291 | } 292 | } 293 | ``` 294 | setAttributes也叫块赋值,只用一行代码将用户所有输入填充到一个模型, 以下两段代码效果是相同的, 都是将终端用户输入的表单数据赋值到Model 295 | ``` 296 | $model = new \app\models\ContactForm; 297 | $model->attributes = \Yii::$app->request->post('ContactForm'); 298 | ``` 299 | ``` 300 | $model = new \app\models\ContactForm; 301 | $data = \Yii::$app->request->post('ContactForm', []); 302 | $model->name = isset($data['name']) ? $data['name'] : null; 303 | $model->email = isset($data['email']) ? $data['email'] : null; 304 | $model->subject = isset($data['subject']) ? $data['subject'] : null; 305 | $model->body = isset($data['body']) ? $data['body'] : null; 306 | ``` 307 | 或者也可以使用load、loadMultiple给属性赋值 308 | ``` 309 | public function load($data, $formName = null) 310 | { 311 | $scope = $formName === null ? $this->formName() : $formName; 312 | if ($scope === '' && !empty($data)) { 313 | $this->setAttributes($data); 314 | 315 | return true; 316 | } elseif (isset($data[$scope])) { 317 | $this->setAttributes($data[$scope]); 318 | 319 | return true; 320 | } 321 | 322 | return false; 323 | } 324 | public static function loadMultiple($models, $data, $formName = null) 325 | { 326 | if ($formName === null) { 327 | /* @var $first Model|false */ 328 | $first = reset($models); 329 | if ($first === false) { 330 | return false; 331 | } 332 | $formName = $first->formName(); 333 | } 334 | 335 | $success = false; 336 | foreach ($models as $i => $model) { 337 | /* @var $model Model */ 338 | if ($formName == '') { 339 | if (!empty($data[$i]) && $model->load($data[$i], '')) { 340 | $success = true; 341 | } 342 | } elseif (!empty($data[$formName][$i]) && $model->load($data[$formName][$i], '')) { 343 | $success = true; 344 | } 345 | } 346 | 347 | return $success; 348 | } 349 | ``` 350 | # 场景 351 | 一个Model可以用在多个场景下,比如User模型用于登录和注册场景,不同的场景会有不同的校验规则和属性 352 | 默认Model场景为default 353 | ``` 354 | const SCENARIO_DEFAULT = 'default'; 355 | private $_scenario = self::SCENARIO_DEFAULT; 356 | ``` 357 | 因为Model继承于Component,所以可以使用属性注入,控制器代码如下 358 | ``` 359 | public function actionUser(){ 360 | $user_model = User::instance(); 361 | $user_model -> scenario = "login"; 362 | return $user_model->scenario; 363 | } 364 | ``` 365 | 会调用Model底层的getScenario和setScenario 366 | ``` 367 | public function getScenario() 368 | { 369 | return $this->_scenario; 370 | } 371 | public function setScenario($value) 372 | { 373 | $this->_scenario = $value; 374 | } 375 | ``` 376 | 也可以在实例化的时候直接给场景赋值,底层走的是BaseYii::configure,然后走属性注入的魔术方法 377 | ``` 378 | public function actionUser(){ 379 | $user_model = new User(["scenario"=>"login"]); 380 | } 381 | ``` 382 | # 验证 383 | 验证需要申明验证规则,base\Model的rules方法返回的是空数组 384 | ``` 385 | public function rules() 386 | { 387 | return []; 388 | } 389 | ``` 390 | 所以需要重写这个方法 391 | ``` 392 | class User extends Model 393 | { 394 | public function rules() 395 | { 396 | return [ 397 | [['name'], 'required'], 398 | [['height','sex'], 'boolean', 'on' => ['default','login']], 399 | [['age'], 'boolean', 'except'=>"unregister"], 400 | ]; 401 | } 402 | } 403 | ``` 404 | rules方法的规则如下 405 | - 如果没有on或者except标识,则说明本条规则在所有场景下适用 406 | - 如果有on标识,则说明本条规则只在on下适用 407 | - 如果有except标识,则说明本条规则除了except下适用 408 | 所以以上User类的规则如下 409 | - defaults场景下验证name、height、sex、age 410 | - login场景下验证name、height、sex、age 411 | - unregister场景下验证name 412 | 获取验证规则可以使用scenarios方法 413 | ``` 414 | public function scenarios() 415 | { 416 | //默认default场景 417 | $scenarios = [self::SCENARIO_DEFAULT => []]; 418 | //获取验证规则类,并且遍历 419 | foreach ($this->getValidators() as $validator) { 420 | //on下的验证规则 421 | foreach ($validator->on as $scenario) { 422 | $scenarios[$scenario] = []; 423 | } 424 | //except下的验证规则 425 | foreach ($validator->except as $scenario) { 426 | $scenarios[$scenario] = []; 427 | } 428 | } 429 | $names = array_keys($scenarios); 430 | //获取验证规则类,并且遍历 431 | foreach ($this->getValidators() as $validator) { 432 | //如果本条验证规则没有on和except,就是所有场景下都适用 433 | if (empty($validator->on) && empty($validator->except)) { 434 | foreach ($names as $name) { 435 | foreach ($validator->attributes as $attribute) { 436 | $scenarios[$name][$attribute] = true; 437 | } 438 | } 439 | //如果本条验证规则只在on场景下适用 440 | } elseif (empty($validator->on)) { 441 | foreach ($names as $name) { 442 | //这个验证规则不在except场景下出现 443 | if (!in_array($name, $validator->except, true)) { 444 | foreach ($validator->attributes as $attribute) { 445 | $scenarios[$name][$attribute] = true; 446 | } 447 | } 448 | } 449 | } else { 450 | foreach ($validator->on as $name) { 451 | foreach ($validator->attributes as $attribute) { 452 | $scenarios[$name][$attribute] = true; 453 | } 454 | } 455 | } 456 | } 457 | foreach ($scenarios as $scenario => $attributes) { 458 | if (!empty($attributes)) { 459 | $scenarios[$scenario] = array_keys($attributes); 460 | } 461 | } 462 | return $scenarios; 463 | } 464 | ``` 465 | 获取验证类逻辑如下 466 | ``` 467 | public function getValidators() 468 | { 469 | if ($this->_validators === null) { 470 | $this->_validators = $this->createValidators(); 471 | } 472 | return $this->_validators; 473 | } 474 | public function createValidators() 475 | { 476 | $validators = new ArrayObject(); 477 | foreach ($this->rules() as $rule) { 478 | if ($rule instanceof Validator) { 479 | $validators->append($rule); 480 | } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type 481 | $validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2)); 482 | $validators->append($validator); 483 | } else { 484 | throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); 485 | } 486 | } 487 | 488 | return $validators; 489 | } 490 | ``` 491 | 涉及到了底层了验证Validator类,验证通过createValidator方法实例化 492 | ``` 493 | public static function createValidator($type, $model, $attributes, $params = []) 494 | { 495 | $params['attributes'] = $attributes; 496 | 497 | if ($type instanceof \Closure || ($model->hasMethod($type) && !isset(static::$builtInValidators[$type]))) { 498 | // method-based validator 499 | $params['class'] = __NAMESPACE__ . '\InlineValidator'; 500 | $params['method'] = $type; 501 | } else { 502 | if (isset(static::$builtInValidators[$type])) { 503 | $type = static::$builtInValidators[$type]; 504 | } 505 | if (is_array($type)) { 506 | $params = array_merge($type, $params); 507 | } else { 508 | $params['class'] = $type; 509 | } 510 | } 511 | return Yii::createObject($params); 512 | } 513 | ``` 514 | 在createValidator里面实例化后还会走init 515 | ``` 516 | public function init() 517 | { 518 | parent::init(); 519 | $this->attributes = (array) $this->attributes; 520 | $this->on = (array) $this->on; 521 | $this->except = (array) $this->except; 522 | } 523 | ``` 524 | 可用的验证规则如下 525 | ``` 526 | public static $builtInValidators = [ 527 | 'boolean' => 'yii\validators\BooleanValidator', 528 | 'captcha' => 'yii\captcha\CaptchaValidator', 529 | 'compare' => 'yii\validators\CompareValidator', 530 | 'date' => 'yii\validators\DateValidator', 531 | 'datetime' => [ 532 | 'class' => 'yii\validators\DateValidator', 533 | 'type' => DateValidator::TYPE_DATETIME, 534 | ], 535 | 'time' => [ 536 | 'class' => 'yii\validators\DateValidator', 537 | 'type' => DateValidator::TYPE_TIME, 538 | ], 539 | 'default' => 'yii\validators\DefaultValueValidator', 540 | 'double' => 'yii\validators\NumberValidator', 541 | 'each' => 'yii\validators\EachValidator', 542 | 'email' => 'yii\validators\EmailValidator', 543 | 'exist' => 'yii\validators\ExistValidator', 544 | 'file' => 'yii\validators\FileValidator', 545 | 'filter' => 'yii\validators\FilterValidator', 546 | 'image' => 'yii\validators\ImageValidator', 547 | 'in' => 'yii\validators\RangeValidator', 548 | 'integer' => [ 549 | 'class' => 'yii\validators\NumberValidator', 550 | 'integerOnly' => true, 551 | ], 552 | 'match' => 'yii\validators\RegularExpressionValidator', 553 | 'number' => 'yii\validators\NumberValidator', 554 | 'required' => 'yii\validators\RequiredValidator', 555 | 'safe' => 'yii\validators\SafeValidator', 556 | 'string' => 'yii\validators\StringValidator', 557 | 'trim' => [ 558 | 'class' => 'yii\validators\FilterValidator', 559 | 'filter' => 'trim', 560 | 'skipOnArray' => true, 561 | ], 562 | 'unique' => 'yii\validators\UniqueValidator', 563 | 'url' => 'yii\validators\UrlValidator', 564 | 'ip' => 'yii\validators\IpValidator', 565 | ]; 566 | ``` 567 | 如果不想用以上的验证规则,可以在自己的类里面新建一个验证规则 568 | ``` 569 | class User extends Model 570 | { 571 | public function rules() 572 | { 573 | return [ 574 | [["name"],"myValidators"] 575 | ]; 576 | } 577 | 578 | public function myValidators($value) 579 | { 580 | if($value != 123){ 581 | return false; 582 | } 583 | return true; 584 | } 585 | } 586 | ``` 587 | 执行验证的方法是validate,就是根据rules和场景来进行Validator类的验证 588 | ``` 589 | public function validate($attributeNames = null, $clearErrors = true) 590 | { 591 | if ($clearErrors) { 592 | //清除所有验证错误信息 593 | $this->clearErrors(); 594 | } 595 | 596 | //验证开始事件 597 | if (!$this->beforeValidate()) { 598 | return false; 599 | } 600 | //获取所有场景下的验证规则 601 | $scenarios = $this->scenarios(); 602 | //获取场景 603 | $scenario = $this->getScenario(); 604 | if (!isset($scenarios[$scenario])) { 605 | throw new InvalidArgumentException("Unknown scenario: $scenario"); 606 | } 607 | //获取本场景下的验证规则 608 | if ($attributeNames === null) { 609 | $attributeNames = $this->activeAttributes(); 610 | } 611 | $attributeNames = (array)$attributeNames; 612 | //获取验证规则对应的验证类,遍历 613 | foreach ($this->getActiveValidators() as $validator) { 614 | $validator->validateAttributes($this, $attributeNames); 615 | } 616 | //验证结束事件 617 | $this->afterValidate(); 618 | //是否有验证错误信息 619 | return !$this->hasErrors(); 620 | } 621 | ``` 622 | 具体的验证在Validator类里面的validateAttributes方法 623 | ``` 624 | public function validateAttributes($model, $attributes = null) 625 | { 626 | //获取被验证类需要验证的属性 627 | $attributes = $this->getValidationAttributes($attributes); 628 | foreach ($attributes as $attribute) { 629 | //在该属性已经有错误信息或者该属性是空的情况下跳过 630 | $skip = $this->skipOnError && $model->hasErrors($attribute) 631 | || $this->skipOnEmpty && $this->isEmpty($model->$attribute); 632 | if (!$skip) { 633 | if ($this->when === null || call_user_func($this->when, $model, $attribute)) { 634 | //执行验证 635 | $this->validateAttribute($model, $attribute); 636 | } 637 | } 638 | } 639 | } 640 | ``` 641 | 判断为空的逻辑如下 642 | ``` 643 | public function isEmpty($value) 644 | { 645 | if ($this->isEmpty !== null) { 646 | return call_user_func($this->isEmpty, $value); 647 | } 648 | 649 | return $value === null || $value === [] || $value === ''; 650 | } 651 | ``` 652 | 如果要在申明规则下不跳过错误并且不跳过空的,可以 653 | ``` 654 | public function rules() 655 | { 656 | return [ 657 | [['name'], 'required'], 658 | //申明height和sex属性不跳过错误并且不跳过空 659 | [['height','sex'], 'boolean', 'on' => ['default','login'],'skipOnEmpty'=>false,"skipOnError"=>false], 660 | ]; 661 | } 662 | ``` 663 | 验证方法为各个验证类的validateValue方法,如果验证失败会执行addError添加验证错误信息 664 | ``` 665 | public function addError($attribute, $error = '') 666 | { 667 | $this->_errors[$attribute][] = $error; 668 | } 669 | ``` 670 | -------------------------------------------------------------------------------- /yii2/[模型层二]ActiveRecord源码.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /yii2/[请求处理一]路由源码.md: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /yii2/[请求处理三]错误处理源码.md: -------------------------------------------------------------------------------- 1 | Yii2使用errorHandler组件来管理异常,errorHandler默认映射为ErrorHandler类 2 | ``` 3 | //yii/vendor/yiisoft/yii2/web/Application.php 4 | public function coreComponents() 5 | { 6 | return array_merge(parent::coreComponents(), [ 7 | 'request' => ['class' => 'yii\web\Request'], 8 | 'response' => ['class' => 'yii\web\Response'], 9 | 'session' => ['class' => 'yii\web\Session'], 10 | 'user' => ['class' => 'yii\web\User'], 11 | 'errorHandler' => ['class' => 'yii\web\ErrorHandler'], 12 | ]); 13 | } 14 | ``` 15 | 在配置文件初始化后,会有专门的方法来注册异常管理类 16 | ``` 17 | //yii/vendor/yiisoft/yii2/base/Application.php 18 | protected function registerErrorHandler(&$config) 19 | { 20 | if (YII_ENABLE_ERROR_HANDLER) { 21 | if (!isset($config['components']['errorHandler']['class'])) { 22 | echo "Error: no errorHandler component is configured.\n"; 23 | exit(1); 24 | } 25 | $this->set('errorHandler', $config['components']['errorHandler']); 26 | unset($config['components']['errorHandler']); 27 | $this->getErrorHandler()->register(); 28 | } 29 | } 30 | ``` 31 | 通过YII_ENABLE_ERROR_HANDLER常量来判断是否使用yii本身的异常管理,这是yii唯一一个有开关选项的默认组件,仔细想想其实是有道理的,因为如果yii强行注册了异常组件,然后用户又想封装一下自己的异常组件,会导致有多个set_error_handler和set_exception_handler,即使使用restore_error_handler和restore_exception_handler也是无法保证会把yii异常组件回收的,因为无法知道已经注册了多少个异常处理层级 32 | ``` 33 | memoryReserveSize > 0) { 61 | $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); 62 | } 63 | register_shutdown_function([$this, 'handleFatalError']); ////注册shutdown函数 64 | } 65 | ``` 66 | 这里值得注意的是会默认占用256KB的内存大小作为memoryReserver,防止错误导致内存不足造成无法记录和显示错误信息 67 | handlerError函数用来捕获非error异常,将捕获到的异常封装为ErrorException类,然后抛出去让handleException函数捕获 68 | ``` 69 | public function handleError($code, $message, $file, $line) 70 | { 71 | if (error_reporting() & $code) { 72 | // load ErrorException manually here because autoloading them will not work 73 | // when error occurs while autoloading a class 74 | if (!class_exists('yii\\base\\ErrorException', false)) { 75 | require_once __DIR__ . '/ErrorException.php'; 76 | } 77 | $exception = new ErrorException($message, $code, $code, $file, $line); 78 | 79 | // in case error appeared in __toString method we can't throw any exception 80 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 81 | array_shift($trace); 82 | foreach ($trace as $frame) { 83 | if ($frame['function'] === '__toString') { 84 | $this->handleException($exception); 85 | if (defined('HHVM_VERSION')) { 86 | flush(); 87 | } 88 | exit(1); 89 | } 90 | } 91 | 92 | throw $exception; 93 | } 94 | 95 | return false; 96 | } 97 | ``` 98 | 这里需要注意的是,如果__toString内的非error异常,是无法再throw的,有兴趣的朋友可以试一下如下代码 99 | ``` 100 | set_error_handler(function(){ 101 | throw new Exception("exception"); 102 | }); 103 | set_exception_handler(function($e){ 104 | var_dump($e); 105 | }); 106 | class obj{ 107 | public function __toString(){ 108 | echo $name; //模拟一个notice异常 109 | return "string"; 110 | } 111 | } 112 | $obj = new obj; 113 | echo $obj; 114 | ``` 115 | yii是通过debug_backtrace函数获取调用栈,来判断是否是因为toString函数造成的异常,我感觉这块非常影响效率 116 | handlerException用来捕获handlerError抛出来的异常,和PHP7中的error异常 117 | ``` 118 | public function handleException($exception) 119 | { 120 | if ($exception instanceof ExitException) { 121 | return; 122 | } 123 | 124 | $this->exception = $exception; 125 | 126 | // disable error capturing to avoid recursive errors while handling exceptions 127 | $this->unregister(); //回收异常函数 128 | 129 | // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent 130 | // HTTP exceptions will override this value in renderException() 131 | if (PHP_SAPI !== 'cli') { 132 | http_response_code(500); //如果不是cli模式,就响应500 133 | } 134 | 135 | try { 136 | $this->logException($exception); //记录日志 137 | if ($this->discardExistingOutput) { 138 | $this->clearOutput(); //清除所有输出层,只输出异常信息 139 | } 140 | $this->renderException($exception); 141 | if (!YII_ENV_TEST) { 142 | \Yii::getLogger()->flush(true); 143 | if (defined('HHVM_VERSION')) { 144 | flush(); 145 | } 146 | exit(1); 147 | } 148 | } catch (\Exception $e) { 149 | // an other exception could be thrown while displaying the exception 150 | $this->handleFallbackExceptionMessage($e, $exception); 151 | } catch (\Throwable $e) { 152 | // additional check for \Throwable introduced in PHP 7 153 | $this->handleFallbackExceptionMessage($e, $exception); 154 | } 155 | 156 | $this->exception = null; 157 | } 158 | ``` 159 | renderException方法是核心 160 | ``` 161 | protected function renderException($exception) 162 | { 163 | if (Yii::$app->has('response')) { //是否已经加载了response组件,在实例化Application过程中会加载 164 | $response = Yii::$app->getResponse(); 165 | // reset parameters of response to avoid interference with partially created response data 166 | // in case the error occurred while sending the response. 167 | $response->isSent = false; 168 | $response->stream = null; 169 | $response->data = null; 170 | $response->content = null; 171 | } else { 172 | $response = new Response(); 173 | } 174 | 175 | $response->setStatusCodeByException($exception); //设置响应信息文字,不是设置httpcode,而是响应header 176 | 177 | $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);//如果响应格式是html并且(不是debug模式或者是UserException异常) 178 | 179 | if ($useErrorView && $this->errorAction !== null) { 180 | $result = Yii::$app->runAction($this->errorAction); //调用errorAction,一般都是调用一个用户响应异常的控制器方法 181 | if ($result instanceof Response) { 182 | $response = $result; 183 | } else { 184 | $response->data = $result; 185 | } 186 | } elseif ($response->format === Response::FORMAT_HTML) { 187 | if ($this->shouldRenderSimpleHtml()) { //如果是开发模式或者是ajax响应 188 | // AJAX request 189 | $response->data = '
' . $this->htmlEncode(static::convertExceptionToString($exception)) . ''; 190 | } else { 191 | // if there is an error during error rendering it's useful to 192 | // display PHP error in debug mode instead of a blank screen 193 | if (YII_DEBUG) { //如果是debug模式就显示带大红框的异常信息 194 | ini_set('display_errors', 1); 195 | } 196 | $file = $useErrorView ? $this->errorView : $this->exceptionView; 197 | $response->data = $this->renderFile($file, [ //加载异常页面 198 | 'exception' => $exception, 199 | ]); 200 | } 201 | } elseif ($response->format === Response::FORMAT_RAW) { 202 | $response->data = static::convertExceptionToString($exception); 203 | } else { 204 | $response->data = $this->convertExceptionToArray($exception); 205 | } 206 | 207 | $response->send(); 208 | } 209 | ``` 210 | 还有一个shutdown函数,因为在PHP5版本中,error级别的异常是无法被set_exception_handler函数捕获的,只能通过注册一个shutdown函数来用error_get_last来获取error异常 211 | ``` 212 | public function handleFatalError() 213 | { 214 | unset($this->_memoryReserve); 215 | 216 | // load ErrorException manually here because autoloading them will not work 217 | // when error occurs while autoloading a class 218 | if (!class_exists('yii\\base\\ErrorException', false)) { 219 | require_once __DIR__ . '/ErrorException.php'; 220 | } 221 | 222 | $error = error_get_last(); 223 | 224 | if (ErrorException::isFatalError($error)) { 225 | if (!empty($this->_hhvmException)) { 226 | $exception = $this->_hhvmException; 227 | } else { 228 | $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); 229 | } 230 | $this->exception = $exception; 231 | 232 | $this->logException($exception); 233 | 234 | if ($this->discardExistingOutput) { 235 | $this->clearOutput(); 236 | } 237 | $this->renderException($exception); 238 | 239 | // need to explicitly flush logs because exit() next will terminate the app immediately 240 | Yii::getLogger()->flush(true); 241 | if (defined('HHVM_VERSION')) { 242 | flush(); 243 | } 244 | exit(1); 245 | } 246 | } 247 | ``` 248 | -------------------------------------------------------------------------------- /yii2/[请求处理二]Session和Cookie源码.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [Session](#Session) 3 | * [Flash数据](#Flash数据) 4 | * [Redis_Session](#Redis_Session) 5 | * [Cookie](#Cookie) 6 | 7 | # Session 8 | Sesssion组件默认在config/web.php是没有配置的,在Application底层代码中会走默认的依赖配置 9 | ``` 10 | //verdor/yiisoft/yii2/base/Application.php 11 | public function preInit(&$config) 12 | { 13 | ... 14 | foreach ($this->coreComponents() as $id => $component) { 15 | if (!isset($config['components'][$id])) { 16 | $config['components'][$id] = $component; 17 | } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) { 18 | $config['components'][$id]['class'] = $component['class']; 19 | } 20 | } 21 | } 22 | 23 | public function coreComponents() 24 | { 25 | return [ 26 | 'log' => ['class' => 'yii\log\Dispatcher'], 27 | 'view' => ['class' => 'yii\web\View'], 28 | 'formatter' => ['class' => 'yii\i18n\Formatter'], 29 | 'i18n' => ['class' => 'yii\i18n\I18N'], 30 | 'mailer' => ['class' => 'yii\swiftmailer\Mailer'], 31 | 'urlManager' => ['class' => 'yii\web\UrlManager'], 32 | 'assetManager' => ['class' => 'yii\web\AssetManager'], 33 | 'security' => ['class' => 'yii\base\Security'], 34 | ]; 35 | } 36 | 37 | //verdor/yiisoft/yii2/web/Application.php 38 | public function coreComponents() 39 | { 40 | return array_merge(parent::coreComponents(), [ 41 | 'request' => ['class' => 'yii\web\Request'], 42 | 'response' => ['class' => 'yii\web\Response'], 43 | 'session' => ['class' => 'yii\web\Session'], 44 | 'user' => ['class' => 'yii\web\User'], 45 | 'errorHandler' => ['class' => 'yii\web\ErrorHandler'], 46 | ]); 47 | } 48 | ```` 49 | 但是你也可以在web/config.php中配置Session组件 50 | ``` 51 | $config = [ 52 | 'id' => 'basic', 53 | 'basePath' => dirname(__DIR__), 54 | 'bootstrap' => ['log'], 55 | 'aliases' => [ 56 | '@bower' => '@vendor/bower-asset', 57 | '@npm' => '@vendor/npm-asset', 58 | ], 59 | 'components' => [ 60 | 'session' => [ 61 | 'cookieParams' => [ 62 | 'lifetime' => time()+30, 63 | 'httponly' => 'true', 64 | ] 65 | ] 66 | ... 67 | ``` 68 | Session组件源码比较简单,就是基本的session操作 69 | ``` 70 | $session = Yii::$app->session; 71 | $session->set("a","b"); 72 | $session["b"] = "c"; 73 | $session["arr"] = [ 74 | "name" => 123, 75 | "age" => 222 76 | ]; 77 | var_dump($session->has("a")); 78 | var_dump($session->get("b")); 79 | var_dump($_SESSION); 80 | ``` 81 | 实例化Session组件会走到init 82 | ``` 83 | public function init() 84 | { 85 | parent::init(); 86 | //注册一个session关闭的函数,在应用执行结束时候调用 87 | register_shutdown_function([$this, 'close']); 88 | if ($this->getIsActive()) {//如果session已经session_start了,就去判断flash性质的session属性是否可用 89 | Yii::warning('Session is already started', __METHOD__); 90 | $this->updateFlashCounters(); 91 | } 92 | } 93 | ``` 94 | session的获取和设置源码如下 95 | ``` 96 | public function set($key, $value) 97 | { 98 | $this->open(); 99 | $_SESSION[$key] = $value; 100 | } 101 | public function get($key, $defaultValue = null) 102 | { 103 | $this->open(); 104 | return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; 105 | } 106 | public function remove($key) 107 | { 108 | $this->open(); 109 | if (isset($_SESSION[$key])) { 110 | $value = $_SESSION[$key]; 111 | unset($_SESSION[$key]); 112 | 113 | return $value; 114 | } 115 | 116 | return null; 117 | } 118 | public function has($key) 119 | { 120 | $this->open(); 121 | return isset($_SESSION[$key]); 122 | } 123 | ``` 124 | 可见不需要手动调用session_start,在open方法中有session_start操作 125 | ``` 126 | public function open() 127 | { 128 | if ($this->getIsActive()) { 129 | return; 130 | } 131 | //注册自定义的session handler 132 | $this->registerSessionHandler(); 133 | //设置与session相关的cookie属性 134 | $this->setCookieParamsInternal(); 135 | //session开启 136 | YII_DEBUG ? session_start() : @session_start(); 137 | 138 | if ($this->getIsActive()) { 139 | Yii::info('Session started', __METHOD__); 140 | //如果开启成功,则走flash判断逻辑 141 | $this->updateFlashCounters(); 142 | } else { 143 | $error = error_get_last(); 144 | $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; 145 | Yii::error($message, __METHOD__); 146 | } 147 | } 148 | public function getIsActive() 149 | { 150 | return session_status() === PHP_SESSION_ACTIVE; 151 | } 152 | ``` 153 | 设置与session相关的cookie属性源码如下 154 | ``` 155 | private function setCookieParamsInternal() 156 | { 157 | $data = $this->getCookieParams(); 158 | if (isset($data['lifetime'], $data['path'], $data['domain'], $data['secure'], $data['httponly'])) { 159 | session_set_cookie_params($data['lifetime'], $data['path'], $data['domain'], $data['secure'], $data['httponly']); 160 | } else { 161 | throw new InvalidArgumentException('Please make sure cookieParams contains these elements: lifetime, path, domain, secure and httponly.'); 162 | } 163 | } 164 | ``` 165 | 基本的session增删改查操作源码非常简单,因为Session组件实现了IteratorAggregate、ArrayAccess、Countable接口,所以可以进行数组式访问、迭代和count操作 166 | ``` 167 | public function offsetSet($offset, $item) 168 | { 169 | $this->open(); 170 | $_SESSION[$offset] = $item; 171 | } 172 | public function offsetGet($offset) 173 | { 174 | $this->open(); 175 | 176 | return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; 177 | } 178 | public function getCount() 179 | { 180 | $this->open(); 181 | return count($_SESSION); 182 | } 183 | public function getIterator() 184 | { 185 | $this->open(); 186 | //返回的是一个自己封装的迭代器 187 | return new SessionIterator(); 188 | } 189 | ``` 190 | # Flash数据 191 | Session组件封装了一个Flash数据,就是设置了之后只在下一次访问有效 192 | ``` 193 | $session = Yii::$app->session; 194 | $session->setFlash("a",123); //在下一次访问$session->getFlash("a")后失效 195 | $session->setFlash("b",123,false); //在下一次调用实例化Session组件后失效 196 | ``` 197 | Flash数据使用第三个参数来控制如何失效,默认是下一次使用getFlash获取后失效 198 | 如果设置第三个参数为false,则在下一次实例化Session组件后失效 199 | ``` 200 | public function setFlash($key, $value = true, $removeAfterAccess = true) 201 | { 202 | $counters = $this->get($this->flashParam, []); 203 | //控制数据如何失效 204 | $counters[$key] = $removeAfterAccess ? -1 : 0; 205 | $_SESSION[$key] = $value; 206 | $_SESSION[$this->flashParam] = $counters; 207 | } 208 | ``` 209 | 获取Flash数据和初始化Session组件会控制Flash数据是否失效 210 | ``` 211 | public function getFlash($key, $defaultValue = null, $delete = false) 212 | { 213 | $counters = $this->get($this->flashParam, []); 214 | if (isset($counters[$key])) { 215 | $value = $this->get($key, $defaultValue); 216 | if ($delete) { 217 | $this->removeFlash($key); 218 | } elseif ($counters[$key] < 0) { 219 | // mark for deletion in the next request 220 | $counters[$key] = 1; 221 | $_SESSION[$this->flashParam] = $counters; 222 | } 223 | 224 | return $value; 225 | } 226 | 227 | return $defaultValue; 228 | } 229 | protected function updateFlashCounters() 230 | { 231 | $counters = $this->get($this->flashParam, []); 232 | if (is_array($counters)) { 233 | foreach ($counters as $key => $count) { 234 | if ($count > 0) { 235 | unset($counters[$key], $_SESSION[$key]); 236 | } elseif ($count == 0) { 237 | $counters[$key]++; 238 | } 239 | } 240 | $_SESSION[$this->flashParam] = $counters; 241 | } else { 242 | // fix the unexpected problem that flashParam doesn't return an array 243 | unset($_SESSION[$this->flashParam]); 244 | } 245 | } 246 | ``` 247 | # Redis_Session 248 | 在多服务器情况下需要Session共享,可以将session存到redis里面,需要Yii2安装redis扩展 249 | ``` 250 | $config = [ 251 | 'id' => 'basic', 252 | 'basePath' => dirname(__DIR__), 253 | 'bootstrap' => ['log'], 254 | 'aliases' => [ 255 | '@bower' => '@vendor/bower-asset', 256 | '@npm' => '@vendor/npm-asset', 257 | ], 258 | 'components' => [ 259 | 'session_redis' => [ 260 | 'class' => 'yii\redis\Connection', 261 | 'hostname' => '192.168.124.10', 262 | 'port' => 6380, 263 | 'database' => 0, 264 | ], 265 | 'session'=>[ 266 | 'class'=>'yii\redis\Session', 267 | 'redis'=>'session_redis' 268 | ] 269 | ..... 270 | ``` 271 | 底层其实使用的是session_set_save_handler来做读写关闭操作 272 | ``` 273 | //yii2-redis/src/Session.php 274 | public function getUseCustomStorage() 275 | { 276 | return true; 277 | } 278 | //yii2/web/Session.php 279 | protected function registerSessionHandler() 280 | { 281 | if ($this->handler !== null) { 282 | if (!is_object($this->handler)) { 283 | $this->handler = Yii::createObject($this->handler); 284 | } 285 | if (!$this->handler instanceof \SessionHandlerInterface) { 286 | throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.'); 287 | } 288 | YII_DEBUG ? session_set_save_handler($this->handler, false) : @session_set_save_handler($this->handler, false); 289 | } elseif ($this->getUseCustomStorage()) { 290 | if (YII_DEBUG) { 291 | session_set_save_handler( 292 | [$this, 'openSession'], 293 | [$this, 'closeSession'], 294 | [$this, 'readSession'], 295 | [$this, 'writeSession'], 296 | [$this, 'destroySession'], 297 | [$this, 'gcSession'] 298 | ); 299 | } else { 300 | @session_set_save_handler( 301 | [$this, 'openSession'], 302 | [$this, 'closeSession'], 303 | [$this, 'readSession'], 304 | [$this, 'writeSession'], 305 | [$this, 'destroySession'], 306 | [$this, 'gcSession'] 307 | ); 308 | } 309 | } 310 | } 311 | ``` 312 | 在调用session_start时候,会调用openSession和readSession函数,在写session时候会调用writeSession函数 313 | ``` 314 | protected function calculateKey($id) 315 | { 316 | return $this->keyPrefix . md5(json_encode([__CLASS__, $id])); 317 | } 318 | public function readSession($id) 319 | { 320 | $data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]); 321 | 322 | return $data === false || $data === null ? '' : $data; 323 | } 324 | 325 | public function writeSession($id, $data) 326 | { 327 | return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]); 328 | } 329 | ``` 330 | 需要注意的是,使用redis-session组件,写session在redis里面的最大生存周期是ini_get('session.gc_maxlifetime') 331 | ``` 332 | public function getTimeout() 333 | { 334 | return (int) ini_get('session.gc_maxlifetime'); 335 | } 336 | ``` 337 | # Cookie 338 | Yii的Cookie组件分为请求Cookie和响应Cookie 339 | ``` 340 | //获取请求的cookie 341 | $request_cookies = Yii::$app->request->cookies; 342 | //响应的cookie 343 | $response_cookies = Yii::$app->response->cookies; 344 | ``` 345 | 因为cookie有加密操作,所以先从发送一个cookie开始说起 346 | ``` 347 | $response_cookies = Yii::$app->response->cookies; 348 | $response_cookies->add(new \yii\web\Cookie([ 349 | 'name' => 'language', 350 | 'value' => 'zh-CN', 351 | ])); 352 | ``` 353 | 响应的cookie会从Response组件中获取 354 | ``` 355 | //yiisoft/yii2/web/Response.php 356 | public function getCookies() 357 | { 358 | if ($this->_cookies === null) { 359 | $this->_cookies = new CookieCollection(); 360 | } 361 | 362 | return $this->_cookies; 363 | } 364 | ``` 365 | add操作会实例化一个Cookie类 366 | ``` 367 | class Cookie extends \yii\base\BaseObject 368 | { 369 | public $name; 370 | 371 | public $value = ''; 372 | 373 | public $domain = ''; 374 | 375 | public $expire = 0; 376 | 377 | public $path = '/'; 378 | 379 | public $secure = false; 380 | 381 | public $httpOnly = true; 382 | 383 | public function __toString() 384 | { 385 | return (string) $this->value; 386 | } 387 | } 388 | ``` 389 | 可见默认的httponly为true,secure为false等 390 | add操作就是将Cookie类添加进CookieCollection 391 | ``` 392 | public function add($cookie) 393 | { 394 | if ($this->readOnly) { 395 | throw new InvalidCallException('The cookie collection is read only.'); 396 | } 397 | $this->_cookies[$cookie->name] = $cookie; 398 | } 399 | ``` 400 | 添加了一个cookie后,也可以删除它,因为Response组件的Cookie的readOnly属性是false 401 | ``` 402 | public function remove($cookie, $removeFromBrowser = true) 403 | { 404 | if ($this->readOnly) { 405 | throw new InvalidCallException('The cookie collection is read only.'); 406 | } 407 | if ($cookie instanceof Cookie) { 408 | $cookie->expire = 1; //过期时间为1,就是删除 409 | $cookie->value = ''; 410 | } else { 411 | $cookie = Yii::createObject([ 412 | 'class' => 'yii\web\Cookie', 413 | 'name' => $cookie, 414 | 'expire' => 1, 415 | ]); 416 | } 417 | if ($removeFromBrowser) { 418 | $this->_cookies[$cookie->name] = $cookie; 419 | } else { 420 | unset($this->_cookies[$cookie->name]); 421 | } 422 | } 423 | ``` 424 | 最后响应的时候,会走到Response组件的sendCookies,会对cookie进行加密操作 425 | ``` 426 | protected function sendCookies() 427 | { 428 | if ($this->_cookies === null) { 429 | return; 430 | } 431 | $request = Yii::$app->getRequest(); 432 | if ($request->enableCookieValidation) { 433 | if ($request->cookieValidationKey == '') { 434 | throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.'); 435 | } 436 | $validationKey = $request->cookieValidationKey; 437 | } 438 | foreach ($this->getCookies() as $cookie) { 439 | $value = $cookie->value; 440 | if ($cookie->expire != 1 && isset($validationKey)) { 441 | //加密 442 | $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey); 443 | } 444 | setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); 445 | } 446 | } 447 | ``` 448 | 加密的方式默认是sha256 449 | ``` 450 | public function hashData($data, $key, $rawHash = false) 451 | { 452 | //$this->macHash默认是sha256 453 | $hash = hash_hmac($this->macHash, $data, $key, $rawHash); 454 | if (!$hash) { 455 | throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash); 456 | } 457 | 458 | return $hash . $data; 459 | } 460 | ``` 461 | 也就是说会用配置文件中的cookieValidationKey,去对serialize([$cookie->name, $value])做sha256加密,然后拼上data,返回的结果是响应的cookie值 462 | 如果要获取一个请求的cookie,需要从Request组件中获取 463 | ``` 464 | $cookies = Yii::$app->request->cookies; 465 | $cookies -> get("a"); 466 | $cookies -> getValue("b","b value"); 467 | ``` 468 | 相应的源码为 469 | ``` 470 | public function getCookies() 471 | { 472 | if ($this->_cookies === null) { 473 | $this->_cookies = new CookieCollection($this->loadCookies(), [ 474 | 'readOnly' => true, 475 | ]); 476 | } 477 | 478 | return $this->_cookies; 479 | } 480 | protected function loadCookies() 481 | { 482 | $cookies = []; 483 | if ($this->enableCookieValidation) { 484 | if ($this->cookieValidationKey == '') { 485 | throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.'); 486 | } 487 | foreach ($_COOKIE as $name => $value) { 488 | if (!is_string($value)) { 489 | continue; 490 | } 491 | //对请求的cookie做加密验证 492 | $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey); 493 | if ($data === false) { 494 | continue; 495 | } 496 | if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) { 497 | $data = @unserialize($data, ['allowed_classes' => false]); 498 | } else { 499 | $data = @unserialize($data); 500 | } 501 | if (is_array($data) && isset($data[0], $data[1]) && $data[0] === $name) { 502 | $cookies[$name] = Yii::createObject([ 503 | 'class' => 'yii\web\Cookie', 504 | 'name' => $name, 505 | 'value' => $data[1], 506 | 'expire' => null, 507 | ]); 508 | } 509 | } 510 | } else { 511 | foreach ($_COOKIE as $name => $value) { 512 | $cookies[$name] = Yii::createObject([ 513 | 'class' => 'yii\web\Cookie', 514 | 'name' => $name, 515 | 'value' => $value, 516 | 'expire' => null, 517 | ]); 518 | } 519 | } 520 | 521 | return $cookies; 522 | } 523 | ``` 524 | 可以请求的cookie需要做加密校验的,逻辑为 525 | ``` 526 | public function validateData($data, $key, $rawHash = false) 527 | { 528 | $test = @hash_hmac($this->macHash, '', '', $rawHash); 529 | if (!$test) { 530 | throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash); 531 | } 532 | $hashLength = StringHelper::byteLength($test); 533 | if (StringHelper::byteLength($data) >= $hashLength) { 534 | $hash = StringHelper::byteSubstr($data, 0, $hashLength); 535 | 536 | $pureData = StringHelper::byteSubstr($data, $hashLength, null); 537 | 538 | $calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash); 539 | 540 | if ($this->compareString($hash, $calculatedHash)) { 541 | return $pureData; 542 | } 543 | } 544 | 545 | return false; 546 | } 547 | ``` 548 | cookie的加密逻辑为 549 | - 如果cookie的键是name,值是yii,cookieValidationKey为abcd 550 | - 做$value = hash_hmac("sha256", serialize(["name","yii"]), "abcd", false); 551 | - 实际响应的为setcookie("name",$value.serialize(["name","yii"])) 552 | 553 | cookie的加密验证逻辑为 554 | - 如果cookie的键是name,值是yii,cookieValidationKey为abcd 555 | - 获取$test = hash_hmac("sha256", '', '', false); 556 | - 获取$test的长度 557 | - 截取请求cookie值的0-len($test),$hash = StringHelper::byteSubstr($data, 0, $hashLength); 558 | - 截取出serialize的值StringHelper::byteSubstr($data, $hashLength, null); 559 | - 将serialize的值加密,然后对比是否与$hash相同 560 | --------------------------------------------------------------------------------