├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── examples ├── client.php ├── proxy.php └── server.php ├── protocols └── MySQL.php └── src ├── Constants ├── Capabilities.php ├── EnumTraits.php ├── Errors.php └── ExceptionCode.php ├── Exceptions ├── Exception.php ├── InvalidArgumentException.php └── PacketException.php ├── Packets ├── AuthMoreDataRequest.php ├── AuthMoreDataResponse.php ├── AuthSwitchRequest.php ├── AuthSwitchResponse.php ├── Command.php ├── EOF.php ├── Error.php ├── Field.php ├── HandshakeInitialization.php ├── HandshakeResponse.php ├── Ok.php ├── PacketInterface.php ├── ResultSetHeader.php └── RowData.php └── Utils ├── Binary.php ├── Charset.php └── Packet.php /.gitignore: -------------------------------------------------------------------------------- 1 | /runtime 2 | /.idea 3 | /.vscode 4 | /vendor 5 | *.log 6 | *.pid 7 | .env 8 | composer.lock 9 | /tests/tmp 10 | /tests/.phpunit.result.cache 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 workbunny [https://github.com/workbunny] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
workbunny/mysql-protocol
** 4 | 5 | **🐇 A PHP implementation of MySQL Protocol. 🐇
** 6 | 7 | # A PHP implementation of MySQL Protocol 8 | 9 | ## 安装 10 | 11 | ### 依赖 12 | 13 | - PHP >= 8.1 14 | - [workerman](https://github.com/walkor/workerman) >= 4.0 【可选,`workerman`环境】 15 | 16 | ### 安装 17 | 18 | ```shel 19 | composer require workbunny/mysql-protocol 20 | ``` 21 | 22 | ## 使用 23 | 24 | ### Binary 二进制流 25 | 26 | - `Binary`提供了二进制流和字节组之间的互转能力(注:PHP是二进制安全语言) 27 | - `Binary`提供了基础的字节组读写操作能力,读写操作的指针相互隔离,读写指针默认从0位开始 28 | - `payload`支持传递`字符串`、`字节数组`、`iterable类型的字节组`、`null` 29 | 30 | ```php 31 | use Workbunny\MysqlProtocol\Utils\Binary; 32 | 33 | $binary = new Binary("workbunny"); 34 | # 输出字节组 35 | $binary->unpack(); 36 | # 输出字符串(输入明文则返回明文,输入二进制数据则返回二进制) 37 | $binary->pack(); 38 | # 输出原始负载 39 | $binary->payload(); 40 | ``` 41 | 42 | #### 读 43 | 44 | - 默认以0位开始,每次操作都会递增相应字节位置 45 | 46 | ```php 47 | use Workbunny\MysqlProtocol\Utils\Binary; 48 | 49 | $binary = new Binary("workbunny"); 50 | 51 | # 设置读取指针 52 | $binary->setReadCursor(); 53 | # 获取读取指针 54 | $binary->getReadCursor(); 55 | 56 | # 读取一个字节 57 | $binary->readByte(); 58 | # 读取多个字节 59 | $binary->readBytes(); 60 | # 读取一个整数(长度编码) 61 | $binary->readLenEncInt(); 62 | # 读取一个字符串(长度编码) 63 | $binary->readLenEncString(); 64 | # 读取一个无符号整数(长度编码) 65 | $binary->readUB(); 66 | # 读取一个字符串(以NULL结束) 67 | $binary->readNullTerminated(); 68 | ``` 69 | 70 | 71 | #### 写 72 | 73 | - 默认以0位开始,每次操作都会递增相应字节位置 74 | 75 | ```php 76 | use Workbunny\MysqlProtocol\Utils\Binary; 77 | 78 | $binary = new Binary(); 79 | 80 | # 设置写指针 81 | $binary->setWriteCursor(); 82 | # 获取写取指针 83 | $binary->getWriteCursor(); 84 | 85 | # 写一个字节 86 | $binary->writeByte(); 87 | # 写多个字节 88 | $binary->writeBytes(); 89 | # 写一个整数(长度编码) 90 | $binary->writeLenEncInt(); 91 | # 写一个字符串(长度编码) 92 | $binary->writeLenEncString(); 93 | # 写一个无符号整数(长度编码) 94 | $binary->writeUB(); 95 | # 写一个字符串(以NULL结束) 96 | $binary->writeNullTerminated(); 97 | ``` 98 | 99 | ### Packet 协议包 100 | 101 | - `Packet`提供了`MySQL`协议基础的二进制包数据的解析与封装能力 102 | - `Packet`提供`PacketInterface`自定义实现 103 | - 默认13种`Packet`覆盖了常见`MySQL`交互动作 104 | 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workbunny/mysql-protocol", 3 | "type": "library", 4 | "license": "Apache-2.0", 5 | "description": "The MySQL protocol implemented by PHP", 6 | "authors": [ 7 | { 8 | "name": "chaz6chez", 9 | "email": "chaz6chez1993@outlook.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=8.1" 14 | }, 15 | "require-dev": { 16 | "workerman/workerman": "^4.0 | ^5.0", 17 | "symfony/var-dumper": "^6.0 | ^7.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Workbunny\\MysqlProtocol\\": "src", 22 | "Protocols\\" : "protocols" 23 | } 24 | }, 25 | "suggest": { 26 | "workerman/workerman": "workerman implementation supporting the MySQL protocol" 27 | } 28 | } -------------------------------------------------------------------------------- /examples/client.php: -------------------------------------------------------------------------------- 1 | name = 'workbunny-mysql-client'; 18 | $server->onWorkerStart = function () { 19 | global $clientMysqlHandshakeStatus; 20 | $clientMysqlHandshakeStatus = $clientMysqlHandshakeStatus ?: 0; 21 | $client = new \Workerman\Connection\AsyncTcpConnection("MySQL://host.docker.internal:3306"); 22 | 23 | $client->onConnect = function (TcpConnection $connection) use (&$clientMysqlHandshakeStatus) { 24 | $connection->errorHandler = function (Throwable $exception) { 25 | dump($exception); 26 | }; 27 | // 模拟心跳 28 | \Workerman\Timer::add(30, function () use ($connection, &$clientMysqlHandshakeStatus) { 29 | if ($clientMysqlHandshakeStatus > 1) { 30 | $connection->send(Command::pack([ 31 | 'command' => Command::COM_QUERY, 32 | 'data' => 'SELECT 1' 33 | ])); 34 | } 35 | }); 36 | }; 37 | 38 | $client->onMessage = function (TcpConnection $connection, Binary $binary) use (&$clientMysqlHandshakeStatus) { 39 | dump($binary->dump()); 40 | // 还未握手 41 | if ($clientMysqlHandshakeStatus < 1) { 42 | // 解析握手包 43 | $handshakeInitialization = HandshakeInitialization::unpack($binary); 44 | $clientMysqlHandshakeStatus = 1; 45 | $connection->send(HandshakeResponse::pack([ 46 | 'packet_id' => $handshakeInitialization['packet_id'] + 1, 47 | "capability_flags" => $handshakeInitialization['capability_flags'], 48 | "max_packet_size" => 1073741824, 49 | "character_set" => 33, 50 | "username" => "root", 51 | "database" => null, 52 | "auth_plugin" => $handshakeInitialization['auth_plugin_name'], 53 | "auth_response" => "", 54 | ])); 55 | } 56 | // 握手还未确认 57 | elseif ($clientMysqlHandshakeStatus < 2) { 58 | if (!$class = Packet::getPacketClass($binary)) { 59 | echo "Error!\n"; 60 | return; 61 | } 62 | $result = $class::unpack($binary); 63 | dump($result); 64 | // todo判定是否握手成功 65 | 66 | } 67 | // 握手成功 68 | else { 69 | 70 | dump($binary->dump()); 71 | } 72 | }; 73 | 74 | $client->connect(); 75 | }; 76 | 77 | \Workerman\Worker::runAll(); 78 | -------------------------------------------------------------------------------- /examples/proxy.php: -------------------------------------------------------------------------------- 1 | name = 'workbunny-mysql-server'; 13 | $server->count = 2; 14 | $server->onConnect = function (TcpConnection $source) { 15 | // 创建与MySQL-server的连接 16 | $target = new \Workerman\Connection\AsyncTcpConnection("MySQL://host.docker.internal:3306"); 17 | // 管道传输 18 | $target->pipe($source); 19 | $target->onMessage = function (TcpConnection $target, Binary $data) use ($source) { 20 | // 客户端的来源信息 21 | dump('-------------------------------------------------------------------------------------------------------------------'); 22 | dump('Server', $data->dump(), Packet::parser(null, $data)); 23 | dump('Packet-Class: ' . Packet::getPacketClass($data)); 24 | dump('-------------------------------------------------------------------------------------------------------------------'); 25 | $source->send($data); 26 | }; 27 | 28 | $source->pipe($target); 29 | $source->onMessage = function (TcpConnection $source, Binary $data) use ($target) { 30 | dump('-------------------------------------------------------------------------------------------------------------------'); 31 | dump('Client', $data->dump(), Packet::parser(null, $data)); 32 | dump('-------------------------------------------------------------------------------------------------------------------'); 33 | $target->send($data); 34 | }; 35 | 36 | $target->connect(); 37 | }; 38 | 39 | \Workerman\Worker::runAll(); 40 | -------------------------------------------------------------------------------- /examples/server.php: -------------------------------------------------------------------------------- 1 | name = 'workbunny-mysql-server'; 18 | $server->count = 2; 19 | $server->onConnect = function (TcpConnection $connection) { 20 | // 构造握手包所需数据(这些数据可根据实际服务器配置、能力及认证数据来定制) 21 | $handshakeData = [ 22 | // 通常,协议版本固定为 10 23 | 'protocol_version' => 10, 24 | // 服务器版本 25 | 'server_version' => '8.4.3-workbunny', 26 | // 连接 ID(示例值,可以自定义) 27 | 'connection_id' => $connection->id, 28 | // 能力标识 29 | 'capability_flags' => 3758096383, 30 | // 字符集索引 31 | 'character_set_index'=> 255, 32 | // 状态标识 33 | 'status_flags' => 2, 34 | // 认证数据,必须至少 8 字节(示例数据) 35 | 'auth_plugin_data' => Packet::authData(21), 36 | // 认证插件名称(MySQL 8 默认认证插件通常是 caching_sha2_password) 37 | 'auth_plugin_name' => 'caching_sha2_password' 38 | ]; 39 | // 生成握手包的 Binary 对象 40 | $binary = HandshakeInitialization::pack($handshakeData); 41 | // 握手状态机 42 | $connection->mysql_handshake_status = 0; 43 | $connection->send($binary); 44 | }; 45 | 46 | $server->onMessage = function (TcpConnection $connection, Binary $binary) { 47 | // 友好打印 48 | dump($binary->dump()); 49 | // 判断状态机 50 | if (!isset($connection->mysql_handshake_status)) { 51 | $connection->close(Error::pack([ 52 | 'error_code' => 0, 53 | 'sql_state' => 'HY000', 54 | 'message' => 'Invalid connection.', 55 | ])); 56 | return; 57 | } 58 | // 状态机:0 握手阶段,1 已经握手 59 | if ($connection->mysql_handshake_status < 1) { 60 | // 握手响应信息获取 61 | $handshakeResponse = HandshakeResponse::unpack($binary); 62 | dump($handshakeResponse); 63 | // todo 可以对 数据信息进行验证,这里暂时不验证用户信息等 64 | 65 | // 状态机:1 已经握手 66 | $connection->mysql_handshake_status = 1; 67 | $connection->send(Ok::pack([ 68 | 'packet_id' => $handshakeResponse['packet_id'] + 1 69 | ])); 70 | } else { // command包 71 | $command = Command::unpack($binary); 72 | $connection->send(Ok::pack([ 73 | 'packet_id' => $command['packet_id'] + 1, 74 | ])); 75 | } 76 | }; 77 | 78 | \Workerman\Worker::runAll(); 79 | -------------------------------------------------------------------------------- /protocols/MySQL.php: -------------------------------------------------------------------------------- 1 | getMessage()}\n"); 34 | $connection->close(); 35 | return 0; 36 | } 37 | } 38 | 39 | /** 40 | * Decode package and emit onMessage($message) callback, $message is the result that decode returned. 41 | * 42 | * @param string $buffer 43 | * @param ConnectionInterface $connection 44 | * @return Binary|null 45 | */ 46 | public static function decode(string $buffer, ConnectionInterface $connection): ?Binary 47 | { 48 | try { 49 | return new Binary($buffer); 50 | } catch (\Throwable $throwable) { 51 | Worker::safeEcho("Error: {$throwable->getMessage()}\n"); 52 | $connection->close(); 53 | return null; 54 | } 55 | } 56 | 57 | /** 58 | * Encode package before sending to client. 59 | * 60 | * @param Binary $data 61 | * @param ConnectionInterface $connection 62 | * @return string 63 | */ 64 | public static function encode(Binary $data, ConnectionInterface $connection): string 65 | { 66 | return $data->pack(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Constants/Capabilities.php: -------------------------------------------------------------------------------- 1 | name))); 19 | } 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Constants/Errors.php: -------------------------------------------------------------------------------- 1 | errorCode = $code; 23 | parent::__construct("[{$code->getName($code->value)}] $message", $code->value, $previous); 24 | } 25 | 26 | /** 27 | * @return ExceptionCode 28 | */ 29 | public function getErrorCode(): ExceptionCode 30 | { 31 | return $this->errorCode; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | readByte(); 39 | if ($flag !== self::PACKET_FLAG) { 40 | throw new PacketException("Invalid packet flag '$flag', expected 0x01", ExceptionCode::ERROR_VALUE); 41 | } 42 | // 如果后续有附加数据,则读取之 43 | $remainingLength = $binary->length() - $binary->getReadCursor(); 44 | $extraData = []; 45 | if ($remainingLength > 0) { 46 | $extraData = $binary->readBytes($remainingLength); 47 | } 48 | 49 | return [ 50 | 'flag' => $flag, 51 | 'extra_data' => $extraData, 52 | ]; 53 | }, $binary); 54 | } 55 | 56 | /** 57 | * 将 AuthMoreDataRequest 数据包打包为 Binary 对象(payload部分,不包含 4 字节包头)。 58 | * 59 | * 需要传入的数据数组至少包含键: 60 | * - flag: int,标志字节(默认 0x01 表示请求全认证) 61 | * 62 | * 可选键: 63 | * - extra_data: array,附加数据 64 | * 65 | * @param array $data 66 | * @return Binary 67 | */ 68 | public static function pack(array $data): Binary 69 | { 70 | $packetId = $data['packet_id'] ?? 0; 71 | return Packet::binary(function (Binary $binary) use ($data) { 72 | $extraData = $data['extra_data'] ?? []; 73 | $binary->writeByte(self::PACKET_FLAG); 74 | if ($extraData) { 75 | if (!is_array($extraData)) { 76 | throw new PacketException('Invalid extra_data type, expected array', ExceptionCode::ERROR_TYPE); 77 | } 78 | $binary->writeBytes($extraData); 79 | } 80 | }, (int)$packetId); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Packets/AuthMoreDataResponse.php: -------------------------------------------------------------------------------- 1 | length() - $binary->getReadCursor(); 31 | $responseBytes = $binary->readBytes($remainingLength); 32 | return [ 33 | 'auth_response' => Binary::BytesToString($responseBytes), 34 | ]; 35 | }, $binary); 36 | } 37 | 38 | /** 39 | * 将 AuthMoreDataResponse 数据包打包为 Binary 对象(payload部分,不含包头)。 40 | * 41 | * 要求传入的数组包含键: 42 | * - auth_response: string,客户端计算或加密后的全认证数据 43 | * 44 | * @param array $data 45 | * @return Binary 46 | */ 47 | public static function pack(array $data): Binary 48 | { 49 | $packetId = $data['packet_id'] ?? 0; 50 | return Packet::binary(function (Binary $binary) use ($data) { 51 | $authResponse = $data['auth_response'] ?? ''; 52 | if (!is_string($authResponse)) { 53 | throw new PacketException('Invalid auth_response type, expected string', ExceptionCode::ERROR_TYPE); 54 | } 55 | $binary->writeBytes(Binary::StringToBytes($authResponse)); 56 | }, (int)$packetId); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Packets/AuthSwitchRequest.php: -------------------------------------------------------------------------------- 1 | readByte(); 31 | if ($flag !== self::PACKET_FLAG) { 32 | throw new PacketException("Invalid packet flag '$flag', expected 0xFE", ExceptionCode::ERROR_VALUE); 33 | } 34 | // 读取 NULL 终止的认证插件名称 35 | $pluginName = Binary::BytesToString($binary->readNullTerminated()); 36 | // 剩下的所有数据为附加的认证数据(可以为空) 37 | $remainingLength = $binary->length() - $binary->getReadCursor(); 38 | $authPluginData = $binary->readBytes($remainingLength); 39 | return [ 40 | 'flag' => $flag, 41 | 'plugin_name' => $pluginName, 42 | 'auth_plugin_data' => $authPluginData, 43 | ]; 44 | }, $binary); 45 | } 46 | 47 | /** 48 | * 将 AuthSwitchRequest 数据包内容封装为 Binary 对象。 49 | * 50 | * @param array $data 数组包含: 51 | * - plugin_name: 认证插件名称(字符串,必填) 52 | * - auth_plugin_data: 附加认证数据(字节组,可选) 53 | * @return Binary 54 | */ 55 | public static function pack(array $data): Binary 56 | { 57 | return Packet::binary(function (Binary $binary) use ($data) { 58 | $pluginName = $data['plugin_name'] ?? ''; 59 | $authPluginData = $data['auth_plugin_data'] ?? []; 60 | // 写入标志字节 0xFE 61 | $binary->writeByte(self::PACKET_FLAG); 62 | // 写入认证插件名称(字符串转换成字节数组)及 NULL 终止符 63 | if (!is_string($pluginName)) { 64 | throw new PacketException('Invalid plugin_name type, expected string', ExceptionCode::ERROR_TYPE); 65 | } 66 | $binary->writeNullTerminated(Binary::StringToBytes($pluginName)); 67 | // 如果附加认证数据存在,则写入 68 | if ($authPluginData) { 69 | if (!is_array($authPluginData)) { 70 | throw new PacketException('Invalid auth_plugin_data type, expected bytes array', ExceptionCode::ERROR_TYPE); 71 | } 72 | $binary->writeBytes($authPluginData); 73 | } 74 | }, $data['packet_id'] ?? 0); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Packets/AuthSwitchResponse.php: -------------------------------------------------------------------------------- 1 | length() - $binary->getReadCursor(); 27 | $authResponse = Binary::BytesToString($binary->readBytes($remainingLength)); 28 | 29 | return [ 30 | 'auth_response' => $authResponse, 31 | ]; 32 | }, $binary); 33 | } 34 | 35 | /** 36 | * 将 AuthSwitchResponse 数据包内容封装为 Binary 对象。 37 | * 38 | * @param array $data 数组包含: 39 | * - auth_response: 客户端针对新的认证挑战计算后的响应(字符串) 40 | * @return Binary 41 | */ 42 | public static function pack(array $data): Binary 43 | { 44 | $packetId = $data['packet_id'] ?? 0; 45 | return Packet::binary(function (Binary $binary) use ($data) { 46 | $authResponse = $data['auth_response'] ?? ''; 47 | if (!is_string($authResponse)) { 48 | throw new PacketException('Invalid auth_response value, expected string', ExceptionCode::ERROR_TYPE); 49 | } 50 | $binary->writeBytes(Binary::StringToBytes($authResponse)); 51 | }, $packetId); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/Packets/Command.php: -------------------------------------------------------------------------------- 1 | readByte(); 46 | // 如果还包含其它数据,则读取剩余部分,并将其视为字符串 47 | $remaining = $binary->length() - $binary->getReadCursor(); 48 | $data = null; 49 | if ($remaining > 0) { 50 | $data = Binary::BytesToString($binary->readBytes($remaining)); 51 | } 52 | return [ 53 | 'command' => $command, 54 | 'data' => $data, 55 | ]; 56 | }, $binary); 57 | } 58 | 59 | /** 60 | * 将 Command Packet 数据组装为 Binary 对象(payload部分,不包含包头)。 61 | * 62 | * 数组中应至少包含以下键: 63 | * - command: int 命令码 64 | * 65 | * 可选: 66 | * - data: string 具体命令数据(如 SQL 查询语句、数据库名称等) 67 | * 68 | * @param array $data 69 | * @return Binary 70 | */ 71 | public static function pack(array $data): Binary 72 | { 73 | $packetId = $data['packet_id'] ?? 0; 74 | return Packet::binary(function (Binary $binary) use ($data) { 75 | $command = $data['command']; 76 | $data = $data['data'] ?? null; 77 | // 写入命令码(1 字节) 78 | $binary->writeByte($command); 79 | 80 | // 如果存在额外数据,则写入(例如对于 COM_QUERY,把 SQL 语句写进来) 81 | if ($data) { 82 | if (!is_string($data)) { 83 | throw new PacketException("Invalid data type, expected string", ExceptionCode::ERROR_TYPE); 84 | } 85 | $binary->writeBytes(Binary::StringToBytes($data)); 86 | } 87 | }, $packetId); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Packets/EOF.php: -------------------------------------------------------------------------------- 1 | int, 'warnings'=>int, 'status_flags'=>int] 25 | */ 26 | public static function unpack(Binary $binary): array 27 | { 28 | return Packet::parser(function (Binary $binary) { 29 | $result = []; 30 | $flag = $binary->readByte(); 31 | if ($flag !== self::PACKET_FLAG) { 32 | throw new PacketException("Invalid packet flag '$flag', expected 0x00", ExceptionCode::ERROR_VALUE); 33 | } 34 | $result['flag'] = $flag; 35 | $result['warnings'] = $binary->readUB(Binary::UB2); 36 | $result['status_flags'] = $binary->readUB(Binary::UB2); 37 | return $result; 38 | }, $binary); 39 | } 40 | 41 | /** 42 | * 封装 EOF 包为 Binary 对象(payload部分)。 43 | * 44 | * 要求数组可包含: 45 | * - warnings (int) 46 | * - status_flags (int) 47 | * 48 | * @param array $data 49 | * @return Binary 50 | */ 51 | public static function pack(array $data): Binary 52 | { 53 | return Packet::binary(function (Binary $binary) use ($data) { 54 | // 写入 0xFE 作为包头标识 55 | $binary->writeByte(self::PACKET_FLAG); 56 | $binary->writeUB((int)($data['warnings'] ?? 0), Binary::UB2); 57 | $binary->writeUB((int)($data['status_flags'] ?? 0), Binary::UB2); 58 | }, $data['packet_id'] ?? 0); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Packets/Error.php: -------------------------------------------------------------------------------- 1 | readByte(); 31 | if ($flag !== self::PACKET_FLAG) { 32 | throw new PacketException("Invalid packet flag '$flag', expected 0xFF", ExceptionCode::ERROR_VALUE); 33 | } 34 | // 2. 读取 error_code:2字节 little-endian 35 | $errorCode = $binary->readUB(Binary::UB2); 36 | // 3. 读取 SQL State Marker,预期为 '#' (ASCII 35) 37 | $marker = $binary->readByte(); 38 | if ($marker !== ord('#')) { 39 | // 兼容旧协议:如果没有 '#',认为没有 SQL state信息 40 | $sqlState = ''; 41 | // 同时把 marker 字节作为错误消息的起始字节处理 42 | $errorMsgBytes = array_merge([$marker], $binary->readBytes($binary->length() - $binary->getReadCursor())); 43 | $errorMessage = Binary::BytesToString($errorMsgBytes); 44 | } else { 45 | // 4. 读取接下来的 5 字节作为 SQL state 46 | $sqlStateBytes = $binary->readBytes(5); 47 | $sqlState = Binary::BytesToString($sqlStateBytes); 48 | // 5. 剩余部分为错误消息 49 | $remaining = $binary->length() - $binary->getReadCursor(); 50 | $errorMsg = ''; 51 | if ($remaining > 0) { 52 | $errorMsgBytes = $binary->readBytes($remaining); 53 | $errorMsg = Binary::BytesToString($errorMsgBytes); 54 | } 55 | $errorMessage = $errorMsg; 56 | } 57 | 58 | return [ 59 | 'flag' => $flag, 60 | 'error_code' => $errorCode, 61 | 'sql_state' => $sqlState, 62 | 'error_message' => $errorMessage, 63 | ]; 64 | }, $binary); 65 | } 66 | 67 | /** 68 | * 封装 ERROR 包为 Binary 对象(payload部分,不包含包头)。 69 | * 70 | * 要求 $data 至少包含以下键: 71 | * - error_code (int) 72 | * - error_message (string) 73 | * 可选: 74 | * - sql_state (string), 默认使用 'HY000' 75 | * 76 | * @param array $data 77 | * @return Binary 78 | */ 79 | public static function pack(array $data): Binary 80 | { 81 | $packetId = $data['packet_id'] ?? 0; 82 | return Packet::binary(function (Binary $binary) use ($data) { 83 | $errorCode = $data['error_code'] ?? 0; 84 | $sqlState = $data['sql_state'] ?? 'HY000'; 85 | $errorMessage = $data['error_message'] ?? null; 86 | // 1. 写入 OK 包头 0x00 87 | $binary->writeByte(self::PACKET_FLAG); 88 | // 2. 写入 error code,2 字节 little-endian 89 | $binary->writeUB((int)$errorCode, Binary::UB2); 90 | // 3. 写入 SQL state marker '#' 和 5 字节 SQL state 91 | $binary->writeByte(ord('#')); 92 | // 不足 5 字节则用空格补齐,多余取前 5 字节 93 | $sqlState = str_pad((string)$sqlState, 5, ' '); 94 | $binary->writeBytes(Binary::StringToBytes(substr($sqlState, 0, 5))); 95 | // 4. 写入错误消息(剩余部分) 96 | if ($errorMessage) { 97 | $binary->writeBytes(Binary::StringToBytes((string)$errorMessage)); 98 | } 99 | }, $packetId); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Packets/Field.php: -------------------------------------------------------------------------------- 1 | readLenEncString(); 37 | $result['schema'] = $binary->readLenEncString(); 38 | $result['table'] = $binary->readLenEncString(); 39 | $result['org_table'] = $binary->readLenEncString(); 40 | $result['name'] = $binary->readLenEncString(); 41 | $result['org_name'] = $binary->readLenEncString(); 42 | 43 | // 固定字段长度,应该为 0x0c 44 | $fixedLength = $binary->readByte(); 45 | if ($fixedLength !== self::PACKET_FIXED_LENGTH) { 46 | throw new PacketException("Invalid packet fixed length '$fixedLength', expected 0x0c", ExceptionCode::ERROR_VALUE); 47 | } 48 | $result['fixed_length'] = $fixedLength; 49 | $result['character_set'] = $binary->readUB(Binary::UB2); 50 | $result['column_length'] = $binary->readUB(Binary::UB4); 51 | $result['type'] = $binary->readByte(); 52 | $result['flags'] = $binary->readUB(Binary::UB2); 53 | $result['decimals'] = $binary->readByte(); 54 | // 跳过 2 字节 filler 55 | $binary->readBytes(2); 56 | 57 | return $result; 58 | }, $binary); 59 | } 60 | 61 | /** 62 | * 封装字段定义数据为 FieldPacket 的 Binary 对象(payload部分)。 63 | * 64 | * 所需数据键包括 catalog, schema, table, org_table, name, org_name, character_set, 65 | * column_length, type, flags, decimals。 66 | * 67 | * @param array $data 68 | * @return Binary 69 | */ 70 | public static function pack(array $data): Binary 71 | { 72 | return Packet::binary(function (Binary $binary) use ($data) { 73 | $binary->writeLenEncString($data['catalog'] ?? 'def'); 74 | $binary->writeLenEncString( $data['schema'] ?? ''); 75 | $binary->writeLenEncString( $data['table'] ?? ''); 76 | $binary->writeLenEncString( $data['org_table'] ?? ''); 77 | $binary->writeLenEncString( $data['name'] ?? ''); 78 | $binary->writeLenEncString( $data['org_name'] ?? ''); 79 | // 固定长度字段:始终写入 0x0c 80 | $binary->writeByte(self::PACKET_FIXED_LENGTH); 81 | $binary->writeUB((int)($data['character_set'] ?? 33), Binary::UB2); 82 | $binary->writeUB((int)($data['column_length'] ?? 0), Binary::UB4); 83 | $binary->writeByte((int)($data['type'] ?? 0)); 84 | $binary->writeUB((int)($data['flags'] ?? 0), Binary::UB2); 85 | $binary->writeByte((int)($data['decimals'] ?? 0)); 86 | // 写入 2 字节 filler 87 | $binary->writeBytes([0x00, 0x00]); 88 | }, $data['packet_id'] ?? 0); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Packets/HandshakeInitialization.php: -------------------------------------------------------------------------------- 1 | readByte(); 42 | // 2. 服务器版本:NULL 终止字符串 43 | $serverVersion = Binary::BytesToString($binary->readNullTerminated()); 44 | // 3. 连接 ID:4 字节(小端序) 45 | $connectionId = $binary->readUB(Binary::UB4); 46 | // 4. Auth-plugin-data-part1:8 字节 47 | $authPluginDataPart1 = $binary->readBytes(8); 48 | // 5. Filler:1 字节(应为 0) 49 | $filler = $binary->readByte(); 50 | if ($filler !== 0) { 51 | throw new PacketException("Filler byte must be 0, found '$filler'", ExceptionCode::ERROR_VALUE); 52 | } 53 | // 6. 能力标识低 2 字节; 54 | $capLow = $binary->readUB(Binary::UB2); 55 | // 7. 字符集:1 字节 56 | $characterSetIndex = $binary->readByte(); 57 | // 8. 状态标识:2 字节 58 | $statusFlags = $binary->readUB(Binary::UB2); 59 | // 9. 能力标识高 2 字节 60 | $capHigh = $binary->readUB(Binary::UB2); 61 | $capabilityFlags = $capLow | ($capHigh << 16); 62 | // 10. Auth-plugin-data 长度:1 字节 63 | $authPluginDataLength = $binary->readByte(); 64 | // 11. 保留字段:10 字节 65 | $reserved = $binary->readBytes(10); 66 | // 12. Auth-plugin-data-part2:若长度 > 8,则读取多余字节,否则为空 67 | $part2Len = ($authPluginDataLength > 8) ? ($authPluginDataLength - 8) : 0; 68 | $authPluginDataPart2 = $part2Len > 0 ? $binary->readBytes($part2Len) : []; 69 | // 13. Auth-plugin 名称:NULL 终止字符串 70 | $authPluginName = Binary::BytesToString($binary->readNullTerminated()); 71 | 72 | return [ 73 | 'protocol_version' => $protocolVersion, 74 | 'server_version' => $serverVersion, 75 | 'connection_id' => $connectionId, 76 | 'capability_flags' => $capabilityFlags, 77 | 'character_set_index' => $characterSetIndex, 78 | 'status_flags' => $statusFlags, 79 | 'auth_plugin_data_length' => $authPluginDataLength, 80 | 'auth_plugin_data_part1' => $authPluginDataPart1, 81 | 'auth_plugin_data_part2' => $authPluginDataPart2, 82 | 'auth_plugin_name' => $authPluginName, 83 | 'reserved' => $reserved, 84 | ]; 85 | }, $binary); 86 | } 87 | 88 | /** 89 | * 将握手包数据封装为 Binary 对象 90 | * 91 | * 要求 $data 数组中必须包含以下字段: 92 | * - server_version (string) 93 | * - connection_id (int) 94 | * - capability_flags (int) 95 | * - character_set (int) 96 | * - status_flags (int) 97 | * - auth_plugin_data (string) (长度至少 8 字节) 98 | * - auth_plugin_name (string) 99 | * 100 | * 可选字段: 101 | * - protocol_version (int),默认为 10 102 | * 103 | * @param array $data 104 | * @return Binary 105 | * @throws PacketException 如果必填字段缺失或不合法 106 | */ 107 | public static function pack(array $data): Binary 108 | { 109 | foreach ( 110 | [ 111 | 'server_version', 'connection_id', 'capability_flags', 'character_set_index', 'status_flags', 112 | 'auth_plugin_data', 'auth_plugin_name' 113 | ] as $field 114 | ) { 115 | if (!isset($data[$field])) { 116 | throw new PacketException("Missing required field '$field' for handshake packet."); 117 | } 118 | } 119 | return Packet::binary(function (Binary $binary) use ($data) { 120 | $protocolVersion = $data['protocol_version'] ?? 10; 121 | $serverVersion = (string)$data['server_version']; 122 | $connectionId = (int)$data['connection_id']; 123 | $capabilityFlags = (int)$data['capability_flags']; 124 | $characterSetIndex = (int)$data['character_set_index']; 125 | $statusFlags = (int)$data['status_flags']; 126 | $authPluginData = (array)$data['auth_plugin_data']; 127 | $authPluginName = (string)$data['auth_plugin_name']; 128 | 129 | // 认证数据长度 130 | if (($authPluginDataLength = count($authPluginData)) < 8) { 131 | throw new PacketException("auth_plugin_data must be at least 8 bytes.", ExceptionCode::ERROR_VALUE); 132 | } 133 | foreach ($authPluginData as $byte) { 134 | if (!is_int($byte) || $byte < 0 || $byte > 255) { 135 | throw new PacketException("auth_plugin_data must be an array of bytes.", ExceptionCode::ERROR_VALUE); 136 | } 137 | } 138 | // 分割认证数据:前 8 字节为 part1,其余为 part2 139 | $authPluginPart1 = array_slice($authPluginData, 0, 8); 140 | $authPluginPart2 = $authPluginDataLength > 8 ? array_slice($authPluginData, 8) : [0]; 141 | 142 | $capLow = $capabilityFlags & 0xFFFF; 143 | $capHigh = ($capabilityFlags >> 16) & 0xFFFF; 144 | 145 | // 1. 写入协议版本(1 字节) 146 | $binary->writeByte($protocolVersion); 147 | // 2. 写入服务器版本(NULL 终止字符串) 148 | $binary->writeNullTerminated(Binary::StringToBytes($serverVersion)); 149 | // 3. 写入连接 ID(4 字节,小端序) 150 | $binary->writeUB($connectionId, Binary::UB4); 151 | // 4. 写入 Auth-plugin-data-part-1(8 字节) 152 | $binary->writeBytes($authPluginPart1); 153 | // 5. 写入 Filler(1 字节,0) 154 | $binary->writeByte(0); 155 | // 6. 写入能力标识低 2 字节(小端序) 156 | $binary->writeUB($capLow, Binary::UB2); 157 | // 7. 写入字符集(1 字节) 158 | $binary->writeByte($characterSetIndex); 159 | // 8. 写入状态标识(2 字节,小端序) 160 | $binary->writeUB($statusFlags, Binary::UB2); 161 | // 9. 写入能力标识高 2 字节(小端序) 162 | $binary->writeUB($capHigh, Binary::UB2); 163 | // 10. 写入 Auth-plugin-data 长度(1 字节) 164 | $binary->writeByte($authPluginDataLength); 165 | // 11. 写入保留字段(10 字节,全部为 0) 166 | $binary->writeBytes(array_fill(0, 10, 0)); 167 | // 12. 写入 Auth-plugin-data-part-2 168 | $binary->writeBytes($authPluginPart2); 169 | // 13. 写入 Auth-plugin 名称(NULL 终止字符串) 170 | $binary->writeNullTerminated(Binary::StringToBytes($authPluginName)); 171 | }, (int)$data['packet_id'] ?? 0); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Packets/HandshakeResponse.php: -------------------------------------------------------------------------------- 1 | readUB(Binary::UB4); 40 | // 2. 最大数据包大小:4 字节 41 | $maxPacketSize = $binary->readUB(Binary::UB4); 42 | // 3. 字符集:1 字节 43 | $characterSet = $binary->readByte(); 44 | // 4. 保留 23 字节 45 | $reserved = $binary->readBytes(23); 46 | // 5. 用户名,以 NULL 终止 47 | $username = Binary::BytesToString($binary->readNullTerminated()); 48 | // 6. 认证数据的读取 49 | if ($capabilityFlags & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { 50 | $authResponse = $binary->readLenEncString(); 51 | } elseif ($capabilityFlags & self::CLIENT_SECURE_CONNECTION) { 52 | $len = $binary->readByte(); 53 | $authResponse = Binary::BytesToString($binary->readBytes($len)); 54 | } else { 55 | $authResponse = Binary::BytesToString($binary->readNullTerminated()); 56 | } 57 | $database = null; 58 | // 7. 如果设置了 CLIENT_CONNECT_WITH_DB,则读取数据库名称(NULL 终止) 59 | if ($capabilityFlags & self::CLIENT_CONNECT_WITH_DB) { 60 | $database = Binary::BytesToString($binary->readNullTerminated()); 61 | } 62 | $authPlugin = null; 63 | // 8. 如果设置了 CLIENT_PLUGIN_AUTH,则读取认证插件名称(NULL 终止) 64 | if ($capabilityFlags & self::CLIENT_PLUGIN_AUTH) { 65 | $authPlugin = Binary::BytesToString($binary->readNullTerminated()); 66 | } 67 | $attributes = []; 68 | // 9. 如果设置了 CLIENT_CONNECT_ATTRS,则读取连接属性 69 | if ($capabilityFlags & self::CLIENT_CONNECT_ATTRS) { 70 | $attrTotalLength = $binary->readLenEncInt(); 71 | $read = 0; 72 | // 循环读取所有的属性键值对 73 | while ($read < $attrTotalLength) { 74 | $key = $binary->readLenEncString(); 75 | $value = $binary->readLenEncString(); 76 | $attributes[$key] = $value; 77 | $read += Binary::lenEncLength(strlen($key)) + strlen($key) 78 | + Binary::lenEncLength(strlen($value)) + strlen($value); 79 | } 80 | } 81 | 82 | return [ 83 | 'capability_flags' => $capabilityFlags, 84 | 'max_packet_size' => $maxPacketSize, 85 | 'character_set' => $characterSet, 86 | 'username' => $username, 87 | 'database' => $database, 88 | 'auth_plugin' => $authPlugin, 89 | 'auth_response' => $authResponse, 90 | 'attributes' => $attributes, 91 | 'reserved' => $reserved, 92 | ]; 93 | }, $binary); 94 | } 95 | 96 | /** 97 | * 将传入的数组数据组装为 HandshakeResponse 的 Binary 对象(payload部分)。 98 | * 99 | * 要求数组必须至少包含以下键: 100 | * - capability_flags (int) 101 | * - max_packet_size (int) 102 | * - character_set (int) 103 | * - username (string) 104 | * - auth_response (string) 105 | * 106 | * 可选: 107 | * - database (string) 如果设置了 CLIENT_CONNECT_WITH_DB 108 | * - auth_plugin (string) 如果设置了 CLIENT_PLUGIN_AUTH 109 | * - attributes (array) 如果设置了 CLIENT_CONNECT_ATTRS 110 | * 111 | * @param array $data 112 | * @return Binary 113 | */ 114 | public static function pack(array $data): Binary 115 | { 116 | $packetId = $data['packet_id'] ?? 0; 117 | return Packet::binary(function (Binary $binary) use ($data) { 118 | $capabilityFlags = (int)$data['capability_flags'] ?? 0; 119 | $maxPacketSize = (int)$data['max_packet_size'] ?? 0; 120 | $characterSet = (int)$data['character_set'] ?? 0; 121 | $username = (string)$data['username'] ?? ''; 122 | $database = (string)$data['database'] ?? ''; 123 | $authPlugin = (string)$data['auth_plugin'] ?? ''; 124 | $attributes = (array)$data['attributes'] ?? []; 125 | $authResponse = (string)$data['auth_response'] ?? ''; 126 | // 1. 写入能力标志(4 字节) 127 | $binary->writeUB($capabilityFlags, Binary::UB4); 128 | // 2. 写入最大数据包大小(4 字节) 129 | $binary->writeUB($maxPacketSize, Binary::UB4); 130 | // 3. 写入字符集(1 字节) 131 | $binary->writeByte($characterSet); 132 | // 4. 写入 23 字节保留字段(全部 0) 133 | $binary->writeBytes(array_fill(0, 23, 0)); 134 | // 5. 写入用户名,以 NULL 结尾 135 | $binary->writeNullTerminated(Binary::StringToBytes($username)); 136 | // 6. 写入认证响应 137 | if ($capabilityFlags & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { 138 | $binary->writeLenEncString($username); 139 | } elseif ($capabilityFlags & self::CLIENT_SECURE_CONNECTION) { 140 | $len = strlen($authResponse); 141 | $binary->writeByte($len); 142 | $binary->writeBytes(Binary::StringToBytes($authResponse)); 143 | } else { 144 | $binary->writeNullTerminated(Binary::StringToBytes($authResponse)); 145 | } 146 | // 7. 如果设置了 CLIENT_CONNECT_WITH_DB,则写入数据库名(NULL 终止) 147 | if ($capabilityFlags & self::CLIENT_CONNECT_WITH_DB) { 148 | $binary->writeNullTerminated(Binary::StringToBytes($database)); 149 | } 150 | // 8. 如果设置了 CLIENT_PLUGIN_AUTH,则写入认证插件名称(NULL 终止) 151 | if ($capabilityFlags & self::CLIENT_PLUGIN_AUTH) { 152 | $binary->writeNullTerminated(Binary::StringToBytes($authPlugin)); 153 | } 154 | // 9. 如果设置了 CLIENT_CONNECT_ATTRS,则写入连接属性 155 | if ($capabilityFlags & self::CLIENT_CONNECT_ATTRS) { 156 | $attrBlob = new Binary(); 157 | foreach ($attributes as $key => $value) { 158 | $attrBlob->writeLenEncString($key); 159 | $attrBlob->writeLenEncString($value); 160 | } 161 | $attrStr = $attrBlob->pack(); 162 | $binary->writeLenEncInt(strlen($attrStr)); 163 | $binary->writeBytes(Binary::StringToBytes($attrStr)); 164 | } 165 | }, (int)$packetId); 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/Packets/Ok.php: -------------------------------------------------------------------------------- 1 | readByte(); 33 | if ($flag !== self::PACKET_FLAG) { 34 | throw new PacketException("Invalid packet flag '$flag', expected 0x00", ExceptionCode::ERROR_VALUE); 35 | } 36 | // 2. 读取 length-encoded affected rows 37 | $affectedRows = $binary->readLenEncInt(); 38 | // 3. 读取 length-encoded last insert id 39 | $lastInsertId = $binary->readLenEncInt(); 40 | // 4. 读取2字节 status flags 41 | $statusFlags = $binary->readUB(Binary::UB2); 42 | // 5. 读取2字节 warnings count 43 | $warnings = $binary->readUB(Binary::UB2); 44 | // 6. 剩下的为 info 字符串 45 | $info = null; 46 | $remaining = $binary->length() - $binary->getReadCursor(); 47 | if ($remaining > 0) { 48 | $info = Binary::BytesToString($binary->readBytes($remaining)); 49 | } 50 | 51 | return [ 52 | 'flag' => $flag, 53 | 'affected_rows' => $affectedRows, 54 | 'last_insert_id' => $lastInsertId, 55 | 'status_flags' => $statusFlags, 56 | 'warnings' => $warnings, 57 | 'info' => $info, 58 | ]; 59 | }, $binary); 60 | } 61 | 62 | /** 63 | * 封装 OK 包为 Binary 对象(payload部分,不包含 4 字节包头)。 64 | * 65 | * 要求 $data 至少包含以下键: 66 | * - affected_rows (int) 67 | * - last_insert_id (int) 68 | * - status_flags (int) 69 | * - warnings (int) 70 | * - info (string,可选) 71 | * 72 | * @param array $data 73 | * @return Binary 74 | */ 75 | public static function pack(array $data): Binary 76 | { 77 | return Packet::binary(function (Binary $binary) use ($data) { 78 | $affectedRows = $data['affected_rows'] ?? 0; 79 | $lastInsertId = $data['last_insert_id'] ?? 0; 80 | $statusFlags = $data['status_flags'] ?? 0; 81 | $warnings = $data['warnings'] ?? 0; 82 | $info = $data['info'] ?? null; 83 | // 1. 写入 OK 包头 0x00 84 | $binary->writeByte(self::PACKET_FLAG); 85 | // 2. 写入 affected rows 以长度编码整数格式写入 86 | $binary->writeLenEncInt((int)$affectedRows); 87 | // 3. 写入 last insert id 以长度编码整数格式写入 88 | $binary->writeLenEncInt((int)$lastInsertId); 89 | // 4. 写入 status flags (2 字节) 90 | $binary->writeUB((int)$statusFlags, Binary::UB2); 91 | // 5. 写入 warnings (2 字节) 92 | $binary->writeUB((int)$warnings, Binary::UB2); 93 | // 6. 写入 info 字符串(如果存在) 94 | if ($info) { 95 | $binary->writeBytes(Binary::StringToBytes((string)$info)); 96 | } 97 | }, (int)$data['packet_id'] ?? 0); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Packets/PacketInterface.php: -------------------------------------------------------------------------------- 1 | int] 17 | */ 18 | public static function unpack(Binary $binary): array 19 | { 20 | return Packet::parser(function (Binary $binary) { 21 | return [ 22 | 'field_count' => $binary->readLenEncInt() 23 | ]; 24 | }, $binary); 25 | } 26 | 27 | /** 28 | * 将结果集头数据组装为 Binary 对象(payload部分)。 29 | * 30 | * 数组中须包含: 31 | * - field_count: int 32 | * 33 | * @param array $data 34 | * @return Binary 35 | */ 36 | public static function pack(array $data): Binary 37 | { 38 | return Packet::binary(function (Binary $binary) use ($data) { 39 | $fieldCount = (int)($data['field_count'] ?? 0); 40 | $binary->writeLenEncInt($fieldCount); 41 | }, (int)$data['packet_id'] ?? 0); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Packets/RowData.php: -------------------------------------------------------------------------------- 1 | getReadCursor() < $binary->length()) { 31 | // 读取第一个字节判断是否为 NULL 指示符 0xFB 32 | $nextByte = $binary->readByte(); 33 | if ($nextByte === self::NULL_VALUE) { 34 | $values[] = null; 35 | } else { 36 | // 非 NULL 值:将刚刚读的字节退回(减 1 个指针位置),再完整读取长度编码字符串 37 | $binary->setReadCursor($binary->getReadCursor() - 1); 38 | $value = $binary->readLenEncString(); 39 | $values[] = $value; 40 | } 41 | } 42 | return [ 43 | 'values' => $values, 44 | ]; 45 | }, $binary); 46 | } 47 | 48 | /** 49 | * 将一行数据封装为 RowDataPacket(payload部分)。 50 | * 51 | * 输入为数组,每个元素代表一列的值。NULL 值写为单字节 0xFB, 52 | * 非 NULL 值以长度编码字符串写入。 53 | * 54 | * @param array $data 55 | * @return Binary 56 | */ 57 | public static function pack(array $data): Binary 58 | { 59 | return Packet::binary(function (Binary $binary) use ($data) { 60 | foreach (($data['values'] ?? []) as $value) { 61 | if (is_null($value)) { 62 | $binary->writeByte(self::NULL_VALUE); 63 | } else { 64 | $binary->writeLenEncString((string)$value); 65 | } 66 | } 67 | }, (int)$data['packet_id'] ?? 0); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Utils/Binary.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $bytes = []; 29 | 30 | /** 31 | * 二进制字符串 32 | * 33 | * @var string|null 34 | */ 35 | protected null|string $string = null; 36 | 37 | /** 38 | * 数据长度 39 | * 40 | * @var int|null 41 | */ 42 | protected ?int $length = null; 43 | 44 | /** 45 | * bytes数量 46 | * 47 | * @var int|null 48 | */ 49 | protected ?int $count = null; 50 | 51 | /** 52 | * 读指针 53 | * 54 | * @var int 55 | */ 56 | protected int $readCursor = 0; 57 | 58 | /** 59 | * 写指针 60 | * 61 | * @var int 写指针 62 | */ 63 | protected int $writeCursor = 0; 64 | 65 | /** 66 | * 字符串转为字节组 67 | * 68 | * @param string $string 二进制流/明文字符串 69 | * @return int[] 70 | */ 71 | public static function StringToBytes(string $string): array 72 | { 73 | if (($bytes = unpack('C*', $string)) === false) { 74 | throw new InvalidArgumentException("String '$string' is invalid", ExceptionCode::ERROR_TYPE); 75 | } 76 | return array_values($bytes); 77 | } 78 | 79 | /** 80 | * 字节组转为字符串 81 | * 82 | * @param array $bytes 83 | * @return string 二进制流/明文字符串 84 | */ 85 | public static function BytesToString(array $bytes): string 86 | { 87 | return pack('C*', ...$bytes); 88 | } 89 | 90 | /** 91 | * 辅助函数:根据字符串真实长度返回对应“长度编码整数”本身所占字节数。 92 | * 93 | * @param int $valueLength 94 | * @return int 95 | */ 96 | public static function LenEncLength(int $valueLength): int 97 | { 98 | return match (true) { 99 | $valueLength < 251 => 1, 100 | $valueLength < (1 << 16) => 1 + 2, 101 | $valueLength < (1 << 24) => 1 + 3, 102 | default => 1 + 8, 103 | }; 104 | } 105 | 106 | /** 107 | * 构造函数 108 | * 109 | * @param array|string|null $payload 字符串或者字节数组 110 | * @throws InvalidArgumentException 111 | */ 112 | public function __construct(mixed $payload = null) 113 | { 114 | $this->payload = $payload; 115 | switch (true) { 116 | case is_null($payload): 117 | $this->bytes = []; 118 | break; 119 | case is_numeric($payload): 120 | $byte = (int)$payload; 121 | if ($byte < 0 || $byte > 255) { 122 | throw new InvalidArgumentException("Payload numeric '$payload' is invalid", ExceptionCode::ERROR_TYPE); 123 | } 124 | $this->bytes = []; 125 | break; 126 | case is_string($payload): 127 | $this->bytes = static::StringToBytes($payload); 128 | break; 129 | case is_iterable($payload) or ($payload instanceof stdClass): 130 | $payload = ($payload instanceof stdClass) ? get_object_vars($payload) : $payload; 131 | $bytes = []; 132 | foreach ($payload as $index => $byte) { 133 | if (!is_int($byte) || $byte < 0 || $byte > 255) { 134 | throw new InvalidArgumentException("Bytes '$index'->'$byte' is invalid", ExceptionCode::ERROR_TYPE); 135 | } 136 | $bytes[] = $byte; 137 | } 138 | $this->bytes = $bytes; 139 | break; 140 | default: 141 | $type = gettype($payload); 142 | throw new InvalidArgumentException("Payload type '$type' is invalid", ExceptionCode::ERROR_TYPE); 143 | } 144 | } 145 | 146 | /** 147 | * 获取读指针的位置 148 | * 149 | * @return int 150 | */ 151 | public function getReadCursor(): int 152 | { 153 | return $this->readCursor; 154 | } 155 | 156 | /** 157 | * 获取写指针的位置 158 | * 159 | * @return int 160 | */ 161 | public function getWriteCursor(): int 162 | { 163 | return $this->writeCursor; 164 | } 165 | 166 | /** 167 | * 设置内部指针的位置 168 | * 169 | * @param int $position 170 | * @throws InvalidArgumentException 171 | */ 172 | public function setReadCursor(int $position): void 173 | { 174 | if ($position < 0 || $position > $this->count()) { 175 | throw new InvalidArgumentException("Invalid cursor '$position'", ExceptionCode::ERROR_VALUE); 176 | } 177 | $this->readCursor = $position; 178 | } 179 | 180 | /** 181 | * @param int $position 182 | * @return void 183 | */ 184 | public function setWriteCursor(int $position): void 185 | { 186 | if ($this->count() < $position) { 187 | for ($i = $this->count(); $i < $position; $i++) { 188 | $this->bytes[] = 0; 189 | } 190 | } 191 | $this->writeCursor = $position; 192 | } 193 | 194 | /** 195 | * @return mixed 196 | */ 197 | public function payload(): mixed 198 | { 199 | return $this->payload; 200 | } 201 | 202 | /** 203 | * 将payload转换为字节数组 204 | * 205 | * @return array 206 | */ 207 | public function unpack(): array 208 | { 209 | return $this->bytes; 210 | } 211 | 212 | /** 213 | * 将bytes转换为字符串形式 214 | * 215 | * @param bool $cache 216 | * @return string 217 | */ 218 | public function pack(bool $cache = false): string 219 | { 220 | if ($cache and $this->string === null) { 221 | $this->string = self::BytesToString($this->bytes); 222 | } 223 | return $this->string; 224 | } 225 | 226 | /** 227 | * 获取二进制字符串长度 228 | * 229 | * @param bool $cache 230 | * @return int 231 | */ 232 | public function length(bool $cache = false): int 233 | { 234 | if (!$cache) { 235 | $this->length = null; 236 | } 237 | if ($this->length === null) { 238 | $this->length = strlen($this->pack()); 239 | } 240 | return $this->length; 241 | } 242 | 243 | /** 244 | * 获取字节数组数量 245 | * 246 | * @param bool $cache 247 | * @return int 248 | */ 249 | public function count(bool $cache = false): int 250 | { 251 | if (!$cache) { 252 | $this->count = null; 253 | } 254 | if ($this->count === null) { 255 | $this->count = count($this->unpack()); 256 | } 257 | return $this->count; 258 | } 259 | 260 | /** 261 | * 生成友好打印的二进制 map 262 | * 263 | * @return string 264 | */ 265 | public function dump(): string 266 | { 267 | $total = $this->count(); 268 | $output = ''; 269 | $bytesPerLine = 16; 270 | 271 | for ($i = 0; $i < $total; $i += $bytesPerLine) { 272 | $line = sprintf('%08X ', $i); 273 | $chunk = array_slice($this->bytes, $i, $bytesPerLine); 274 | $hexPart = ''; 275 | $asciiPart = ''; 276 | 277 | foreach ($chunk as $index => $byte) { 278 | $hexPart .= sprintf('%02X ', $byte); 279 | if ($index === 7) { 280 | $hexPart .= ' '; 281 | } 282 | $asciiPart .= ($byte >= 32 && $byte <= 126) ? chr($byte) : '.'; 283 | } 284 | $expectedHexLen = ($bytesPerLine * 3) + 1; 285 | $hexPart = str_pad($hexPart, $expectedHexLen); 286 | $line .= "$hexPart |$asciiPart|\n"; 287 | $output .= $line; 288 | } 289 | 290 | return $output; 291 | } 292 | 293 | /** 294 | * 读取当前指针位置的一个字节(返回 0~255 的整数),随后指针前移 1 295 | * 296 | * @return int 297 | */ 298 | public function readByte(): int 299 | { 300 | if ($this->readCursor >= $this->length()) { 301 | throw new InvalidArgumentException("Read Cursor '$this->readCursor' > length of data", ExceptionCode::ERROR_CURSOR); 302 | } 303 | return $this->bytes[$this->readCursor++]; 304 | } 305 | 306 | /** 307 | * 从当前指针位置读取指定长度的字节串,随后指针前移相应长度 308 | * 309 | * @param int $length 310 | * @return int[] 311 | */ 312 | public function readBytes(int $length): array 313 | { 314 | if ($this->readCursor + $length > $this->length()) { 315 | throw new InvalidArgumentException("Read Cursor '$this->readCursor' + length '$length' > length of data", ExceptionCode::ERROR_CURSOR); 316 | } 317 | $readOffset = $this->readCursor; 318 | $this->readCursor += $length; 319 | return array_slice($this->bytes, $readOffset, $length); 320 | } 321 | 322 | /** 323 | * 从当前指针位置读取直到第一个 NULL 字节(0x00) 324 | * 325 | * @return int[] 326 | */ 327 | public function readNullTerminated(): array 328 | { 329 | $bytes = []; 330 | while (true) { 331 | $byte = $this->readByte(); 332 | if ($byte === 0) { 333 | break; 334 | } 335 | $bytes[] = $byte; 336 | } 337 | return $bytes; 338 | } 339 | 340 | /** 341 | * 当前指针位置读取指定字节数组成的无符号整数 342 | * 343 | * @param int $byteCount 字节数 344 | * @param bool $littleEndian 小端序 345 | * @return int 346 | */ 347 | public function readUB(int $byteCount, bool $littleEndian = true): int 348 | { 349 | $bytes = $this->readBytes($byteCount); 350 | if (!$littleEndian) { 351 | $bytes = array_reverse($bytes); 352 | } 353 | $func = static function ($bytes, $byteCount) { 354 | $value = 0; 355 | for ($i = 0; $i < $byteCount; $i++) { 356 | $byte = $bytes[$i]; 357 | $value |= $byte << (8 * $i); 358 | } 359 | return $value; 360 | }; 361 | return match ($byteCount) { 362 | self::UB2 => unpack('v', self::BytesToString($bytes))[1], 363 | self::UB4 => unpack('V',self::BytesToString($bytes))[1], 364 | self::UB8 => unpack('P', self::BytesToString($bytes))[1], 365 | default => $func($bytes, $byteCount) 366 | }; 367 | } 368 | 369 | /** 370 | * 写入一个字节到当前的字符串数据末尾,并刷新缓存 371 | * 372 | * @param int $byte 取值 0~255 373 | */ 374 | public function writeByte(int $byte): void 375 | { 376 | if ($byte < 0 || $byte > 255) { 377 | throw new InvalidArgumentException("Byte '$byte' is invalid", ExceptionCode::ERROR_VALUE); 378 | } 379 | $this->bytes[$this->writeCursor ++] = $byte; 380 | } 381 | 382 | /** 383 | * 写入一组字节到当前的字符串数据末尾 384 | * 385 | * @param int[] $bytes 386 | */ 387 | public function writeBytes(array $bytes): void 388 | { 389 | $bs = []; 390 | foreach ($bytes as $index => $byte) { 391 | if ($byte < 0 || $byte > 255) { 392 | throw new InvalidArgumentException("Bytes '$index'->'$byte' is invalid", ExceptionCode::ERROR_VALUE); 393 | } 394 | $bs[] = $byte; 395 | } 396 | foreach ($bs as $byte) { 397 | $this->bytes[$this->writeCursor ++] = $byte; 398 | } 399 | } 400 | 401 | /** 402 | * 写入一个无符号整数,写入字节数由 $byteCount 决定 403 | * 404 | * @param int $int 405 | * @param int $byteCount 406 | * @param bool $littleEndian 407 | */ 408 | public function writeUB(int $int, int $byteCount, bool $littleEndian = true): void 409 | { 410 | $fuc = static function ($int, $byteCount) { 411 | $bytes = []; 412 | for ($i = 0; $i < $byteCount; $i++) { 413 | $bytes[] = ($int >> (8 * $i)) & 255; 414 | } 415 | return $bytes; 416 | }; 417 | $bytes = match ($byteCount) { 418 | self::UB2 => unpack('C*', pack('v', $int)), 419 | self::UB4 => unpack('C*', pack('V', $int)), 420 | self::UB8 => unpack('C*', pack('P', $int)), 421 | default => $fuc($int, $byteCount) 422 | }; 423 | $this->writeBytes(!$littleEndian ? array_reverse($bytes) : $bytes); 424 | } 425 | 426 | /** 427 | * 追加写入NULL终止符 428 | * 429 | * @param array $bytes 430 | */ 431 | public function writeNullTerminated(array $bytes): void 432 | { 433 | $bytes[] = 0; 434 | $this->writeBytes($bytes); 435 | } 436 | 437 | /** 438 | * 读取“长度编码整数”。 439 | * 440 | * @return int 441 | */ 442 | public function readLenEncInt(): int 443 | { 444 | $first = $this->readByte(); 445 | return match (true) { 446 | $first < 251 => $first, 447 | $first === 0xfc => $this->readUB(static::UB2), 448 | $first === 0xfd => $this->readUB(static::UB3), 449 | $first === 0xfe => $this->readUB(static::UB8), 450 | default => throw new InvalidArgumentException("Integer first tag '$first' is invalid", ExceptionCode::ERROR_VALUE), 451 | }; 452 | } 453 | 454 | /** 455 | * 读取“长度编码字符串”。 456 | * 457 | * @return string 458 | */ 459 | public function readLenEncString(): string 460 | { 461 | return static::BytesToString($this->readBytes($this->readLenEncInt())); 462 | } 463 | 464 | /** 465 | * 写入“长度编码整数”。 466 | * 467 | * @param int $value 468 | */ 469 | public function writeLenEncInt(int $value): void 470 | { 471 | switch (true) { 472 | case $value < 251: 473 | $this->writeByte($value); 474 | break; 475 | case $value < (1 << 16): 476 | $this->writeByte(0xfc); 477 | $this->writeUB($value, static::UB2); 478 | break; 479 | case $value < (1 << 24): 480 | $this->writeByte(0xfd); 481 | $this->writeUB($value, static::UB3); 482 | break; 483 | default: 484 | $this->writeByte(0xfe); 485 | $this->writeUB($value, static::UB8); 486 | break; 487 | } 488 | } 489 | 490 | /** 491 | * 写入“长度编码字符串”。 492 | * 493 | * @param string $str 494 | */ 495 | public function writeLenEncString(string $str): void 496 | { 497 | $bytes = static::StringToBytes($str); 498 | $this->writeLenEncInt(strlen($str)); 499 | $this->writeBytes($bytes); 500 | } 501 | } -------------------------------------------------------------------------------- /src/Utils/Charset.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private const CHARSET_MAP = [ 18 | 1 => 'big5', 19 | 3 => 'dec8', 20 | 4 => 'cp850', 21 | 6 => 'hp8', 22 | 8 => 'latin1', 23 | 12 => 'macce', 24 | 15 => 'macroman', 25 | 16 => 'dos', 26 | 17 => 'cp852', 27 | 18 => 'latin2', 28 | 19 => 'swe7', 29 | 20 => 'ibm850', 30 | 21 => 'ibm866', 31 | 22 => 'cp865', 32 | 23 => 'cp1252', 33 | 24 => 'cp1251', 34 | 25 => 'cp1256', 35 | 26 => 'cp1257', 36 | 33 => 'utf8', 37 | 45 => 'utf8mb4', 38 | 63 => 'binary' 39 | ]; 40 | 41 | /** 42 | * 根据字符集编号获取对应的字符集名称 43 | * 44 | * @param int $index 45 | * @return string 46 | * @throws InvalidArgumentException 47 | */ 48 | public static function getCharsetNameByIndex(int $index): string 49 | { 50 | if (!isset(self::CHARSET_MAP[$index])) { 51 | throw new InvalidArgumentException("Charset index '$index' is not supported.", ExceptionCode::ERROR_SUPPORT); 52 | } 53 | return self::CHARSET_MAP[$index]; 54 | } 55 | 56 | /** 57 | * 根据字符集名称查询对应的字符集编号 58 | * 59 | * @param string $name 字符集名称(例如 "utf8mb4") 60 | * @return int 61 | * @throws InvalidArgumentException 62 | */ 63 | public static function getCharsetIndexByName(string $name): int 64 | { 65 | $normalized = strtolower($name); 66 | // 使用 array_map 将所有名称转换为小写,与 normalized 进行比较 67 | $lookup = array_map('strtolower', self::CHARSET_MAP); 68 | $index = array_search($normalized, $lookup, true); 69 | if ($index === false) { 70 | throw new InvalidArgumentException("Charset '$name' is not supported.", ExceptionCode::ERROR_SUPPORT); 71 | } 72 | return (int)$index; 73 | } 74 | 75 | /** 76 | * 返回所有支持的字符集映射。 77 | * 78 | * @return array