不兼容修改-003
最后更新于:2022-04-02 07:08:20
## 不兼容修改-003 (2020-03-13)
> 本次更新主要是为了和即将推出的微服务开发统一骨架代码,在`SyncInvoke` `JsonRpc`两个模块有不兼容改动
当你使用 `composer update` 命令升级后出现以下异常时,请根据以下提示修复
## Class 'Mix\JsonRpc\Pool\ConnectionPool' not found
由于 JsonRpc 微服务化,所以重写了客户端,也移除了 Pool 的功能,因此需修改依赖注入使用新的客户端类。
1. 在 manifest/beans/jsonrpc.php 文件中全部内容修改为如下:
~~~
\Mix\JsonRpc\Client\Connection::class,
// 初始方法
'initMethod' => 'connect',
// 属性注入
'properties' => [
// host
'host' => '127.0.0.1',
// port
'port' => 9506,
],
],
// JsonRpc服务器
[
// 名称
'name' => 'jsonRpcServer',
// 类路径
'class' => \Mix\JsonRpc\Server::class,
// 构造函数注入
'constructorArgs' => [
// host
'127.0.0.1',
// port
9506,
],
],
];
~~~
2. 在客户端调用处修改 `context()->get()` 内的依赖名称
```
$this->client = context()->get(\Mix\JsonRpc\Client::class);
```
修改为:
```
$this->client = context()->get(\Mix\JsonRpc\Client\Connection::class);
```
## Class 'Mix\SyncInvoke\Client' not found
1. 在 manifest/beans/syncinvoke.php 文件中全部内容修改为如下:
~~~
'syncInvokePool',
// 作用域
'scope' => \Mix\Bean\BeanDefinition::SINGLETON,
// 类路径
'class' => \Mix\SyncInvoke\Pool\ConnectionPool::class,
// 属性注入
'properties' => [
// 最多可空闲连接数
'maxIdle' => 5,
// 最大连接数
'maxActive' => 50,
// 拨号器
'dialer' => ['ref' => \Mix\SyncInvoke\Pool\Dialer::class],
// 事件调度器
'eventDispatcher' => ['ref' => 'event'],
],
],
// SyncInvoke连接池拨号器
[
// 类路径
'class' => \Mix\SyncInvoke\Pool\Dialer::class,
// 属性注入
'properties' => [
// port
'port' => 9505,
],
],
// SyncInvoke服务器
[
// 名称
'name' => 'syncInvokeServer',
// 类路径
'class' => \Mix\SyncInvoke\Server::class,
// 构造函数注入
'constructorArgs' => [
// port
9505,
],
],
];
~~~
2. 在客户端同步代码调用处修改为如下这种连接池的方式:
~~~
*/
class CurlController
{
/**
* @var ConnectionPool
*/
public $pool;
/**
* CurlController constructor.
*/
public function __construct(ServerRequest $request, Response $response)
{
$this->pool = context()->get('syncInvokePool');
}
/**
* Index
* @param ServerRequest $request
* @param Response $response
* @return Response
* @throws \Mix\SyncInvoke\Exception\InvokeException
* @throws \Swoole\Exception
*/
public function index(ServerRequest $request, Response $response)
{
// 跨进程执行同步代码
$conn = $this->pool->getConnection();
$data = $conn->invoke(function () {
// ...
});
$conn->release();
// 响应
$content = ['code' => 0, 'message' => 'OK', 'data' => $data];
return ResponseHelper::json($response, $content);
}
}
~~~
## Cannot make static method Mix\\Route\\Router::show404() non static in class App\\Web\\Route\\Router
凡是出现 `show404`、`show500` 异常的,都是因为继承重写该两个方法导致的问题,因为我修改了父类的方法,这时候用户只需要查看一下父类的这两个方法是否是 `static` ,修改为与父类一致即可。
';
不兼容修改-002
最后更新于:2022-04-02 07:08:17
## 不兼容修改-002
> 本次修改主要为了统一命名风格
当你使用 `composer update` 命令升级后出现以下异常时,请根据以下提示修复
## Mix\Sync\Invoke 类找不到
启动时抛出以下异常:
```
[error] 2020-01-15 12:13:27.743 <89836> Class 'Mix\Sync\Invoke\Pool\ConnectionPool' not found
[code] 0 [type] Error
[file] in /Users/liujian/Downloads/mix/vendor/mix/bean/src/BeanDefinition.php on line 156
```
用编辑器替换全部字符 `Sync\Invoke` 为 `SyncInvoke` 。
## Connection::invoke 调用同步执行器时出错
当 invoke 时出现一下异常时:
```
[error] 2020-01-15 12:27:04.584 <89876> Call to undefined method Mix\SyncInvoke\Connection::invoke()
[code] 0 [type] Error
[file] in /Users/liujian/Downloads/mix/app/Http/Controllers/CurlController.php on line 43
```
在 `manifest.php` 文件中增加以下依赖配置:
~~~
// SyncInvoke客户端
[
// 类路径
'class' => \Mix\SyncInvoke\Client::class,
// 属性注入
'properties' => [
// 连接池
'pool' => ['ref' => 'syncInvokePool'],
],
],
~~~
调用同步代码修改为以下调用方式:
```
/** @var \Mix\SyncInvoke\Client $client */
$client = context()->get(\Mix\SyncInvoke\Client::class);
$data = $client->invoke(function () {
// 同步代码 ...
});
```
## Redis\Subscribe 异常
当使用 WebSocket 订阅时出现一下异常时:
```
[error] 2020-01-15 12:21:19.843 <89859> Too few arguments to function Mix\Redis\Subscribe\Subscriber::__construct(), 0 passed in /Users/liujian/Downloads/mix/vendor/mix/bean/src/BeanDefinition.php on line 156 and at least 2 expected
[code] 0 [type] ArgumentCountError
[file] in /Users/liujian/Downloads/mix/vendor/mix/redis-subscribe/src/Subscriber.php on line 58
```
修改 `manifest.php` 文件中的 Redis 订阅器配置如下:
```
// Redis订阅器
[
// 类路径
'class' => \Mix\Redis\Subscribe\Subscriber::class,
// 构造函数注入
'constructorArgs' => [
// host
getenv('REDIS_HOST'),
// port
getenv('REDIS_PORT'),
// password
getenv('REDIS_PASSWORD'),
// timeout
5,
],
],
```
';
不兼容修改-001
最后更新于:2022-04-02 07:08:15
## 不兼容修改-001
> 该修改是由于 Swoole 无法在协程中 fork 进程,因此只能通过该方式处理进程守护。
当你使用 `composer update` 命令升级后出现以下异常时,请根据以下提示修复
## event 依赖找不到
启动时抛出以下异常
```
[error] 2020-01-11 08:11:55.697 <802> Bean definition not found: event
[code] 0 [type] Mix\Bean\Exception\NotFoundException
```
再 manifest.php 中找到事件调度器的配置,增加 name => 'event' 配置:
~~~
// 事件调度器
[
// 名称
'name' => 'event',
// 作用域
'scope' => \Mix\Bean\BeanDefinition::SINGLETON,
// 类路径
'class' => \Mix\Event\EventDispatcher::class,
// 构造函数注入
'constructorArgs' => [
\App\Common\Listeners\DatabaseListener::class,
\App\Common\Listeners\RedisListener::class,
],
],
~~~
## 后台执行失效
- 升级后 `-d` 后台执行失效,如下:
```
php bin/mix.php http:start -d
```
- 或者启动服务器时,抛出以下异常
```
MacOS unsupport fork in coroutine, please use it before the Swoole\Coroutine\Scheduler start.
```
### 解决方法
首先再 `Common/Listeners` 目录中创建一个 `CommandListener.php`
~~~
*/
class CommandListener implements ListenerInterface
{
/**
* 监听的事件
* @return array
*/
public function events(): array
{
// 要监听的事件数组,可监听多个事件
return [
CommandBeforeExecuteEvent::class,
];
}
/**
* 处理事件
* @param object $event
* @throws \Swoole\Exception
*/
public function process(object $event)
{
// 事件触发后,会执行该方法
// 守护处理
if ($event instanceof CommandBeforeExecuteEvent) {
switch ($event->command) {
case \App\Http\Commands\StartCommand::class:
case \App\WebSocket\Commands\StartCommand::class:
case \App\Tcp\Commands\StartCommand::class:
case \App\Udp\Commands\StartCommand::class:
case \App\Sync\Commands\StartCommand::class:
if (Flag::bool(['d', 'daemon'], false)) {
ProcessHelper::daemon();
}
break;
}
}
}
}
~~~
然后再 manifest.php 中找到事件调度器的配置,将上面的监听器类注册进去:
~~~
// 事件调度器
[
// 名称
'name' => 'event',
// 作用域
'scope' => \Mix\Bean\BeanDefinition::SINGLETON,
// 类路径
'class' => \Mix\Event\EventDispatcher::class,
// 构造函数注入
'constructorArgs' => [
\App\Common\Listeners\CommandListener::class,
\App\Common\Listeners\DatabaseListener::class,
\App\Common\Listeners\RedisListener::class,
],
],
~~~
然后找到项目中全部的 StartCommand::class 文件中删除守护处理的相关代码:
```
// 守护处理
$daemon = Flag::bool(['d', 'daemon'], false);
if ($daemon) {
ProcessHelper::daemon();
}
```
## DatabaseListener/RedisListener 监听命令数据失效
- 把 DatabaseListener 监听的事件从 ExecuteEvent::class 修改为 ExecutedEvent::class
- 把 RedisListener 监听的事件从 ExecuteEvent::class 修改为 CalledEvent::class
';
升级指导
最后更新于:2022-04-02 07:08:13
[不兼容修改-001](%E4%B8%8D%E5%85%BC%E5%AE%B9%E4%BF%AE%E6%94%B9-001.md)
[不兼容修改-002](%E4%B8%8D%E5%85%BC%E5%AE%B9%E4%BF%AE%E6%94%B9-002.md)
[不兼容修改-003](%E4%B8%8D%E5%85%BC%E5%AE%B9%E4%BF%AE%E6%94%B9-003.md)
';
如何接入EasyWeChat
最后更新于:2022-04-02 07:08:11
## EasyWeChat 接入
国内中小型公司有大量的微信接入需求,[EasyWeChat](https://www.easywechat.com/) 是一个非常流行的微信开发库,由于该库是为 FPM 模式的传统框架而打造,因此很多 Swoole 用户不知道如何使用,下面详细介绍一下 [MixPHP v2.1](https://github.com/mix-php/mix) 中如何使用。
## Hook Guzzle
首先由于 [overtrue/wechat](https://github.com/overtrue/wechat) 是基于 GuzzleHttp 开发的,因为 GuzzleHttp 无法直接在 Swoole 中使用,所以需要先安装 Mix Guzzle Hook,该库能在不修改源码的情况下让 GuzzleHttp 协程化。
* [https://github.com/mix-php/guzzle-hook](https://github.com/mix-php/guzzle-hook)
## Request 类代理
由于 EasyWeChat 中使用的是 Symfony 框架的 Request 类,并且又不完全符合 [PSR-7](https://www.php-fig.org/psr/psr-7/) 规范,因此我们需要创建一个 Request 代理类:
```
request = $request;
}
public function get($key)
{
return $this->request->getAttribute($key);
}
public function getContent()
{
return $this->request->getBody()->getContents();
}
public function getContentType()
{
return $this->request->getHeaderLine('Content-Type');
}
public function getUri()
{
return $this->request->getUri()->__toString();
}
public function getMethod()
{
return $this->request->getMethod();
}
}
```
## 框架中使用
创建完成后就可在 MixPHP 的控制器中按如下代码使用:
```
public function index(ServerRequest $request, Response $response)
{
$config = [
'app_id' => 'wx3cf0f39249eb0xxx',
'secret' => 'f1c242f4f28f735d4687abb469072xxx',
'token' => 'TestToken',
'response_type' => 'array',
//...
];
$app = \EasyWeChat\Factory::officialAccount($config);
$app->request = new \App\Http\EasyWeChat\Request($request);
$wechatResponse = $app->server->serve();
$body = (new StreamFactory())->createStream($wechatResponse->getContent());
$code = $wechatResponse->getStatusCode();
$response->withBody($body)
->withStatus($code);
return $response;
}
```
';
输出大于2M的文件失败 (xlsx)
最后更新于:2022-04-02 07:08:08
## 输出大于2M的文件 (xlsx)
当使用 mix 输出生成的文件时,比如:图片,xlsx 等,因为 Swoole 默认只支持 2MB 的输出缓存区,因此需要配置 swoole 的 `buffer_output_size` 参数 [Swoole 文档](https://wiki.swoole.com/wiki/page/p-buffer_output_size.html)。
- 在 HTTP 服务器 StartCommand::class 代码中增加该配置:
```
$server->set([
// 配置你需要的长度
'buffer_output_size' => 20 * 1024 * 1024,
]);
```
';
form-data 上传文件失败
最后更新于:2022-04-02 07:08:06
## form-data 上传文件失败
因为默认 swoole 的 post body 只支持 2mb,因此需要配置 swoole 的 `package_max_length` 参数 [Swoole 文档](https://wiki.swoole.com/wiki/page/301.html)。
- 在 HTTP 服务器 StartCommand::class 代码中增加该配置:
```
$server->set([
// 配置你需要的长度
'package_max_length' => 20 * 1024 * 1024,
]);
```
';
如何设置跨域
最后更新于:2022-04-02 07:08:04
## 如何设置跨域
框架骨架提供了现成的跨域中间件代码,用户只需将该中间件配置到路由规则中即可实现跨域:
- 中间件源码:
[>> \App\Web\Middleware\CorsMiddleware::class <<](https://github.com/mix-php/mix-skeleton/tree/v2.1/app/Web/Middleware/CorsMiddleware.php)
- 在路由类依赖配置中设置中间件:
- [beans/route.php](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/route.php)
';
使用主从数据库
最后更新于:2022-04-02 07:08:02
## 使用主从数据库
>[success] Redis 也是同样的配置方法,只是 ::class 不同
框架中获取连接的主力方式是通过连接池,而 mix 的连接池是通过拨号器的方式由用户在拨号器自主创建连接,得益于这种灵活的设计,我们可以通过:新增一个从库单例 ConnectionPool::class 通过控制拨号器实例化不配置的连接而实现用户获取连接时获取到不同的数据库从库,从而达到主从的目的。
## 开始
>[danger] 使用该功能需先阅读 "核心功能 > 依赖注入" 章节
首先在 .env 增加从库使用的连接信息:
~~~
DATABASE_DSN_SLAVE1='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
DATABASE_USERNAME_SLAVE1=root
DATABASE_PASSWORD_SLAVE1=
DATABASE_DSN_SLAVE2='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
DATABASE_USERNAME_SLAVE2=root
DATABASE_PASSWORD_SLAVE2=
~~~
然后在 manifest.php 为每个从库配置一个 Connection::class 的依赖配置:
- 配置中使用了上面配置的新环境变量
~~~
// Database Slave1
[
// 名称
'name' => 'dbSlave1',
// 类路径
'class' => \Mix\Database\Connection::class,
// 初始化方法
'initMethod' => 'connect',
// 属性注入
'properties' => [
// 数据源格式
'dsn' => getenv('DATABASE_DSN_SLAVE1'),
// 数据库用户名
'username' => getenv('DATABASE_USERNAME_SLAVE1'),
// 数据库密码
'password' => getenv('DATABASE_PASSWORD_SLAVE1'),
// 驱动连接选项: http://php.net/manual/zh/pdo.setattribute.php
'attributes' => [
// 设置默认的提取模式: \PDO::FETCH_OBJ | \PDO::FETCH_ASSOC
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
// 超时
\PDO::ATTR_TIMEOUT => 5,
],
// 事件调度器
'eventDispatcher' => ['ref' => \Mix\Event\EventDispatcher::class],
],
],
// Database Slave2
[
// 名称
'name' => 'dbSlave2',
// 类路径
'class' => \Mix\Database\Connection::class,
// 初始化方法
'initMethod' => 'connect',
// 属性注入
'properties' => [
// 数据源格式
'dsn' => getenv('DATABASE_DSN_SLAVE2'),
// 数据库用户名
'username' => getenv('DATABASE_USERNAME_SLAVE2'),
// 数据库密码
'password' => getenv('DATABASE_PASSWORD_SLAVE2'),
// 驱动连接选项: http://php.net/manual/zh/pdo.setattribute.php
'attributes' => [
// 设置默认的提取模式: \PDO::FETCH_OBJ | \PDO::FETCH_ASSOC
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
// 超时
\PDO::ATTR_TIMEOUT => 5,
],
// 事件调度器
'eventDispatcher' => ['ref' => \Mix\Event\EventDispatcher::class],
],
],
~~~
新增一个拨号器 DatabaseSlaveDialer::class:
- 通过多个依赖名称轮转实例化数据库连接对象,实现创建不同从库的连接
~~~
*/
class DatabaseSlaveDialer implements DialerInterface
{
/**
* @var array
*/
protected $slaveBeanNames = ['dbSlave1', 'dbSlave2'];
/**
* 拨号
* @return Connection
*/
public function dial()
{
// 轮转
$beanName = array_pop($this->slaveBeanNames);
array_unshift($this->slaveBeanNames, $beanName);
// 创建一个连接并返回
return context()->get($beanName);
}
}
~~~
在 manifest.php 增加名称为 `dbPoolSlave` 的 ConnectionPool::class 依赖配置:
- 配置中 dialer 属性引用了 DatabaseSlaveDialer::class 完成拨号
~~~
// Database Slave 连接池
[
// 名称
'name' => 'dbPoolSlave',
// 作用域
'scope' => \Mix\Bean\BeanDefinition::SINGLETON,
// 类路径
'class' => \Mix\Database\Pool\ConnectionPool::class,
// 属性注入
'properties' => [
// 最多可空闲连接数
'maxIdle' => 5,
// 最大连接数
'maxActive' => 50,
// 拨号器
'dialer' => ['ref' => \App\Common\Dialers\DatabaseSlaveDialer::class],
],
],
// Database连接池拨号
[
// 类路径
'class' => \App\Common\Dialers\DatabaseSlaveDialer::class,
],
~~~
业务代码中调用:
>[danger] 与传统框架自动通过 SQL 切换主从的方式不同,mix 的这种主从实现其实只是扩展了多个从库调用的方式,因此需要用户自己在代码中选择使用主库还是从库。
- 当使用主库时,使用默认配置的 `dbPool` 依赖名称获取池实例
```
/** @var \Mix\Database\Pool\ConnectionPool $dbPool */
$dbPool = context()->get('dbPool');
$db = $dbPool->getConnection();
// ...
$db->release(); // 不手动释放的连接不会归还连接池,会在析构时丢弃
```
- 当使用从库时,使用刚刚上面配置的 `dbPoolSlave` 依赖名称获取池实例
~~~
/** @var \Mix\Database\Pool\ConnectionPool $dbPool */
$dbPool = context()->get('dbPoolSlave');
$db = $dbPool->getConnection();
//...
$db->release(); // 不手动释放的连接不会归还连接池,会在析构时丢弃
~~~
';
连接多个数据库
最后更新于:2022-04-02 07:07:59
## 连接多个数据库
>[success] Redis 也是同样的配置方法,只是 ::class 不同
通常需求中连接两个数据库有以下两种情况:
- 第二个库频繁使用
- 第二个库少量使用
## 第二个库频繁使用
配置一个新的 ConnectionPool::class 类的 Bean 调用另一个数据库
>[danger] 使用该功能需先阅读 "核心功能 > 依赖注入" 章节
首先在 .env 增加新的数据库使用的连接信息:
~~~
DATABASE_DSN_2='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
DATABASE_USERNAME_2=root
DATABASE_PASSWORD_2=
~~~
然后在 manifest.php 增加名称为 `db2` 的 Connection::class 依赖配置:
- 配置中使用了上面配置的新环境变量
~~~
// Database
[
// 名称
'name' => 'db2',
// 类路径
'class' => \Mix\Database\Connection::class,
// 初始化方法
'initMethod' => 'connect',
// 属性注入
'properties' => [
// 数据源格式
'dsn' => getenv('DATABASE_DSN_2'),
// 数据库用户名
'username' => getenv('DATABASE_USERNAME_2'),
// 数据库密码
'password' => getenv('DATABASE_PASSWORD_2'),
// 驱动连接选项: http://php.net/manual/zh/pdo.setattribute.php
'attributes' => [
// 设置默认的提取模式: \PDO::FETCH_OBJ | \PDO::FETCH_ASSOC
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
// 超时
\PDO::ATTR_TIMEOUT => 5,
],
// 事件调度器
'eventDispatcher' => ['ref' => \Mix\Event\EventDispatcher::class],
],
],
~~~
新增一个拨号器 Database2Dialer::class:
- 通过依赖名称 `db2` 实例化数据库连接对象
~~~
*/
class Database2Dialer implements DialerInterface
{
/**
* 拨号
* @return Connection
*/
public function dial()
{
// 创建一个连接并返回
return context()->get('db2');
}
}
~~~
在 manifest.php 增加名称为 `dbPool2` 的 ConnectionPool::class 依赖配置:
- 配置中 dialer 属性引用了 Database2Dialer::class 完成拨号
~~~
// Database连接池
[
// 名称
'name' => 'dbPool2',
// 作用域
'scope' => \Mix\Bean\BeanDefinition::SINGLETON,
// 类路径
'class' => \Mix\Database\Pool\ConnectionPool::class,
// 属性注入
'properties' => [
// 最多可空闲连接数
'maxIdle' => 5,
// 最大连接数
'maxActive' => 50,
// 拨号器
'dialer' => ['ref' => \App\Common\Dialers\Database2Dialer::class],
],
],
// Database连接池拨号
[
// 类路径
'class' => \App\Common\Dialers\Database2Dialer::class,
],
~~~
业务代码中调用:
~~~
/** @var \Mix\Database\Pool\ConnectionPool $dbPool */
$dbPool = context()->get('dbPool2');
$db = $dbPool->getConnection();
//...
$db->release(); // 不手动释放的连接不会归还连接池,会在析构时丢弃
~~~
## 第二个库少量使用
通过依赖注入直接实例化另一个数据库的连接
>[danger] 使用该功能需先阅读 "核心功能 > 依赖注入" 章节
首先在 .env 增加新的数据库使用的连接信息:
~~~
DATABASE_DSN_2='mysql:host=127.0.0.1;port=3306;charset=utf8;dbname=test'
DATABASE_USERNAME_2=root
DATABASE_PASSWORD_2=
~~~
然后在 manifest.php 增加名称为 `db2` 的 Connection::class 依赖配置:
- 配置中使用了上面配置的新环境变量
~~~
// Database
[
// 名称
'name' => 'db2',
// 类路径
'class' => \Mix\Database\Connection::class,
// 初始化方法
'initMethod' => 'connect',
// 属性注入
'properties' => [
// 数据源格式
'dsn' => getenv('DATABASE_DSN_2'),
// 数据库用户名
'username' => getenv('DATABASE_USERNAME_2'),
// 数据库密码
'password' => getenv('DATABASE_PASSWORD_2'),
// 驱动连接选项: http://php.net/manual/zh/pdo.setattribute.php
'attributes' => [
// 设置默认的提取模式: \PDO::FETCH_OBJ | \PDO::FETCH_ASSOC
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
// 超时
\PDO::ATTR_TIMEOUT => 5,
],
// 事件调度器
'eventDispatcher' => ['ref' => \Mix\Event\EventDispatcher::class],
],
],
~~~
业务代码中调用:
~~~
/** @var \Mix\Database\Connection $db */
$db = context()->get('db2');
~~~
';
如何利用CPU多核
最后更新于:2022-04-02 07:07:57
## 如何利用 CPU 多核
框架修改为单进程单线程后就与 node.js 一样,在部署方面具有了同样的优缺点:
- 优点:单线程天然对容器友好,使用容器化的公司会更方便
- 缺点:而众多采用传统服务器架构的中小型公司,就会存在如何利用 CPU 多核的问题
## 解决方法
> 在 `Linux v3.10` 或更高版本内核可用
端口复用,支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
* 允许多个套接字 bind()/listen() 同一个TCP/UDP端口
* 每一个线程拥有自己的服务器套接字
* 在服务器套接字上没有了锁的竞争
* 内核层面实现负载均衡
* 安全层面,监听同一个端口的套接字只能位于同一个用户下面
## HTTP
>[info] 多开后重启服务器就会比较繁琐,建议线上环境写一个 shell 脚本批量操作
通过多开的方式启动多个进程利用多核,根据CPU数量多开同样多的进程,在启动命令中增加 `-r` 参数开启端口复用:
~~~
php bin/mix.php http:start --port=9501 -r -d
php bin/mix.php http:start --port=9501 -r -d
php bin/mix.php http:start --port=9501 -r -d
~~~
通过 SLB 或者 Nginx 等反向代理工具代理到主机的 9501 端口
## WebSocket
>[info] 多开后重启服务器就会比较繁琐,建议线上环境写一个 shell 脚本批量操作
通过多开的方式启动多个进程利用多核,根据CPU数量多开同样多的进程,在启动命令中增加 `-r` 参数开启端口复用:
~~~
php bin/mix.php ws:start --port=9502 -r -d
php bin/mix.php ws:start --port=9502 -r -d
php bin/mix.php ws:start --port=9502 -r -d
~~~
通过 SLB 或者 Nginx 等反向代理工具代理到主机的 9502 端口
';
常见问题
最后更新于:2022-04-02 07:07:55
[如何利用CPU多核](%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8CPU%E5%A4%9A%E6%A0%B8.md)
[连接多个数据库](%E8%BF%9E%E6%8E%A5%E5%A4%9A%E4%B8%AA%E6%95%B0%E6%8D%AE%E5%BA%93.md)
[使用主从数据库](%E4%BD%BF%E7%94%A8%E4%B8%BB%E4%BB%8E%E6%95%B0%E6%8D%AE%E5%BA%93.md)
[如何设置跨域](%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AE%E8%B7%A8%E5%9F%9F.md)
[form-data 上传文件失败](form-data%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E5%A4%B1%E8%B4%A5.md)
[输出大于2M的文件失败 (xlsx)](%E8%BE%93%E5%87%BA%E5%A4%A7%E4%BA%8E2M%E7%9A%84%E6%96%87%E4%BB%B6.md)
[如何接入EasyWeChat](%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5EasyWeChat.md)
';
CalledEvent
最后更新于:2022-04-02 07:07:52
## Mix\Redis\Event\CalledEvent::class
该类是一个事件对象,当 Connection 每执行完一个命令时,会通过事件调度器调度一个该类的实例,该实例包含 command,arguments,time 三个字段。
## 使用
>[danger] 使用该功能需先阅读 "核心功能 > 事件" 章节
- 程序骨架中包含了 Listener 的范例代码:
[>> \App\Common\Listeners\RedisListener::class <<](https://github.com/mix-php/mix-skeleton/tree/v2.1/app/Common/Listeners/RedisListener.php)
- 在 manifest.php 中注册该监听器:
[>> beans/event <<](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/event.php)
';
Connection
最后更新于:2022-04-02 07:07:50
## Mix\Redis\Connection::class
该类使用魔术方法对 phpredis 扩展提供的方法做映射处理,可调用扩展内提供的所有方法。
## 依赖注入配置
- [beans/redis.php#L32](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/redis.php#L32)
## 直接获取连接
除了通过连接池,我们还能直接通过某个依赖配置直接获取连接实例:
> 由于 manifest.php 中 Connection::class 的依赖配置并没有设置 name 属性,因此 name = 命名空间
~~~
/** @var \Mix\Redis\Connection $db */
$db = context()->get(\Mix\Redis\Connection::class);
~~~
## 命令调用
这里只举例几个常用方法,更多方法请自行百度。
~~~
// 写入一个string值
$redis->set($key, $value);
// 写入一个带生存时间的string值
$redis->setex($key, 3600, $value);
// 在名称为key的list左边(头)添加一个值为value的 元素
$redis->lpush($key, $value);
~~~
## 长连接超时问题
Redis 配置文件内的 `timeout` 参数,决定了 sleep 多长时间的连接会被主动 kill,正常情况下是需要用户自己来处理连接超时的问题,但使用该组件,用户不需要处理,组件底层已经帮你处理了。
';
ConnectionPool
最后更新于:2022-04-02 07:07:48
## Mix\Redis\Pool\ConnectionPool::class
通常频繁调用的数据库都会以连接池的方式获取连接,mix 的数据库连接池是基于一个独立的连接池库 [mix/pool](https://github.com/mix-php/pool) 开发的,其大概流程为:
- 通过依赖注入获取连接池实例(单例)
- 通过连接池获取连接
- 池内无可用连接时调用连接池的拨号器 dialer 创建新连接
## 组件
使用 [composer]([https://www.phpcomposer.com/](https://www.phpcomposer.com/)) 安装:
~~~
composer require mix/redis
~~~
## 依赖注入配置
- [beans/redis.php](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/redis.php)
## 通过池获取连接
通过连接池获取连接:
```
/** @var \Mix\Redis\Pool\ConnectionPool $redisPool */
$redisPool = context()->get('redisPool');
$redis = $redisPool->getConnection();
// ...
$redis->release(); // 不手动释放的连接不会归还连接池,会在析构时丢弃
```
';
Redis
最后更新于:2022-04-02 07:07:46
[ConnectionPool](redisConnectionPool.md)
[Connection](RedisConnection.md)
[CalledEvent](CalledEvent.md)
';
ExecutedEvent
最后更新于:2022-04-02 07:07:43
## Mix\Database\Event\ExecutedEvent::class
该类是一个事件对象,当 Connection 每执行完一个 SQL 时,会通过事件调度器调度一个该类的实例,该实例包含 sql,bindings,time 三个字段。
## 使用
>[danger] 使用该功能需先阅读 "核心功能 > 事件" 章节
- 程序骨架中包含了 Listener 的范例代码:
[>> \App\Common\Listeners\DatabaseListener::class <<](https://github.com/mix-php/mix-skeleton/tree/v2.1/app/Common/Listeners/DatabaseListener.php)
- 在 manifest.php 中注册该监听器:
[>> beans/event <<](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/event.php)
';
QueryBuilder
最后更新于:2022-04-02 07:07:41
## Mix\Database\QueryBuilder::class
查询生成器,能帮助 Connection 生成 `select` 类型的语句,便于阅读性与开发效率,能生成大部分常用业务的 SQL。
>[info] insert、batchInsert、update、delete 类型的 SQL 与其他更为复杂的 SQL 依然需使用 Connection 开发
## Result
任何 Connection 连接对象的 table 方法,都可返回一个 QueryBuilder 实例。
```
// 返回全部
$ret = $db->table('users')->get();
// 返回第一行
$ret = $db->table('users')->first();
```
## Select
```
// 常规
$ret = $db->table('users')->select('name', 'email as user_email')->get();
// 函数
$ret = $db->table('users')->select('count(*)')->first();
```
## Join
全部关联方法有:
- join
- leftJoin
- rightJoin
- fullJoin
~~~
$ret = $db->table('users')
->join('orders', ['users.id', '=', 'orders.user_id'])
->select('users.*', 'orders.price')
->get();
~~~
多个关联条件:
> AND
```
->join('orders', [['users.id', '=', 'orders.user_id'], ['users.name', '=', 'orders.user_name']])
// JOIN orders ON users.id = orders.user_id AND users.name = orders.user_name
```
> OR
```
->join('orders', [['users.id', '=', 'orders.user_id'], ['or', ['users.name', '=', 'orders.user_name']]])
// JOIN orders ON users.id = orders.user_id OR users.name = orders.user_name
```
## Where
只有一个 `where` 方法:
```
// 一纬
$ret = $db->table('users')->where(['id', '=', 1])->get();
```
```
// 二维
$ret = $db->table('users')
->where([['id', '=', 1], ['name', '=', "Xiao Liu"]])
->get();
```
多个关联条件:
> AND
```
$ret = $db->table('users')
->where(['id', '=', 1])
->where(['name', '=', 'Xiao Liu'])
->get();
```
> OR
```
$ret = $db->table('users')
->where(['id', '=', 1])
->where(['or', ['id', '=', 2]])
->get();
```
> OR AND
```
$ret = $db->table('users')
->where(['id', '=', 1])
->where(['or', [['id', '=', 2], ['id', '=', 3]]])
->get();
// WHERE id = 1 OR (id = 2 AND id = 3)
```
> AND OR
```
$ret = $db->table('users')
->where(['id', '=', 1])
->where(['and', [['id', '=', 2], ['or', ['id', '=', 3]]]])
->get();
// WHERE id = 1 AND (id = 2 OR id = 3)
```
### In / NotIn
> IN
```
$ret = $db->table('users')
->where(['id', 'in', [1, 2]])
->get();
```
> NOT IN
```
$ret = $db->table('users')
->where(['id', 'not in', [1, 2]])
->get();
```
### Between / NotBetween
> BETWEEN
```
// 最新用法
$ret = $db->table('users')
->where(['id', 'between', [5, 10]])
->get();
```
> NOT BETWEEN
```
// 最新用法
$ret = $db->table('users')
->where(['id', 'not between', [5, 10]])
->get();
```
### Null / NotNull
> IS NULL
~~~
$ret = $db->table('users')
->where(['id', 'is null'])
->get();
~~~
> IS NOT NULL
~~~
$ret = $db->table('users')
->where(['id', 'is not null'])
->get();
~~~
## Order, Group, Limit, Offset
### orderBy
单个:
~~~
$ret = $db->table('users')
->orderBy('id', 'desc')
->get();
~~~
多个:
~~~
$ret = $db->table('users')
->orderBy('id', 'desc')
->orderBy('email', 'asc')
->get();
~~~
### groupBy / having
单个:
~~~
$ret = $db->table('orders')
->select('user_id', 'count(id) as counts')
->groupBy('user_id')
->having('count(id)', '>=', 1)
->get();
~~~
多个:
~~~
$ret = $db->table('orders')
->select('user_id', 'price', 'count(id) as counts')
->groupBy('user_id', 'price')
->having('count(id)', '>=', 1)
->get();
~~~
### limit / offset
~~~
$ret = $db->table('users')
->offset(10)
->limit(5)
->get();
~~~
';
Connection
最后更新于:2022-04-02 07:07:39
## Mix\Database\Connection::class
该类用于 MySQL 等关系型数据库的操作,语法简单明了,且具有独特的查询构造方式,可构造任何复杂的SQL。
>[success] 该类基于 PDO 扩展,[语句预处理](http://php.net/manual/zh/pdo.prepared-statements.php) 将帮助你免于SQL注入攻击
## 依赖注入配置
- [beans/database.php#L32](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/database.php#L32)
## 直接获取连接
除了通过连接池,我们还能直接通过某个依赖配置直接获取连接实例:
> 由于 manifest.php 中 Connection::class 的依赖配置并没有设置 name 属性,因此 name = 命名空间
~~~
/** @var \Mix\Database\Connection $db */
$db = context()->get(\Mix\Database\Connection::class);
~~~
## 插入
~~~
$data = [
'name' => 'xiaoliu',
'content' => 'hahahaha',
];
$success = $db->insert('post', $data)->execute();
// 获得刚插入数据的id
$insertId = $db->getLastInsertId();
~~~
## 批量插入
~~~
$data = [
['name' => 'xiaoliu', 'content' => 'hahahaha'],
['name' => 'xiaoliu', 'content' => 'hahahaha'],
['name' => 'xiaoliu', 'content' => 'hahahaha'],
['name' => 'xiaoliu', 'content' => 'hahahaha'],
];
$success = $db->batchInsert('post', $data)->execute();
// 获得受影响的行数
$affectedRows = $db->getRowCount();
~~~
## 更新
常规更新:
~~~
$set = [
'num' => 2,
'name' => 'xiaoliu2',
];
$where = [
['id', '=', 23],
];
$success = $db->update('post', $set, $where)->execute();
// 获得受影响的行数
$affectedRows = $db->getRowCount();
~~~
>[success] 更新 / 删除支持 where 的各种 OR / AND / IN / NOT IN / BETWEEN / NOT BETWEEN 组合,与 QueryBuilder 使用方式相同,如:$where = [['id', '=', 1], ['or', ['id', '=', 2]]];
自增、自减:
~~~
$set = [
'num' => ['+', 2],
'num1' => ['-', 1],
];
$where = [
['id', '=', 23],
];
$success = $db->update('post', $set, $where)->execute();
// 获得受影响的行数
$affectedRows = $db->getRowCount();
~~~
## 删除
~~~
$where = [
['id', '=', 23],
];
$success = $db->delete('post', $where)->execute();
// 获得受影响的行数
$affectedRows = $db->getRowCount();
~~~
## 查询
- 执行原生 `SQL`
>[info] 请不要直接把参数拼接在 SQL 内执行,带参数的 SQL 请使用参数绑定。
~~~
$rows = $db->prepare("SELECT * FROM `post`")->queryAll();
~~~
- 参数绑定
普通参数绑定:
~~~
$sql = "SELECT * FROM `post` WHERE id = :id AND name = :name";
$rows = $db->prepare($sql)->bindParams([
'id' => 28,
'name' => 'xiaoliu',
])->queryOne();
~~~
`IN`、`NOT IN` 数组参数绑定:
> `PDO` 扩展是不支持绑定数组参数的,所以 WHERE IN 都只能直接 `implode(', ',$array)` 拼接到 SQL 里面,MixPHP 帮你做了这一步,所以只需像下面这样使用。
~~~
$sql = "SELECT * FROM `post` WHERE id IN (:id)";
$rows = $db->prepare($sql)->bindParams([
'id' => [28, 29, 30],
])->queryAll();
~~~
## 查询组合
>[success] 可使用 QueryBuilder 替代该功能处理常用 SQL 的生成,而只在复杂 SQL 时使用该方法。
MixPHP 推崇原生 SQL 查询数据库,但由于原生 SQL 在动态 Where 时,做参数绑定会导致逻辑有些复杂,所以 MixPHP 封装了一个查询组合的功能,方便动态控制 Where 与 自动参数绑定。
用户可将整个 SQL 拆分为多个部分,每个部分可选择使用下例两个参数:
- `params` 字段内的值会绑定到对应的sql中。
- `if` 字段的值为 false 时,该段 SQL 会丢弃。
常用查询组合:
~~~
$rows = $db->prepare([
['SELECT * FROM `post`'],
['WHERE id = :id AND name = :name ORDER BY id ASC',
'params' => [
'id' => $this->id,
'name' => $this->name,
],
],
])->queryAll();
~~~
动态 Where 查询组合:
~~~
$rows = $db->prepare([
['SELECT * FROM `post` WHERE 1 = 1'],
['AND id = :id', 'params' => ['id' => $this->id], 'if' => isset($this->id)],
['AND name = :name', 'params' => ['name' => $this->name], 'if' => isset($this->name)],
['ORDER BY `post`.id ASC'],
])->queryAll();
~~~
>[info] WHERE 1 = 1 是一个小技巧,能避免没有 Where 时 SQL 错误的情况出现。
更复杂的查询组合,包含了:
- 动态 Join
- 动态 Where
- 分页
~~~
$rows = $db->prepare([
['SELECT *'],
['FROM `post`'],
[
'INNER JOIN `user` ON `user`.id = `post`.id',
'if' => isset($this->name),
],
['WHERE 1 = 1'],
[
'AND `post`.id = :id',
'params' => ['id' => $this->id],
'if' => isset($this->id),
],
[
'AND `user`.name = :name',
'params' => ['name' => $this->name],
'if' => isset($this->name),
],
['ORDER BY `post`.id ASC'],
['LIMIT :offset, :rows', 'params' => ['offset' => ($this->currentPage - 1) * $this->perPage, 'rows' => $this->perPage]],
])->queryAll();
~~~
## 使用 MySQL 函数、存储过程
```
$ret = $db->prepare('select :uuid')->bindParams([
'uuid' => $db::raw('uuid()'),
])->queryAll();
$ret = $db->batchInsert('test', [
['text' => 'aaa', 'created_at' => $db::raw('CURRENT_TIMESTAMP()'), 'uuid' => $db::raw('uuid()')],
['text' => 'bbb', 'created_at' => $db::raw('CURRENT_TIMESTAMP()'), 'uuid' => $db::raw('uuid()')],
])->execute();
```
## 查询返回结果集
返回多行,每行都是列名和值的关联数组。
> 没有结果返回空数组。
~~~
// 默认返回二维数组
$rows = $db->prepare("SELECT * FROM `post`")->queryAll();
// 指定返回对象数组
$rows = $db->prepare("SELECT * FROM `post`")->queryAll(\PDO::FETCH_OBJ);
~~~
返回一行 (第一行)。
> 没有结果返回 false。
~~~
// 默认返回数组
$row = $db->prepare("SELECT * FROM `post` WHERE id = 28")->queryOne();
// 指定返回对象
$row = $db->prepare("SELECT * FROM `post` WHERE id = 28")->queryOne(\PDO::FETCH_OBJ);
~~~
返回一列。
> 没有结果返回空数组。
~~~
// 第一列
$titles = $db->prepare("SELECT title FROM `post`")->queryColumn();
// 第二列
$titles = $db->prepare("SELECT * FROM `post`")->queryColumn(1);
~~~
返回一个标量值。
> 如果该查询没有结果则返回 false。
~~~
$count = $db->prepare("SELECT COUNT(*) FROM `post`")->queryScalar();
~~~
返回一个原生结果集 `PDOStatement` 对象。
> 通常在查询大量结果时,为了避免内存溢出时使用。
~~~
$statement = $db->prepare("SELECT * FROM `post`")->query();
while ($item = $statement->fetch()) {
var_dump($item);
}
~~~
## 返回 SQL 语句
`PDO` 扩展是无法获取最近执行的 SQL 的,所以这个功能是 MixPHP 通过参数构建出来的,这个在调试时是非常好用的功能。
~~~
$sql = $db->getLastSql();
~~~
## 返回 SQL 日志
返回一个数组,包含 sql, bindings, time 三个字段。
~~~
$sql = $db->getLastLog();
~~~
## 事务
手动事务:
~~~
$db->beginTransaction();
try {
$db->insert('test', [
'text' => '测试测试',
])->execute();
$db->commit();
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
~~~
自动事务:等同于上面的手动事务,抛出异常时自动回滚。
~~~
$db->transaction(function () use ($db) {
$db->insert('test', [
'text' => '测试测试',
])->execute();
});
~~~
## 长连接超时问题
MySQL 配置文件内的 `interactive_timeout` 与 `wait_timeout` 参数,决定了 sleep 多长时间的连接会被主动 kill,正常情况下是需要用户自己来处理连接超时的问题,但使用该组件,用户不需要处理,组件底层已经帮你处理了。
';
ConnectionPool
最后更新于:2022-04-02 07:07:37
## Mix\Database\Pool\ConnectionPool::class
通常频繁调用的数据库都会以连接池的方式获取连接,mix 的数据库连接池是基于一个独立的连接池库 [mix/pool](https://github.com/mix-php/pool) 开发的,其大概流程为:
- 通过依赖注入获取连接池实例(单例)
- 通过连接池获取连接
- 池内无可用连接时调用连接池的拨号器 dialer 创建新连接
## 组件
使用 [composer]([https://www.phpcomposer.com/](https://www.phpcomposer.com/)) 安装:
~~~
composer require mix/database
~~~
## 依赖注入配置
- [beans/database.php](https://github.com/mix-php/mix-skeleton/tree/v2.1/manifest/beans/database.php)
## 通过池获取连接
通过连接池获取连接:
```
/** @var \Mix\Database\Pool\ConnectionPool $dbPool */
$dbPool = context()->get('dbPool');
$db = $dbPool->getConnection();
// ...
$db->release(); // 不手动释放的连接不会归还连接池,会在析构时丢弃
```
';